= ({
- threads,
- className,
- selectedThreadId,
- onThreadIdSelect,
- onThreadSelect,
- onConfirmation,
- postFeed,
- updateThreadHandler,
- editAnnouncementPermission,
-}) => {
- const { t } = useTranslation();
- const { updatedFeedList: updatedThreads } =
- getFeedListWithRelativeDays(threads);
-
- const toggleReplyEditor = (id: string) => {
- onThreadIdSelect(selectedThreadId === id ? '' : id);
- };
-
- const activeAnnouncements = updatedThreads.filter(
- (thread) =>
- thread.announcement &&
- isActiveAnnouncement(
- thread.announcement?.startTime,
- thread.announcement?.endTime
- )
- );
-
- const inActiveAnnouncements = updatedThreads.filter(
- (thread) =>
- !(
- thread.announcement &&
- isActiveAnnouncement(
- thread.announcement?.startTime,
- thread.announcement?.endTime
- )
- )
- );
-
- const getAnnouncements = (announcements: Thread[]) => {
- return announcements.map((thread, index) => {
- const mainFeed = {
- message: thread.message,
- postTs: thread.threadTs,
- from: thread.createdBy,
- id: thread.id,
- reactions: thread.reactions,
- } as Post;
-
- const postLength = thread?.posts?.length || 0;
- const replies = thread.postsCount ? thread.postsCount - 1 : 0;
- const repliedUsers = [
- ...new Set((thread?.posts || []).map((f) => f.from)),
- ];
- const repliedUniqueUsersList = repliedUsers.slice(
- 0,
- postLength >= 3 ? 2 : 1
- );
- const lastPost = thread?.posts?.[postLength - 1];
-
- return (
-
-
-
-
-
onThreadSelect(thread.id)}
- />
-
- {postLength > 0 ? (
-
- {postLength > 1 ? (
-
- {Boolean(lastPost) &&
}
-
- onThreadSelect(thread.id)}
- />
-
-
- ) : null}
-
-
toggleReplyEditor(thread.id)}
- />
-
-
- ) : null}
- {selectedThreadId === thread.id ? (
-
- ) : null}
-
-
- );
- });
- };
-
- return (
-
- {getAnnouncements(activeAnnouncements)}
- {Boolean(inActiveAnnouncements.length) && (
- <>
-
- {t('label.inactive-announcement-plural')}
-
-
- >
- )}
-
- {getAnnouncements(inActiveAnnouncements)}
-
- );
-};
-
-export default AnnouncementThreads;
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityThreadPanel/announcement.less b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityThreadPanel/announcement.less
deleted file mode 100644
index 5d044abea7c..00000000000
--- a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityThreadPanel/announcement.less
+++ /dev/null
@@ -1,22 +0,0 @@
-/*
- * Copyright 2023 Collate.
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- * http://www.apache.org/licenses/LICENSE-2.0
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-@import url('../../../styles/variables.less');
-
-.announcement-thread-card {
- margin-top: 20px;
- padding-top: 8px;
- border-radius: 8px;
- border: 1px solid @announcement-border;
- background: @announcement-background;
-}
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/Shared/AnnouncementBadge.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/Shared/AnnouncementBadge.tsx
index 3aeefb6ccca..e463e6b0257 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/Shared/AnnouncementBadge.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/Shared/AnnouncementBadge.tsx
@@ -11,6 +11,7 @@
* limitations under the License.
*/
+import Icon from '@ant-design/icons/lib/components/Icon';
import { Typography } from 'antd';
import React from 'react';
import { useTranslation } from 'react-i18next';
@@ -22,11 +23,11 @@ const AnnouncementBadge = () => {
return (
-
+
-
+
{t('label.announcement')}
-
+
);
};
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/Shared/task-badge.less b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/Shared/task-badge.less
index c56b55f93fd..514ff8f6d86 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/Shared/task-badge.less
+++ b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/Shared/task-badge.less
@@ -20,16 +20,23 @@
background: @announcement-background-dark;
border-radius: 4px;
border: 1px solid @announcement-border;
- padding: 0 8px;
+ padding: 3px 8px;
display: flex;
align-items: center;
justify-content: center;
-}
-.announcement-badge {
- width: 14px;
- height: 14px;
- color: @primary-color;
+ .announcement-badge {
+ width: 14px;
+ height: 14px;
+ color: @announcement-border;
+ }
+
+ .announcement-text {
+ margin-left: 4px;
+ color: @announcement-border;
+ font-size: 12px;
+ font-weight: 700;
+ }
}
.task-badge {
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Announcement/Announcement.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/Announcement/Announcement.interface.ts
new file mode 100644
index 00000000000..20cf0aecb2c
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/components/Announcement/Announcement.interface.ts
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2024 Collate.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { Operation } from 'fast-json-patch';
+import { HTMLAttributes } from 'react';
+import {
+ AnnouncementDetails,
+ CreateThread,
+ ThreadType,
+} from '../../generated/api/feed/createThread';
+import { Post, Thread } from '../../generated/entity/feed/thread';
+import { ThreadUpdatedFunc } from '../../interface/feed.interface';
+import { ConfirmState } from '../ActivityFeed/ActivityFeedCard/ActivityFeedCard.interface';
+
+export type ThreadUpdatedFunction = (
+ threadId: string,
+ postId: string,
+ isThread: boolean,
+ data: Operation[]
+) => Promise
;
+
+export interface AnnouncementThreadProp extends HTMLAttributes {
+ threadLink: string;
+ threadType?: ThreadType;
+ open?: boolean;
+ postFeedHandler: (value: string, id: string) => Promise;
+ createThread: (data: CreateThread) => Promise;
+ updateThreadHandler: ThreadUpdatedFunction;
+ onCancel?: () => void;
+ deletePostHandler?: (
+ threadId: string,
+ postId: string,
+ isThread: boolean
+ ) => Promise;
+}
+
+export interface AnnouncementThreadBodyProp
+ extends HTMLAttributes,
+ Pick<
+ AnnouncementThreadProp,
+ | 'threadLink'
+ | 'updateThreadHandler'
+ | 'postFeedHandler'
+ | 'deletePostHandler'
+ > {
+ refetchThread: boolean;
+ editPermission: boolean;
+}
+
+export interface AnnouncementThreadListProp
+ extends HTMLAttributes,
+ Pick {
+ editPermission: boolean;
+ threads: Thread[];
+ postFeed: (value: string, id: string) => Promise;
+ onConfirmation: (data: ConfirmState) => void;
+}
+
+export interface AnnouncementFeedCardProp {
+ feed: Post;
+ task: Thread;
+ editPermission: boolean;
+ onConfirmation: (data: ConfirmState) => void;
+ updateThreadHandler: ThreadUpdatedFunction;
+ postFeed: (value: string, id: string) => Promise;
+}
+
+export interface AnnouncementFeedCardBodyProp
+ extends HTMLAttributes {
+ feed: Post;
+ editPermission: boolean;
+ entityLink?: string;
+ isThread?: boolean;
+ task: Thread;
+ announcementDetails?: AnnouncementDetails;
+ showRepliesButton?: boolean;
+ isReplyThreadOpen?: boolean;
+ onReply?: () => void;
+ onConfirmation: (data: ConfirmState) => void;
+ showReplyThread?: () => void;
+ updateThreadHandler: ThreadUpdatedFunc;
+}
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Announcement/AnnouncementFeedCard.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Announcement/AnnouncementFeedCard.component.tsx
new file mode 100644
index 00000000000..48479fcf23b
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/components/Announcement/AnnouncementFeedCard.component.tsx
@@ -0,0 +1,168 @@
+/*
+ * Copyright 2024 Collate.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { Card, Col, Row } from 'antd';
+import { AxiosError } from 'axios';
+import { Operation } from 'fast-json-patch';
+import React, { useEffect, useMemo, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import { Post } from '../../generated/entity/feed/thread';
+import { getFeedById } from '../../rest/feedsAPI';
+import { showErrorToast } from '../../utils/ToastUtils';
+import ActivityFeedEditor from '../ActivityFeed/ActivityFeedEditor/ActivityFeedEditor';
+import AnnouncementBadge from '../ActivityFeed/Shared/AnnouncementBadge';
+import ProfilePicture from '../common/ProfilePicture/ProfilePicture';
+import { AnnouncementFeedCardProp } from './Announcement.interface';
+import './announcement.less';
+import AnnouncementFeedCardBody from './AnnouncementFeedCardBody.component';
+
+const AnnouncementFeedCard = ({
+ feed,
+ task,
+ editPermission,
+ postFeed,
+ onConfirmation,
+ updateThreadHandler,
+}: AnnouncementFeedCardProp) => {
+ const { t } = useTranslation();
+ const [isReplyThreadVisible, setIsReplyThreadVisible] =
+ useState(false);
+ const [postFeedData, setPostFeedData] = useState([]);
+
+ const fetchAnnouncementThreadData = async () => {
+ try {
+ const res = await getFeedById(task.id);
+ setPostFeedData(res.data.posts ?? []);
+ } catch (err) {
+ showErrorToast(
+ err as AxiosError,
+ t('message.entity-fetch-error', {
+ entity: t('label.message-lowercase-plural'),
+ })
+ );
+ }
+ };
+
+ const handleUpdateThreadHandler = async (
+ threadId: string,
+ postId: string,
+ isThread: boolean,
+ data: Operation[]
+ ) => {
+ await updateThreadHandler(threadId, postId, isThread, data);
+
+ if (isReplyThreadVisible) {
+ fetchAnnouncementThreadData();
+ }
+ };
+
+ const handleSaveReply = async (value: string) => {
+ await postFeed(value, task.id);
+
+ if (isReplyThreadVisible) {
+ fetchAnnouncementThreadData();
+ }
+ };
+
+ const handleOpenReplyThread = () => {
+ fetchAnnouncementThreadData();
+ setIsReplyThreadVisible((prev) => !prev);
+ };
+
+ const postFeedReplies = useMemo(
+ () =>
+ postFeedData.map((reply) => (
+
+ )),
+ [
+ task,
+ postFeedData,
+ editPermission,
+ onConfirmation,
+ handleUpdateThreadHandler,
+ ]
+ );
+
+ // fetch announcement thread after delete action
+ useEffect(() => {
+ if (postFeedData.length !== task.postsCount) {
+ if (isReplyThreadVisible) {
+ fetchAnnouncementThreadData();
+ }
+ }
+ }, [task.postsCount]);
+
+ return (
+ <>
+
+
+
+
+
+ {isReplyThreadVisible && (
+
+
+
+
+
+
+
+
+ {postFeedReplies}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )}
+ >
+ );
+};
+
+export default AnnouncementFeedCard;
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Announcement/AnnouncementFeedCard.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Announcement/AnnouncementFeedCard.test.tsx
new file mode 100644
index 00000000000..5ebbb589bc7
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/components/Announcement/AnnouncementFeedCard.test.tsx
@@ -0,0 +1,165 @@
+/*
+ * Copyright 2024 Collate.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { act, fireEvent, render, screen } from '@testing-library/react';
+import React from 'react';
+import {
+ MOCK_ANNOUNCEMENT_DATA,
+ MOCK_ANNOUNCEMENT_FEED_DATA,
+} from '../../mocks/Announcement.mock';
+import { getFeedById } from '../../rest/feedsAPI';
+import AnnouncementFeedCard from './AnnouncementFeedCard.component';
+
+jest.mock('../../rest/feedsAPI', () => ({
+ getFeedById: jest.fn().mockImplementation(() => Promise.resolve()),
+}));
+
+jest.mock('./AnnouncementFeedCardBody.component', () =>
+ jest
+ .fn()
+ .mockImplementation(
+ ({ showReplyThread, updateThreadHandler, onConfirmation, onReply }) => (
+ <>
+ AnnouncementFeedCardBody
+ ShowReplyThreadButton
+
+ updateThreadHandler('threadId', 'postId', true, 'data')
+ }>
+ UpdateThreadHandlerButton
+
+ ConfirmationButton
+ ReplyButton
+ >
+ )
+ )
+);
+
+jest.mock('../ActivityFeed/ActivityFeedEditor/ActivityFeedEditor', () => {
+ return jest.fn().mockImplementation(({ onSave }) => (
+ <>
+ ActivityFeedEditor
+ onSave('changesValue')}>onSaveReply
+ >
+ ));
+});
+
+jest.mock('../ActivityFeed/Shared/AnnouncementBadge', () => {
+ return jest.fn().mockReturnValue(AnnouncementBadge
);
+});
+
+jest.mock('../common/ProfilePicture/ProfilePicture', () => {
+ return jest.fn().mockReturnValue(ProfilePicture
);
+});
+
+jest.mock('../../utils/ToastUtils', () => ({
+ showErrorToast: jest.fn(),
+}));
+
+const mockProps = {
+ feed: {
+ message: 'Cypress announcement',
+ postTs: 1714026576902,
+ from: 'admin',
+ id: '36ea94c9-7f12-489c-94df-56cbefe14b2f',
+ reactions: [],
+ },
+ task: MOCK_ANNOUNCEMENT_DATA.data[0],
+ editPermission: true,
+ postFeed: jest.fn(),
+ onConfirmation: jest.fn(),
+ updateThreadHandler: jest.fn(),
+};
+
+describe('Test AnnouncementFeedCard Component', () => {
+ it('should render AnnouncementFeedCard component', () => {
+ render( );
+
+ expect(screen.getByText('AnnouncementBadge')).toBeInTheDocument();
+ expect(screen.getByText('AnnouncementFeedCardBody')).toBeInTheDocument();
+ });
+
+ it('should trigger onConfirmation', () => {
+ render( );
+
+ fireEvent.click(screen.getByText('ConfirmationButton'));
+
+ expect(mockProps.onConfirmation).toHaveBeenCalled();
+ });
+
+ it('should trigger updateThreadHandler without fetchAnnouncementThreadData when replyThread is closed', () => {
+ render( );
+
+ fireEvent.click(screen.getByText('UpdateThreadHandlerButton'));
+
+ expect(mockProps.updateThreadHandler).toHaveBeenCalledWith(
+ 'threadId',
+ 'postId',
+ true,
+ 'data'
+ );
+ expect(getFeedById).not.toHaveBeenCalled();
+ });
+
+ it('should trigger updateThreadHandler with fetchAnnouncementThreadData when replyThread is open', () => {
+ render( );
+
+ act(() => {
+ fireEvent.click(screen.getByText('ShowReplyThreadButton'));
+ });
+
+ expect(getFeedById).toHaveBeenCalledWith(MOCK_ANNOUNCEMENT_DATA.data[0].id);
+
+ fireEvent.click(screen.getByText('UpdateThreadHandlerButton'));
+
+ expect(mockProps.updateThreadHandler).toHaveBeenCalledWith(
+ 'threadId',
+ 'postId',
+ true,
+ 'data'
+ );
+ expect(getFeedById).toHaveBeenCalledWith(MOCK_ANNOUNCEMENT_DATA.data[0].id);
+ });
+
+ it('should trigger onReply with fetchAnnouncementThreadData', async () => {
+ (getFeedById as jest.Mock).mockImplementationOnce(() =>
+ Promise.resolve({ data: MOCK_ANNOUNCEMENT_FEED_DATA })
+ );
+
+ await act(async () => {
+ render( );
+ });
+
+ await act(async () => {
+ fireEvent.click(screen.getByText('ReplyButton'));
+ });
+
+ expect(getFeedById).toHaveBeenCalledWith(MOCK_ANNOUNCEMENT_DATA.data[0].id);
+
+ expect(screen.getByTestId('replies')).toBeInTheDocument();
+
+ expect(screen.getAllByText('AnnouncementFeedCardBody')).toHaveLength(5);
+
+ expect(screen.getByText('ProfilePicture')).toBeInTheDocument();
+
+ expect(screen.getByText('ActivityFeedEditor')).toBeInTheDocument();
+
+ fireEvent.click(screen.getByText('onSaveReply'));
+
+ expect(mockProps.postFeed).toHaveBeenCalledWith(
+ 'changesValue',
+ '36ea94c9-7f12-489c-94df-56cbefe14b2f'
+ );
+
+ expect(getFeedById).toHaveBeenCalledWith(MOCK_ANNOUNCEMENT_DATA.data[0].id);
+ });
+});
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Announcement/AnnouncementFeedCardBody.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Announcement/AnnouncementFeedCardBody.component.tsx
new file mode 100644
index 00000000000..1ea11feb865
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/components/Announcement/AnnouncementFeedCardBody.component.tsx
@@ -0,0 +1,282 @@
+/*
+ * Copyright 2024 Collate.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import Icon from '@ant-design/icons/lib/components/Icon';
+import { Avatar, Button, Col, Popover, Row } from 'antd';
+import classNames from 'classnames';
+import { compare, Operation } from 'fast-json-patch';
+import { isEmpty, isUndefined } from 'lodash';
+import React, { useEffect, useMemo, useRef, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import { ReactComponent as ArrowBottom } from '../../assets/svg/ic-arrow-down.svg';
+import { ReactionOperation } from '../../enums/reactions.enum';
+import {
+ AnnouncementDetails,
+ ThreadType,
+} from '../../generated/api/feed/createThread';
+import { Post } from '../../generated/entity/feed/thread';
+import { Reaction, ReactionType } from '../../generated/type/reaction';
+import { useApplicationStore } from '../../hooks/useApplicationStore';
+import {
+ getEntityField,
+ getEntityFQN,
+ getEntityType,
+} from '../../utils/FeedUtils';
+import FeedCardBody from '../ActivityFeed/ActivityFeedCard/FeedCardBody/FeedCardBody';
+import FeedCardHeader from '../ActivityFeed/ActivityFeedCard/FeedCardHeader/FeedCardHeader';
+import PopoverContent from '../ActivityFeed/ActivityFeedCard/PopoverContent';
+import UserPopOverCard from '../common/PopOverCard/UserPopOverCard';
+import ProfilePicture from '../common/ProfilePicture/ProfilePicture';
+import EditAnnouncementModal from '../Modals/AnnouncementModal/EditAnnouncementModal';
+import { AnnouncementFeedCardBodyProp } from './Announcement.interface';
+import './announcement.less';
+
+const AnnouncementFeedCardBody = ({
+ feed,
+ entityLink,
+ isThread,
+ editPermission,
+ showRepliesButton = true,
+ showReplyThread,
+ onReply,
+ announcementDetails,
+ onConfirmation,
+ updateThreadHandler,
+ task,
+ isReplyThreadOpen,
+}: AnnouncementFeedCardBodyProp) => {
+ const { t } = useTranslation();
+ const entityType = getEntityType(entityLink ?? '');
+ const entityFQN = getEntityFQN(entityLink ?? '');
+ const entityField = getEntityField(entityLink ?? '');
+ const { currentUser } = useApplicationStore();
+ const containerRef = useRef(null);
+ const [feedDetail, setFeedDetail] = useState(feed);
+
+ const [visible, setVisible] = useState(false);
+ const [isEditAnnouncement, setIsEditAnnouncement] = useState(false);
+ const [isEditPost, setIsEditPost] = useState(false);
+
+ const isAuthor = feedDetail.from === currentUser?.name;
+
+ const { id: threadId, type: feedType, posts } = task;
+
+ const repliesPostAvatarGroup = useMemo(() => {
+ return (
+
+ {(posts ?? []).map((u) => (
+
+ ))}
+
+ );
+ }, [posts]);
+
+ const onFeedUpdate = (data: Operation[]) => {
+ updateThreadHandler(
+ threadId ?? feedDetail.id,
+ feedDetail.id,
+ Boolean(isThread),
+ data
+ );
+ };
+
+ const onReactionSelect = (
+ reactionType: ReactionType,
+ reactionOperation: ReactionOperation
+ ) => {
+ let updatedReactions = feedDetail.reactions || [];
+ if (reactionOperation === ReactionOperation.ADD) {
+ const reactionObject = {
+ reactionType,
+ user: {
+ id: currentUser?.id as string,
+ },
+ };
+
+ updatedReactions = [...updatedReactions, reactionObject as Reaction];
+ } else {
+ updatedReactions = updatedReactions.filter(
+ (reaction) =>
+ !(
+ reaction.reactionType === reactionType &&
+ reaction.user.id === currentUser?.id
+ )
+ );
+ }
+
+ const patch = compare(
+ { ...feedDetail, reactions: [...(feedDetail.reactions || [])] },
+ {
+ ...feedDetail,
+ reactions: updatedReactions,
+ }
+ );
+
+ if (!isEmpty(patch)) {
+ onFeedUpdate(patch);
+ }
+ };
+
+ const handleAnnouncementUpdate = (
+ title: string,
+ announcement: AnnouncementDetails
+ ) => {
+ const existingAnnouncement = {
+ ...feedDetail,
+ announcement: announcementDetails,
+ };
+
+ const updatedAnnouncement = {
+ ...feedDetail,
+ message: title,
+ announcement,
+ };
+
+ const patch = compare(existingAnnouncement, updatedAnnouncement);
+
+ if (!isEmpty(patch)) {
+ onFeedUpdate(patch);
+ }
+ setIsEditAnnouncement(false);
+ };
+
+ const handlePostUpdate = (message: string) => {
+ const updatedPost = { ...feedDetail, message };
+
+ const patch = compare(feedDetail, updatedPost);
+
+ if (!isEmpty(patch)) {
+ onFeedUpdate(patch);
+ }
+ setIsEditPost(false);
+ };
+
+ const handleThreadEdit = () => {
+ if (announcementDetails) {
+ setIsEditAnnouncement(true);
+ } else {
+ setIsEditPost(true);
+ }
+ };
+
+ const handleVisibleChange = (newVisible: boolean) => setVisible(newVisible);
+
+ const onHide = () => setVisible(false);
+
+ useEffect(() => {
+ setFeedDetail(feed);
+ }, [feed]);
+
+ return (
+
+
+ }
+ destroyTooltipOnHide={{ keepParent: false }}
+ getPopupContainer={() => containerRef.current || document.body}
+ key="reaction-options-popover"
+ open={visible && !isEditPost}
+ overlayClassName="ant-popover-feed"
+ placement="topRight"
+ trigger="hover"
+ onOpenChange={handleVisibleChange}>
+
+
+
+
+ {showRepliesButton && repliesPostAvatarGroup}
+
+
+
+
+ setIsEditPost(false)}
+ onPostUpdate={handlePostUpdate}
+ onReactionSelect={onReactionSelect}
+ />
+ {!isEmpty(task.posts) && showRepliesButton ? (
+
+ {`${task.postsCount} ${t('label.reply-lowercase-plural')}`}
+
+
+
+ ) : null}
+
+
+
+
+
+ {isEditAnnouncement && announcementDetails && (
+
setIsEditAnnouncement(false)}
+ onConfirm={handleAnnouncementUpdate}
+ />
+ )}
+
+ );
+};
+
+export default AnnouncementFeedCardBody;
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Announcement/AnnouncementFeedCardBody.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Announcement/AnnouncementFeedCardBody.test.tsx
new file mode 100644
index 00000000000..61eed6fea88
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/components/Announcement/AnnouncementFeedCardBody.test.tsx
@@ -0,0 +1,180 @@
+/*
+ * Copyright 2024 Collate.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { fireEvent, render, screen } from '@testing-library/react';
+import React from 'react';
+import { ReactionOperation } from '../../enums/reactions.enum';
+import { Thread } from '../../generated/entity/feed/thread';
+import { ReactionType } from '../../generated/type/reaction';
+import { MOCK_ANNOUNCEMENT_DATA } from '../../mocks/Announcement.mock';
+import { mockUserData } from '../../mocks/MyDataPage.mock';
+import AnnouncementFeedCardBody from './AnnouncementFeedCardBody.component';
+
+jest.mock('../../utils/FeedUtils', () => ({
+ getEntityField: jest.fn(),
+ getEntityFQN: jest.fn(),
+ getEntityType: jest.fn(),
+}));
+
+jest.mock('../../hooks/useApplicationStore', () => ({
+ useApplicationStore: jest.fn(() => ({
+ currentUser: mockUserData,
+ })),
+}));
+
+jest.mock('../ActivityFeed/ActivityFeedCard/FeedCardBody/FeedCardBody', () =>
+ jest.fn().mockImplementation(({ onPostUpdate, onReactionSelect }) => (
+ <>
+ FeedCardBody
+ onPostUpdate('message')}>PostUpdateButton
+
+ onReactionSelect(ReactionType.Confused, ReactionOperation.ADD)
+ }>
+ ReactionSelectButton
+
+ >
+ ))
+);
+
+jest.mock(
+ '../ActivityFeed/ActivityFeedCard/FeedCardHeader/FeedCardHeader',
+ () => {
+ return jest.fn().mockReturnValue(FeedCardHeader
);
+ }
+);
+
+jest.mock('../common/PopOverCard/UserPopOverCard', () => {
+ return jest.fn().mockImplementation(() => UserPopOverCard
);
+});
+jest.mock('../common/ProfilePicture/ProfilePicture', () => {
+ return jest.fn().mockImplementation(() => ProfilePicture
);
+});
+
+jest.mock('../Modals/AnnouncementModal/EditAnnouncementModal', () => {
+ return jest.fn().mockImplementation(() => EditAnnouncementModal
);
+});
+
+jest.mock('../ActivityFeed/ActivityFeedCard/PopoverContent', () => {
+ return jest.fn().mockImplementation(() => PopoverContent
);
+});
+
+const mockFeedCardProps = {
+ feed: {
+ from: 'admin',
+ id: '36ea94c9-7f12-489c-94df-56cbefe14b2f',
+ message: 'Cypress announcement',
+ postTs: 1714026576902,
+ reactions: [],
+ },
+ task: MOCK_ANNOUNCEMENT_DATA.data[0],
+ entityLink:
+ '<#E::database::cy-database-service-373851.cypress-database-1714026557974>',
+ isThread: true,
+ editPermission: true,
+ isReplyThreadOpen: false,
+ updateThreadHandler: jest.fn(),
+ onReply: jest.fn(),
+ onConfirmation: jest.fn(),
+ showReplyThread: jest.fn(),
+};
+
+describe('Test AnnouncementFeedCardBody Component', () => {
+ it('Check if AnnouncementFeedCardBody component has all child components', async () => {
+ render( );
+ const feedCardHeader = screen.getByText('FeedCardHeader');
+ const feedCardBody = screen.getByText('FeedCardBody');
+ const profilePictures = screen.getAllByText('ProfilePicture');
+ const userPopOverCard = screen.getByText('UserPopOverCard');
+
+ expect(feedCardHeader).toBeInTheDocument();
+ expect(feedCardBody).toBeInTheDocument();
+ expect(userPopOverCard).toBeInTheDocument();
+ expect(profilePictures).toHaveLength(4);
+ });
+
+ it('should trigger onPostUpdate from FeedCardBody', async () => {
+ render( );
+
+ const postUpdateButton = screen.getByText('PostUpdateButton');
+
+ fireEvent.click(postUpdateButton);
+
+ expect(mockFeedCardProps.updateThreadHandler).toHaveBeenCalledWith(
+ MOCK_ANNOUNCEMENT_DATA.data[0].id,
+ MOCK_ANNOUNCEMENT_DATA.data[0].id,
+ true,
+ [{ op: 'replace', path: '/message', value: 'message' }]
+ );
+ });
+
+ it('should trigger ReactionSelectButton from FeedCardBody', async () => {
+ render( );
+
+ const reactionSelectButton = screen.getByText('ReactionSelectButton');
+
+ fireEvent.click(reactionSelectButton);
+
+ expect(mockFeedCardProps.updateThreadHandler).toHaveBeenCalledWith(
+ MOCK_ANNOUNCEMENT_DATA.data[0].id,
+ MOCK_ANNOUNCEMENT_DATA.data[0].id,
+ true,
+ [
+ {
+ op: 'add',
+ path: '/reactions/0',
+ value: {
+ reactionType: 'confused',
+ user: {
+ id: '123',
+ },
+ },
+ },
+ ]
+ );
+ });
+
+ it('should trigger postReplies button', async () => {
+ render( );
+
+ const showReplyThread = screen.getByTestId('show-reply-thread');
+
+ fireEvent.click(showReplyThread);
+
+ expect(mockFeedCardProps.showReplyThread).toHaveBeenCalled();
+ });
+
+ it('should not render PostReplies Profile Picture if showRepliesButton is false', async () => {
+ render(
+
+ );
+
+ const profilePictures = screen.queryByText('ProfilePicture');
+ const showReplyThread = screen.queryByTestId('show-reply-thread');
+
+ expect(profilePictures).not.toBeInTheDocument();
+ expect(showReplyThread).not.toBeInTheDocument();
+ });
+
+ it('should not render PostReplies button if repliesPost is empty', async () => {
+ render(
+
+ );
+
+ const showReplyThread = screen.queryByTestId('show-reply-thread');
+
+ expect(showReplyThread).not.toBeInTheDocument();
+ });
+});
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Announcement/AnnouncementThreadBody.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Announcement/AnnouncementThreadBody.component.tsx
new file mode 100644
index 00000000000..d7410fd7bbb
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/components/Announcement/AnnouncementThreadBody.component.tsx
@@ -0,0 +1,152 @@
+/*
+ * Copyright 2024 Collate.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { Typography } from 'antd';
+import { AxiosError } from 'axios';
+import { Operation } from 'fast-json-patch';
+import { isEmpty } from 'lodash';
+import React, { useEffect, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import { confirmStateInitialValue } from '../../constants/Feeds.constants';
+import { ERROR_PLACEHOLDER_TYPE } from '../../enums/common.enum';
+import { FeedFilter } from '../../enums/mydata.enum';
+import { Thread, ThreadType } from '../../generated/entity/feed/thread';
+import { getAllFeeds } from '../../rest/feedsAPI';
+import { showErrorToast } from '../../utils/ToastUtils';
+import { ConfirmState } from '../ActivityFeed/ActivityFeedCard/ActivityFeedCard.interface';
+import ErrorPlaceHolder from '../common/ErrorWithPlaceholder/ErrorPlaceHolder';
+import ConfirmationModal from '../Modals/ConfirmationModal/ConfirmationModal';
+import { AnnouncementThreadBodyProp } from './Announcement.interface';
+import AnnouncementThreads from './AnnouncementThreads';
+
+const AnnouncementThreadBody = ({
+ threadLink,
+ refetchThread,
+ editPermission,
+ postFeedHandler,
+ deletePostHandler,
+ updateThreadHandler,
+}: AnnouncementThreadBodyProp) => {
+ const { t } = useTranslation();
+ const [threads, setThreads] = useState([]);
+ const [confirmationState, setConfirmationState] = useState(
+ confirmStateInitialValue
+ );
+ const [isThreadLoading, setIsThreadLoading] = useState(true);
+
+ const getThreads = async (after?: string) => {
+ setIsThreadLoading(true);
+
+ try {
+ const res = await getAllFeeds(
+ threadLink,
+ after,
+ ThreadType.Announcement,
+ FeedFilter.ALL
+ );
+
+ setThreads(res.data);
+ } catch (error) {
+ showErrorToast(
+ error as AxiosError,
+ t('server.entity-fetch-error', {
+ entity: t('label.thread-plural-lowercase'),
+ })
+ );
+ } finally {
+ setIsThreadLoading(false);
+ }
+ };
+
+ const loadNewThreads = () => {
+ setTimeout(() => {
+ getThreads();
+ }, 500);
+ };
+
+ const onDiscard = () => {
+ setConfirmationState(confirmStateInitialValue);
+ };
+
+ const onPostDelete = async (): Promise => {
+ if (confirmationState.postId && confirmationState.threadId) {
+ await deletePostHandler?.(
+ confirmationState.threadId,
+ confirmationState.postId,
+ confirmationState.isThread
+ );
+ }
+ onDiscard();
+ loadNewThreads();
+ };
+
+ const onConfirmation = (data: ConfirmState) => {
+ setConfirmationState(data);
+ };
+
+ const postFeed = async (value: string, id: string): Promise => {
+ await postFeedHandler?.(value, id);
+ loadNewThreads();
+ };
+
+ const onUpdateThread = async (
+ threadId: string,
+ postId: string,
+ isThread: boolean,
+ data: Operation[]
+ ): Promise => {
+ await updateThreadHandler(threadId, postId, isThread, data);
+ loadNewThreads();
+ };
+
+ useEffect(() => {
+ getThreads();
+ }, [threadLink, refetchThread]);
+
+ if (isEmpty(threads) && !isThreadLoading) {
+ return (
+
+
+ {t('message.no-announcement-message')}
+
+
+ );
+ }
+
+ return (
+
+ );
+};
+
+export default AnnouncementThreadBody;
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Announcement/AnnouncementThreadBody.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Announcement/AnnouncementThreadBody.test.tsx
new file mode 100644
index 00000000000..3ef4d44a696
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/components/Announcement/AnnouncementThreadBody.test.tsx
@@ -0,0 +1,276 @@
+/*
+ * Copyright 2024 Collate.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { fireEvent, render, screen } from '@testing-library/react';
+import React from 'react';
+import { act } from 'react-test-renderer';
+import { MOCK_ANNOUNCEMENT_DATA } from '../../mocks/Announcement.mock';
+import { getAllFeeds } from '../../rest/feedsAPI';
+import AnnouncementThreadBody from './AnnouncementThreadBody.component';
+
+jest.mock('../../rest/feedsAPI', () => ({
+ getAllFeeds: jest.fn().mockImplementation(() => Promise.resolve()),
+}));
+
+jest.mock('./AnnouncementThreads', () =>
+ jest
+ .fn()
+ .mockImplementation(({ postFeed, updateThreadHandler, onConfirmation }) => (
+ <>
+ AnnouncementThreads
+ postFeed('valueId', 'id')}>
+ PostFeedButton
+
+
+ onConfirmation({
+ state: true,
+ threadId: 'threadId',
+ postId: 'postId',
+ isThread: false,
+ })
+ }>
+ ConfirmationButton
+
+
+ updateThreadHandler('threadId', 'postId', true, {
+ op: 'replace',
+ path: '/announcement/description',
+ value: 'Cypress announcement description.',
+ })
+ }>
+ UpdateThreadButton
+
+ >
+ ))
+);
+
+jest.mock('../Modals/ConfirmationModal/ConfirmationModal', () =>
+ jest.fn().mockImplementation(({ visible, onConfirm, onCancel }) => (
+ <>
+ {visible ? 'Confirmation Modal is open' : 'Confirmation Modal is close'}
+ Confirm Confirmation Modal
+ Cancel Confirmation Modal
+ >
+ ))
+);
+
+jest.mock('../common/ErrorWithPlaceholder/ErrorPlaceHolder', () => {
+ return jest.fn().mockReturnValue(ErrorPlaceHolder
);
+});
+
+jest.mock('../../utils/ToastUtils', () => ({
+ showErrorToast: jest.fn(),
+}));
+
+const mockProps = {
+ threadLink: 'threadLink',
+ refetchThread: false,
+ editPermission: true,
+ postFeedHandler: jest.fn(),
+ deletePostHandler: jest.fn(),
+ updateThreadHandler: jest.fn(),
+};
+
+describe('Test AnnouncementThreadBody Component', () => {
+ it('should call getAllFeeds when component is mount', async () => {
+ render( );
+
+ expect(getAllFeeds).toHaveBeenCalledWith(
+ 'threadLink',
+ undefined,
+ 'Announcement',
+ 'ALL'
+ );
+ });
+
+ it('should render empty placeholder when data is not there', async () => {
+ await act(async () => {
+ render( );
+ });
+
+ const emptyPlaceholder = screen.getByText('ErrorPlaceHolder');
+
+ expect(emptyPlaceholder).toBeInTheDocument();
+ });
+
+ it('Check if all child elements rendered', async () => {
+ (getAllFeeds as jest.Mock).mockImplementationOnce(() =>
+ Promise.resolve(MOCK_ANNOUNCEMENT_DATA)
+ );
+
+ await act(async () => {
+ render( );
+ });
+
+ const component = screen.getByTestId('announcement-thread-body');
+ const announcementThreads = screen.getByText('AnnouncementThreads');
+ const confirmationModal = screen.getByText('Confirmation Modal is close');
+
+ expect(component).toBeInTheDocument();
+ expect(confirmationModal).toBeInTheDocument();
+ expect(announcementThreads).toBeInTheDocument();
+ });
+
+ // Confirmation Modal
+
+ it('should open delete confirmation modal', async () => {
+ (getAllFeeds as jest.Mock).mockImplementationOnce(() =>
+ Promise.resolve(MOCK_ANNOUNCEMENT_DATA)
+ );
+
+ await act(async () => {
+ render( );
+ });
+
+ const confirmationCloseModal = screen.getByText(
+ 'Confirmation Modal is close'
+ );
+
+ expect(confirmationCloseModal).toBeInTheDocument();
+
+ const confirmationButton = screen.getByText('ConfirmationButton');
+ act(() => {
+ fireEvent.click(confirmationButton);
+ });
+ const confirmationOpenModal = screen.getByText(
+ 'Confirmation Modal is open'
+ );
+
+ expect(confirmationOpenModal).toBeInTheDocument();
+ });
+
+ it('should trigger onConfirm in confirmation modal', async () => {
+ (getAllFeeds as jest.Mock).mockImplementationOnce(() =>
+ Promise.resolve(MOCK_ANNOUNCEMENT_DATA)
+ );
+
+ await act(async () => {
+ render( );
+ });
+
+ const confirmationButton = screen.getByText('ConfirmationButton');
+ act(() => {
+ fireEvent.click(confirmationButton);
+ });
+
+ expect(screen.getByText('Confirmation Modal is open')).toBeInTheDocument();
+
+ const confirmConfirmationButton = screen.getByText(
+ 'Confirm Confirmation Modal'
+ );
+
+ act(() => {
+ fireEvent.click(confirmConfirmationButton);
+ });
+
+ expect(mockProps.deletePostHandler).toHaveBeenCalledWith(
+ 'threadId',
+ 'postId',
+ false
+ );
+
+ expect(getAllFeeds).toHaveBeenCalledWith(
+ 'threadLink',
+ undefined,
+ 'Announcement',
+ 'ALL'
+ );
+ });
+
+ it('should trigger onCancel in confirmation modal', async () => {
+ (getAllFeeds as jest.Mock).mockImplementationOnce(() =>
+ Promise.resolve(MOCK_ANNOUNCEMENT_DATA)
+ );
+
+ await act(async () => {
+ render( );
+ });
+
+ const confirmationButton = screen.getByText('ConfirmationButton');
+ act(() => {
+ fireEvent.click(confirmationButton);
+ });
+
+ expect(screen.getByText('Confirmation Modal is open')).toBeInTheDocument();
+
+ const cancelConfirmationButton = screen.getByText(
+ 'Cancel Confirmation Modal'
+ );
+
+ act(() => {
+ fireEvent.click(cancelConfirmationButton);
+ });
+
+ expect(screen.getByText('Confirmation Modal is close')).toBeInTheDocument();
+ });
+
+ // AnnouncementThreads Component
+
+ it('should trigger postFeedHandler', async () => {
+ (getAllFeeds as jest.Mock).mockImplementationOnce(() =>
+ Promise.resolve(MOCK_ANNOUNCEMENT_DATA)
+ );
+
+ await act(async () => {
+ render( );
+ });
+
+ const postFeedButton = screen.getByText('PostFeedButton');
+ act(() => {
+ fireEvent.click(postFeedButton);
+ });
+
+ expect(mockProps.postFeedHandler).toHaveBeenCalledWith('valueId', 'id');
+
+ expect(getAllFeeds).toHaveBeenCalledWith(
+ 'threadLink',
+ undefined,
+ 'Announcement',
+ 'ALL'
+ );
+ });
+
+ it('should trigger updateThreadHandler', async () => {
+ (getAllFeeds as jest.Mock).mockImplementationOnce(() =>
+ Promise.resolve(MOCK_ANNOUNCEMENT_DATA)
+ );
+
+ await act(async () => {
+ render( );
+ });
+
+ const postFeedButton = screen.getByText('UpdateThreadButton');
+ act(() => {
+ fireEvent.click(postFeedButton);
+ });
+
+ expect(mockProps.updateThreadHandler).toHaveBeenCalledWith(
+ 'threadId',
+ 'postId',
+ true,
+ {
+ op: 'replace',
+ path: '/announcement/description',
+ value: 'Cypress announcement description.',
+ }
+ );
+
+ expect(getAllFeeds).toHaveBeenCalledWith(
+ 'threadLink',
+ undefined,
+ 'Announcement',
+ 'ALL'
+ );
+ });
+});
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityThreadPanel/AnnouncementThreads.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Announcement/AnnouncementThreads.test.tsx
similarity index 72%
rename from openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityThreadPanel/AnnouncementThreads.test.tsx
rename to openmetadata-ui/src/main/resources/ui/src/components/Announcement/AnnouncementThreads.test.tsx
index 0c6125633bd..179aa0d4a08 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityThreadPanel/AnnouncementThreads.test.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/Announcement/AnnouncementThreads.test.tsx
@@ -14,10 +14,10 @@
import { render, screen } from '@testing-library/react';
import React from 'react';
import { MemoryRouter } from 'react-router-dom';
-import { mockThreadData } from './ActivityThread.mock';
+import { mockThreadData } from '../ActivityFeed/ActivityThreadPanel/ActivityThread.mock';
import AnnouncementThreads from './AnnouncementThreads';
-jest.mock('../../../utils/FeedUtils', () => ({
+jest.mock('../../utils/FeedUtils', () => ({
getFeedListWithRelativeDays: jest.fn().mockReturnValue({
updatedFeedList: mockThreadData,
relativeDays: ['Today', 'Yesterday'],
@@ -27,6 +27,7 @@ jest.mock('../../../utils/FeedUtils', () => ({
const mockAnnouncementThreadsProp = {
threads: mockThreadData,
selectedThreadId: '',
+ editPermission: true,
postFeed: jest.fn(),
onThreadIdSelect: jest.fn(),
onThreadSelect: jest.fn(),
@@ -34,16 +35,8 @@ const mockAnnouncementThreadsProp = {
updateThreadHandler: jest.fn(),
};
-jest.mock('../ActivityFeedCard/ActivityFeedCard', () => {
- return jest.fn().mockReturnValue(ActivityFeedCard
);
-});
-
-jest.mock('../ActivityFeedEditor/ActivityFeedEditor', () => {
- return jest.fn().mockReturnValue(ActivityFeedEditor
);
-});
-
-jest.mock('../ActivityFeedCard/FeedCardFooter/FeedCardFooter', () => {
- return jest.fn().mockReturnValue(FeedCardFooter
);
+jest.mock('./AnnouncementFeedCard.component', () => {
+ return jest.fn().mockReturnValue(AnnouncementFeedCard
);
});
describe('Test AnnouncementThreads Component', () => {
@@ -52,7 +45,7 @@ describe('Test AnnouncementThreads Component', () => {
wrapper: MemoryRouter,
});
- const threads = await screen.findAllByTestId('announcement-card');
+ const threads = await screen.findAllByText('AnnouncementFeedCard');
expect(threads).toHaveLength(2);
});
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Announcement/AnnouncementThreads.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Announcement/AnnouncementThreads.tsx
new file mode 100644
index 00000000000..4a7f76d26f7
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/components/Announcement/AnnouncementThreads.tsx
@@ -0,0 +1,112 @@
+/*
+ * Copyright 2022 Collate.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Divider, Typography } from 'antd';
+import React, { FC, useCallback, useMemo } from 'react';
+import { useTranslation } from 'react-i18next';
+import { Post, Thread } from '../../generated/entity/feed/thread';
+import { isActiveAnnouncement } from '../../utils/AnnouncementsUtils';
+import { getFeedListWithRelativeDays } from '../../utils/FeedUtils';
+import { AnnouncementThreadListProp } from './Announcement.interface';
+import './announcement.less';
+import AnnouncementFeedCard from './AnnouncementFeedCard.component';
+
+const AnnouncementThreads: FC = ({
+ threads,
+ editPermission,
+ postFeed,
+ onConfirmation,
+ updateThreadHandler,
+}) => {
+ const { t } = useTranslation();
+ const { updatedFeedList: updatedThreads } =
+ getFeedListWithRelativeDays(threads);
+
+ const { activeAnnouncements, inActiveAnnouncements } = useMemo(() => {
+ return updatedThreads.reduce(
+ (
+ acc: {
+ activeAnnouncements: Thread[];
+ inActiveAnnouncements: Thread[];
+ },
+ cv: Thread
+ ) => {
+ if (
+ cv.announcement &&
+ isActiveAnnouncement(
+ cv.announcement?.startTime,
+ cv.announcement?.endTime
+ )
+ ) {
+ acc.activeAnnouncements.push(cv);
+ } else {
+ acc.inActiveAnnouncements.push(cv);
+ }
+
+ return acc;
+ },
+ {
+ activeAnnouncements: [],
+ inActiveAnnouncements: [],
+ }
+ );
+ }, [updatedThreads]);
+
+ const getAnnouncements = useCallback(
+ (announcements: Thread[]) => {
+ return announcements.map((thread) => {
+ const mainFeed = {
+ message: thread.message,
+ postTs: thread.threadTs,
+ from: thread.createdBy,
+ id: thread.id,
+ reactions: thread.reactions,
+ } as Post;
+
+ return (
+
+ );
+ });
+ },
+ [editPermission, postFeed, updateThreadHandler, onConfirmation]
+ );
+
+ return (
+ <>
+ {getAnnouncements(activeAnnouncements)}
+ {Boolean(inActiveAnnouncements.length) && (
+
+
+ {inActiveAnnouncements.length} {' '}
+ {t('label.inactive-announcement-plural')}
+
+
+
+ )}
+
+ {getAnnouncements(inActiveAnnouncements)}
+ >
+ );
+};
+
+export default AnnouncementThreads;
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Announcement/announcement.less b/openmetadata-ui/src/main/resources/ui/src/components/Announcement/announcement.less
new file mode 100644
index 00000000000..fe29111adfe
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/components/Announcement/announcement.less
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2024 Collate.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+@import (reference) url('../../styles/variables.less');
+
+.announcement-thread-body {
+ margin-top: 16px;
+
+ .text-announcement {
+ color: @announcement-border;
+ }
+
+ .ant-card.announcement-thread-card {
+ margin-top: 20px;
+ padding-top: 8px;
+ border-radius: 8px;
+ border: 1px solid @announcement-border;
+ background: @announcement-background;
+
+ .avatar-column {
+ position: relative;
+
+ &::after {
+ position: absolute;
+ content: '';
+ top: 0;
+ left: 50%;
+ transform: translateX(-50%);
+ width: 1px;
+ background: @announcement-border-light;
+ height: calc(100% - 10px);
+ z-index: 1;
+ }
+
+ .assignee-item {
+ margin: 0;
+ z-index: 10;
+ }
+
+ .ant-avatar-group {
+ z-index: 10;
+ }
+ }
+
+ .arrow-icon {
+ font-size: 10px;
+ }
+
+ .rotate-180 {
+ transform: rotate(180deg);
+ }
+ }
+}
+
+.feed-line {
+ margin-left: 8px;
+ background: @announcement-border-light;
+ width: 1px;
+ height: calc(100% - 10px);
+}
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Database/TableQueries/TableQueryRightPanel/TableQueryRightPanel.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Database/TableQueries/TableQueryRightPanel/TableQueryRightPanel.component.tsx
index fe560737b9b..fc6a54bd895 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/Database/TableQueries/TableQueryRightPanel/TableQueryRightPanel.component.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/Database/TableQueries/TableQueryRightPanel/TableQueryRightPanel.component.tsx
@@ -147,7 +147,6 @@ const TableQueryRightPanel = ({
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Domain/DomainExperts/DomainExperts.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Domain/DomainExperts/DomainExperts.component.tsx
index 3c1ef939372..ab57f7c8e48 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/Domain/DomainExperts/DomainExperts.component.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/Domain/DomainExperts/DomainExperts.component.tsx
@@ -63,7 +63,6 @@ function DomainExperts({
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryDetailsRightPanel/GlossaryReviewers.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryDetailsRightPanel/GlossaryReviewers.tsx
index a310a4c6278..0840d6c6db9 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryDetailsRightPanel/GlossaryReviewers.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryDetailsRightPanel/GlossaryReviewers.tsx
@@ -73,7 +73,6 @@ function GlossaryReviewers({
displayName={getEntityName(reviewer)}
isTeam={reviewer.type === UserTeam.Team}
name={reviewer.name ?? ''}
- textClass="text-xs"
width="20"
/>
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Modals/AnnouncementModal/AddAnnouncementModal.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Modals/AnnouncementModal/AddAnnouncementModal.test.tsx
index 74a6a1e48a0..ce45fde2a49 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/Modals/AnnouncementModal/AddAnnouncementModal.test.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/Modals/AnnouncementModal/AddAnnouncementModal.test.tsx
@@ -42,12 +42,14 @@ jest.mock('react-router-dom', () => ({
useLocation: jest.fn().mockReturnValue({ pathname: 'pathname' }),
}));
const onCancel = jest.fn();
+const onSave = jest.fn();
const mockProps = {
open: true,
entityType: '',
entityFQN: '',
onCancel,
+ onSave,
};
describe('Test Add Announcement modal', () => {
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Modals/AnnouncementModal/AddAnnouncementModal.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Modals/AnnouncementModal/AddAnnouncementModal.tsx
index e4f3e2e7a16..1b5447e19a0 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/Modals/AnnouncementModal/AddAnnouncementModal.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/Modals/AnnouncementModal/AddAnnouncementModal.tsx
@@ -35,6 +35,7 @@ interface Props {
entityType: string;
entityFQN: string;
onCancel: () => void;
+ onSave: () => void;
}
export interface CreateAnnouncement {
@@ -47,6 +48,7 @@ export interface CreateAnnouncement {
const AddAnnouncementModal: FC = ({
open,
onCancel,
+ onSave,
entityType,
entityFQN,
}) => {
@@ -85,7 +87,7 @@ const AddAnnouncementModal: FC = ({
if (data) {
showSuccessToast(t('message.announcement-created-successfully'));
}
- onCancel();
+ onSave();
} catch (error) {
showErrorToast(error as AxiosError);
} finally {
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Users/UserProfileIcon/UserProfileIcon.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Users/UserProfileIcon/UserProfileIcon.component.tsx
index c7838747342..66b3577708a 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Users/UserProfileIcon/UserProfileIcon.component.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Users/UserProfileIcon/UserProfileIcon.component.tsx
@@ -40,7 +40,7 @@ import {
getImageWithResolutionAndFallback,
ImageQuality,
} from '../../../../utils/ProfilerUtils';
-import Avatar from '../../../common/AvatarComponent/Avatar';
+import ProfilePicture from '../../../common/ProfilePicture/ProfilePicture';
import './user-profile-icon.less';
type ListMenuItemProps = {
@@ -337,7 +337,7 @@ export const UserProfileIcon = () => {
onError={handleOnImageError}
/>
) : (
-
+
)}
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Users/UserProfileIcon/UserProfileIcon.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Users/UserProfileIcon/UserProfileIcon.test.tsx
index de574f31cd3..416e4091965 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Users/UserProfileIcon/UserProfileIcon.test.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Users/UserProfileIcon/UserProfileIcon.test.tsx
@@ -40,8 +40,8 @@ jest.mock('../../../../utils/ProfilerUtils', () => ({
ImageQuality: jest.fn().mockReturnValue('6x'),
}));
-jest.mock('../../../common/AvatarComponent/Avatar', () =>
- jest.fn().mockReturnValue(Avatar
)
+jest.mock('../../../common/ProfilePicture/ProfilePicture', () =>
+ jest.fn().mockReturnValue(ProfilePicture
)
);
jest.mock('react-router-dom', () => ({
@@ -84,7 +84,7 @@ describe('UserProfileIcon', () => {
const { queryByTestId, getByText } = render( );
expect(queryByTestId('app-bar-user-profile-pic')).not.toBeInTheDocument();
- expect(getByText('Avatar')).toBeInTheDocument();
+ expect(getByText('ProfilePicture')).toBeInTheDocument();
});
it('should display the user team', () => {
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Users/UsersProfile/UserProfileImage/UserProfileImage.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Users/UsersProfile/UserProfileImage/UserProfileImage.component.tsx
index d31b3fcc336..53f4e58b66f 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Users/UsersProfile/UserProfileImage/UserProfileImage.component.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Users/UsersProfile/UserProfileImage/UserProfileImage.component.tsx
@@ -53,7 +53,6 @@ const UserProfileImage = ({ userData }: UserProfileImageProps) => {
displayName={userData?.displayName ?? userData.name}
height="54"
name={userData?.name ?? ''}
- textClass="text-xl"
width="54"
/>
)}
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/EntityPageInfos/AnnouncementCard/AnnouncementCard.less b/openmetadata-ui/src/main/resources/ui/src/components/common/EntityPageInfos/AnnouncementCard/AnnouncementCard.less
index dbe73f548e8..69258f540c0 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/common/EntityPageInfos/AnnouncementCard/AnnouncementCard.less
+++ b/openmetadata-ui/src/main/resources/ui/src/components/common/EntityPageInfos/AnnouncementCard/AnnouncementCard.less
@@ -13,7 +13,7 @@
@import url('../../../../styles/variables.less');
-.announcement-card {
+.ant-card.announcement-card {
width: 340px;
background: @announcement-background;
border: 1px solid @announcement-border;
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/EntityPageInfos/AnnouncementDrawer/AnnouncementDrawer.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/EntityPageInfos/AnnouncementDrawer/AnnouncementDrawer.test.tsx
index 90399e94d47..20120f79d89 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/common/EntityPageInfos/AnnouncementDrawer/AnnouncementDrawer.test.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/common/EntityPageInfos/AnnouncementDrawer/AnnouncementDrawer.test.tsx
@@ -28,12 +28,9 @@ jest.mock('../../../../utils/ToastUtils', () => ({
showErrorToast: jest.fn(),
}));
-jest.mock(
- '../../../ActivityFeed/ActivityThreadPanel/ActivityThreadPanelBody',
- () => {
- return jest.fn().mockReturnValue(ActivityThreadPanelBody
);
- }
-);
+jest.mock('../../../Announcement/AnnouncementThreadBody.component', () => {
+ return jest.fn().mockReturnValue(AnnouncementThreadBody
);
+});
jest.mock('../../../Modals/AnnouncementModal/AddAnnouncementModal', () => {
return jest.fn().mockReturnValue(AddAnnouncementModal
);
@@ -51,21 +48,31 @@ describe('Test Announcement drawer component', () => {
it('Should render the component', async () => {
render( );
- const drawer = await screen.findByTestId('announcement-drawer');
+ const announcementHeader = screen.getByText('label.announcement-plural');
+ const addAnnouncementButton = screen.getByTestId('add-announcement');
- const addButton = await screen.findByTestId('add-announcement');
+ const addButton = screen.getByTestId('add-announcement');
- const announcements = await screen.findByText('ActivityThreadPanelBody');
+ const announcements = screen.getByText('AnnouncementThreadBody');
- expect(drawer).toBeInTheDocument();
+ expect(announcementHeader).toBeInTheDocument();
+ expect(addAnnouncementButton).toBeInTheDocument();
expect(addButton).toBeInTheDocument();
expect(announcements).toBeInTheDocument();
});
+ it('Should be disabled if not having permission to create', async () => {
+ render( );
+
+ const addButton = screen.getByTestId('add-announcement');
+
+ expect(addButton).toBeDisabled();
+ });
+
it('Should open modal on click of add button', async () => {
render( );
- const addButton = await screen.findByTestId('add-announcement');
+ const addButton = screen.getByTestId('add-announcement');
fireEvent.click(addButton);
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/EntityPageInfos/AnnouncementDrawer/AnnouncementDrawer.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/EntityPageInfos/AnnouncementDrawer/AnnouncementDrawer.tsx
index 6e9c3f30de2..b05a59dbaf9 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/common/EntityPageInfos/AnnouncementDrawer/AnnouncementDrawer.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/common/EntityPageInfos/AnnouncementDrawer/AnnouncementDrawer.tsx
@@ -15,29 +15,24 @@ import { CloseOutlined } from '@ant-design/icons';
import { Button, Drawer, Space, Tooltip, Typography } from 'antd';
import { AxiosError } from 'axios';
import { Operation } from 'fast-json-patch';
-import { uniqueId } from 'lodash';
-import React, { FC, useState } from 'react';
+import React, { FC, useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
-import {
- CreateThread,
- ThreadType,
-} from '../../../../generated/api/feed/createThread';
import { Post } from '../../../../generated/entity/feed/thread';
-import { postFeedById, postThread } from '../../../../rest/feedsAPI';
+import { postFeedById } from '../../../../rest/feedsAPI';
import { getEntityFeedLink } from '../../../../utils/EntityUtils';
import { deletePost, updateThreadData } from '../../../../utils/FeedUtils';
import { showErrorToast } from '../../../../utils/ToastUtils';
-import ActivityThreadPanelBody from '../../../ActivityFeed/ActivityThreadPanel/ActivityThreadPanelBody';
import { useApplicationStore } from '../../../../hooks/useApplicationStore';
+import AnnouncementThreadBody from '../../../Announcement/AnnouncementThreadBody.component';
import AddAnnouncementModal from '../../../Modals/AnnouncementModal/AddAnnouncementModal';
interface Props {
open: boolean;
entityType: string;
entityFQN: string;
+ createPermission: boolean;
onClose: () => void;
- createPermission?: boolean;
}
const AnnouncementDrawer: FC = ({
@@ -45,11 +40,13 @@ const AnnouncementDrawer: FC = ({
onClose,
entityFQN,
entityType,
- createPermission,
+ createPermission = false,
}) => {
const { t } = useTranslation();
const { currentUser } = useApplicationStore();
- const [isAnnouncement, setIsAnnouncement] = useState(false);
+ const [isAddAnnouncementOpen, setIsAddAnnouncementOpen] =
+ useState(false);
+ const [refetchThread, setRefetchThread] = useState(false);
const title = (
= ({
);
- const createThread = async (data: CreateThread) => {
+ const deletePostHandler = async (
+ threadId: string,
+ postId: string,
+ isThread: boolean
+ ): Promise => {
+ await deletePost(threadId, postId, isThread);
+ };
+
+ const postFeedHandler = async (value: string, id: string): Promise => {
+ const data = {
+ message: value,
+ from: currentUser?.name,
+ } as Post;
+
try {
- await postThread(data);
+ await postFeedById(id, data);
} catch (err) {
showErrorToast(err as AxiosError);
}
};
- const deletePostHandler = (
- threadId: string,
- postId: string,
- isThread: boolean
- ) => {
- deletePost(threadId, postId, isThread);
- };
-
- const postFeedHandler = (value: string, id: string) => {
- const data = {
- message: value,
- from: currentUser?.name,
- } as Post;
- postFeedById(id, data).catch((err: AxiosError) => {
- showErrorToast(err);
- });
- };
-
- const updateThreadHandler = (
+ const updateThreadHandler = async (
threadId: string,
postId: string,
isThread: boolean,
data: Operation[]
- ) => {
+ ): Promise => {
const callback = () => {
return;
};
- updateThreadData(threadId, postId, isThread, data, callback);
+ await updateThreadData(threadId, postId, isThread, data, callback);
};
- return (
- <>
-
-
-
-
- setIsAnnouncement(true)}>
- {t('label.add-entity', { entity: t('label.announcement') })}
-
-
-
+ const handleCloseAnnouncementModal = useCallback(
+ () => setIsAddAnnouncementOpen(false),
+ []
+ );
+ const handleOpenAnnouncementModal = useCallback(
+ () => setIsAddAnnouncementOpen(true),
+ []
+ );
-
-
+ const handleSaveAnnouncement = useCallback(() => {
+ handleCloseAnnouncementModal();
+ setRefetchThread((prev) => !prev);
+ }, []);
+
+ return (
+
+
+
+
+ {t('label.add-entity', { entity: t('label.announcement') })}
+
+
- {isAnnouncement && (
+
+
+ {isAddAnnouncementOpen && (
setIsAnnouncement(false)}
+ open={isAddAnnouncementOpen}
+ onCancel={handleCloseAnnouncementModal}
+ onSave={handleSaveAnnouncement}
/>
)}
- >
+
);
};
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/PopOverCard/UserPopOverCard.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/PopOverCard/UserPopOverCard.tsx
index dd0c0a34d73..54b97c4a7b6 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/common/PopOverCard/UserPopOverCard.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/common/PopOverCard/UserPopOverCard.tsx
@@ -237,6 +237,7 @@ const UserPopOverCard: FC
= ({
}) => {
const profilePicture = (
{
it('ProfilePicture component should render with Avatar', async () => {
const { container } = render( );
- const avatar = await findByText(container, 'Avatar');
+ const avatar = await findByTestId(container, 'profile-avatar');
expect(avatar).toBeInTheDocument();
});
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/ProfilePicture/ProfilePicture.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/ProfilePicture/ProfilePicture.tsx
index 8f618c91820..7369bc67da8 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/common/ProfilePicture/ProfilePicture.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/common/ProfilePicture/ProfilePicture.tsx
@@ -11,16 +11,16 @@
* limitations under the License.
*/
+import { Avatar } from 'antd';
import classNames from 'classnames';
import { ImageShape } from 'Models';
import React, { useMemo } from 'react';
import { usePermissionProvider } from '../../../context/PermissionProvider/PermissionProvider';
import { ResourceEntity } from '../../../context/PermissionProvider/PermissionProvider.interface';
-import { EntityReference, User } from '../../../generated/entity/teams/user';
+import { User } from '../../../generated/entity/teams/user';
import { useUserProfile } from '../../../hooks/user-profile/useUserProfile';
-import { getEntityName } from '../../../utils/EntityUtils';
+import { getRandomColor } from '../../../utils/CommonUtils';
import { userPermissions } from '../../../utils/PermissionsUtils';
-import Avatar from '../AvatarComponent/Avatar';
import Loader from '../Loader/Loader';
type UserData = Pick;
@@ -28,25 +28,28 @@ type UserData = Pick;
interface Props extends UserData {
width?: string;
type?: ImageShape;
- textClass?: string;
className?: string;
height?: string;
- profileImgClasses?: string;
isTeam?: boolean;
+ size?: number | 'small' | 'default' | 'large';
+ avatarType?: 'solid' | 'outlined';
}
const ProfilePicture = ({
name,
displayName,
className = '',
- textClass = '',
type = 'circle',
width = '36',
height,
- profileImgClasses,
isTeam = false,
+ size,
+ avatarType = 'solid',
}: Props) => {
const { permissions } = usePermissionProvider();
+ const { color, character, backgroundColor } = getRandomColor(
+ displayName ?? name
+ );
const viewUserPermission = useMemo(() => {
return userPermissions.hasViewPermissions(ResourceEntity.USER, permissions);
@@ -61,12 +64,17 @@ const ProfilePicture = ({
const getAvatarByName = () => {
return (
);
};
@@ -97,17 +105,13 @@ const ProfilePicture = ({
};
return profileURL ? (
-
-
-
+
) : (
getAvatarElement()
);
diff --git a/openmetadata-ui/src/main/resources/ui/src/mocks/Announcement.mock.ts b/openmetadata-ui/src/main/resources/ui/src/mocks/Announcement.mock.ts
new file mode 100644
index 00000000000..ba1ba6d556f
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/mocks/Announcement.mock.ts
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2024 Collate.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { ThreadType } from '../generated/api/feed/createThread';
+
+export const MOCK_ANNOUNCEMENT_DATA = {
+ data: [
+ {
+ id: '36ea94c9-7f12-489c-94df-56cbefe14b2f',
+ type: ThreadType.Announcement,
+ href: 'http://localhost:8585/api/v1/feed/36ea94c9-7f12-489c-94df-56cbefe14b2f',
+ threadTs: 1714026576902,
+ about:
+ '<#E::database::cy-database-service-373851.cypress-database-1714026557974>',
+ entityId: '123f24e3-2a00-432e-b42b-b709f7ae74c0',
+ createdBy: 'admin',
+ updatedAt: 1714037939788,
+ updatedBy: 'shreya',
+ resolved: false,
+ message: 'Cypress announcement',
+ postsCount: 4,
+ posts: [
+ {
+ id: 'ccf1ad4a-4cf0-4be9-bcc7-1459f533bab0',
+ message: 'this is done!',
+ postTs: 1714036398114,
+ from: 'admin',
+ reactions: [],
+ },
+ {
+ id: '738eb0ae-0b71-4a13-8dd2-d7d7d73073b6',
+ message: 'having a look on it!',
+ postTs: 1714037894068,
+ from: 'david',
+ reactions: [],
+ },
+ {
+ id: 'fdc984e7-2d69-4f06-8b94-531ff8b696f7',
+ message: 'this if fixed and RCA given!',
+ postTs: 1714037939785,
+ from: 'shreya',
+ reactions: [],
+ },
+ {
+ id: '62434a57-57ec-4b5f-83c1-9ae5870337b6',
+ message: 'test',
+ postTs: 1714027952172,
+ from: 'admin',
+ reactions: [],
+ },
+ ],
+ reactions: [],
+ announcement: {
+ description: 'Cypress announcement description',
+ startTime: 1713983400,
+ endTime: 1714415400,
+ },
+ },
+ ],
+ paging: {
+ total: 1,
+ },
+};
+
+export const MOCK_ANNOUNCEMENT_FEED_DATA = {
+ id: '36ea94c9-7f12-489c-94df-56cbefe14b2f',
+ type: 'Announcement',
+ href: 'http://localhost:8585/api/v1/feed/36ea94c9-7f12-489c-94df-56cbefe14b2f',
+ threadTs: 1714026576902,
+ about:
+ '<#E::database::cy-database-service-373851.cypress-database-1714026557974>',
+ entityId: '123f24e3-2a00-432e-b42b-b709f7ae74c0',
+ createdBy: 'admin',
+ updatedAt: 1714047427117,
+ updatedBy: 'admin',
+ resolved: false,
+ message: 'Cypress announcement',
+ postsCount: 4,
+ posts: [
+ {
+ id: '62434a57-57ec-4b5f-83c1-9ae5870337b6',
+ message: 'test',
+ postTs: 1714027952172,
+ from: 'admin',
+ reactions: [],
+ },
+ {
+ id: 'ccf1ad4a-4cf0-4be9-bcc7-1459f533bab0',
+ message: 'this is done!',
+ postTs: 1714036398114,
+ from: 'admin',
+ reactions: [],
+ },
+ {
+ id: '738eb0ae-0b71-4a13-8dd2-d7d7d73073b6',
+ message: 'having a look on it!',
+ postTs: 1714037894068,
+ from: 'david',
+ reactions: [],
+ },
+ {
+ id: 'fdc984e7-2d69-4f06-8b94-531ff8b696f7',
+ message: 'this if fixed and RCA given!',
+ postTs: 1714037939785,
+ from: 'shreya',
+ reactions: [],
+ },
+ ],
+ reactions: [],
+ announcement: {
+ description: 'Cypress announcement description.',
+ startTime: 1713983400,
+ endTime: 1714415400,
+ },
+};
diff --git a/openmetadata-ui/src/main/resources/ui/src/styles/variables.less b/openmetadata-ui/src/main/resources/ui/src/styles/variables.less
index 188f21235e3..4368d88d6ee 100644
--- a/openmetadata-ui/src/main/resources/ui/src/styles/variables.less
+++ b/openmetadata-ui/src/main/resources/ui/src/styles/variables.less
@@ -81,9 +81,10 @@
@global-border: 1px solid @border-color;
@active-color: #e8f4ff;
@background-color: #ffffff;
-@announcement-background: #e3f2fd30;
-@announcement-background-dark: #9dd6ff;
-@announcement-border: @info-color;
+@announcement-background: #0950c50d;
+@announcement-background-dark: #e1edff;
+@announcement-border: @blue-3;
+@announcement-border-light: #e2e2e2;
@test-parameter-bg-color: #e7ebf0;
@group-title-color: #76746f;
@light-border-color: #f0f0f0;
diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/AdvancedSearchUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/AdvancedSearchUtils.tsx
index 9ca9095e3ef..5a57e71e087 100644
--- a/openmetadata-ui/src/main/resources/ui/src/utils/AdvancedSearchUtils.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/utils/AdvancedSearchUtils.tsx
@@ -184,7 +184,6 @@ export const generateSearchDropdownLabel = (
)}
diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/CommonUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/CommonUtils.tsx
index b844b699aa4..3e5b096113b 100644
--- a/openmetadata-ui/src/main/resources/ui/src/utils/CommonUtils.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/utils/CommonUtils.tsx
@@ -421,17 +421,18 @@ export const getNameFromFQN = (fqn: string): string => {
export const getRandomColor = (name: string) => {
const firstAlphabet = name.charAt(0).toLowerCase();
- const asciiCode = firstAlphabet.charCodeAt(0);
- const colorNum =
- asciiCode.toString() + asciiCode.toString() + asciiCode.toString();
+ // Convert the user's name to a numeric value
+ let nameValue = 0;
+ for (let i = 0; i < name.length; i++) {
+ nameValue += name.charCodeAt(i) * 8;
+ }
- const num = Math.round(0xffffff * parseInt(colorNum));
- const r = (num >> 16) & 255;
- const g = (num >> 8) & 255;
- const b = num & 255;
+ // Generate a random hue based on the name value
+ const hue = nameValue % 360;
return {
- color: 'rgb(' + r + ', ' + g + ', ' + b + ', 0.6)',
+ color: `hsl(${hue}, 70%, 40%)`,
+ backgroundColor: `hsl(${hue}, 100%, 92%)`,
character: firstAlphabet.toUpperCase(),
};
};
diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/FeedUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/FeedUtils.tsx
index badc4bff248..690bdfb2c65 100644
--- a/openmetadata-ui/src/main/resources/ui/src/utils/FeedUtils.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/utils/FeedUtils.tsx
@@ -385,9 +385,9 @@ export const deletePost = async (
}
} else {
try {
- const deletResponse = await deletePostById(threadId, postId);
+ const deleteResponse = await deletePostById(threadId, postId);
// get updated thread only if delete response and callback is present
- if (deletResponse && callback) {
+ if (deleteResponse && callback) {
const data = await getUpdatedThread(threadId);
callback((pre) => {
return pre.map((thread) => {
@@ -437,62 +437,60 @@ export const getEntityFieldDisplay = (entityField: string) => {
return null;
};
-export const updateThreadData = (
+export const updateThreadData = async (
threadId: string,
postId: string,
isThread: boolean,
data: Operation[],
callback: (value: React.SetStateAction) => void
-) => {
+): Promise => {
if (isThread) {
- updateThread(threadId, data)
- .then((res) => {
- callback((prevData) => {
- return prevData.map((thread) => {
- if (isEqual(threadId, thread.id)) {
- return {
- ...thread,
- reactions: res.reactions,
- message: res.message,
- announcement: res?.announcement,
- };
- } else {
- return thread;
- }
- });
+ try {
+ const res = await updateThread(threadId, data);
+ callback((prevData) => {
+ return prevData.map((thread) => {
+ if (isEqual(threadId, thread.id)) {
+ return {
+ ...thread,
+ reactions: res.reactions,
+ message: res.message,
+ announcement: res?.announcement,
+ };
+ } else {
+ return thread;
+ }
});
- })
- .catch((err: AxiosError) => {
- showErrorToast(err);
});
+ } catch (error) {
+ showErrorToast(error as AxiosError);
+ }
} else {
- updatePost(threadId, postId, data)
- .then((res) => {
- callback((prevData) => {
- return prevData.map((thread) => {
- if (isEqual(threadId, thread.id)) {
- const updatedPosts = (thread.posts || []).map((post) => {
- if (isEqual(postId, post.id)) {
- return {
- ...post,
- reactions: res.reactions,
- message: res.message,
- };
- } else {
- return post;
- }
- });
+ try {
+ const res = await updatePost(threadId, postId, data);
+ callback((prevData) => {
+ return prevData.map((thread) => {
+ if (isEqual(threadId, thread.id)) {
+ const updatedPosts = (thread.posts || []).map((post) => {
+ if (isEqual(postId, post.id)) {
+ return {
+ ...post,
+ reactions: res.reactions,
+ message: res.message,
+ };
+ } else {
+ return post;
+ }
+ });
- return { ...thread, posts: updatedPosts };
- } else {
- return thread;
- }
- });
+ return { ...thread, posts: updatedPosts };
+ } else {
+ return thread;
+ }
});
- })
- .catch((err: AxiosError) => {
- showErrorToast(err);
});
+ } catch (error) {
+ showErrorToast(error as AxiosError);
+ }
}
};