diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ActivityFeed.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ActivityFeed.spec.ts index 82f2d7f53c5..c3a02a4db2b 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ActivityFeed.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ActivityFeed.spec.ts @@ -365,6 +365,52 @@ test.describe('Activity feed', () => { await checkTaskCountInActivityFeed(page, 0, 1); }); + test('Replies should be visible in the task feed', async ({ page }) => { + const value: TaskDetails = { + term: entity2.entity.displayName, + assignee: user1.responseData.name, + }; + await redirectToHomePage(page); + + await entity2.visitEntityPage(page); + + await page.getByTestId('request-description').click(); + + await createDescriptionTask(page, value); + + // Task 1 - Update Description right panel check + const descriptionTask = await page.getByTestId('task-title').innerText(); + + expect(descriptionTask).toContain('Request to update description'); + + for (let i = 0; i < 10; i++) { + const commentInput = page.locator('[data-testid="comments-input-field"]'); + commentInput.click(); + + await page.fill( + '[data-testid="editor-wrapper"] .ql-editor', + `Reply message ${i}` + ); + const sendReply = page.waitForResponse('/api/v1/feed/*/posts'); + await page.getByTestId('send-button').click({ force: true }); + await sendReply; + } + + await page.reload(); + await page.waitForSelector('[data-testid="loader"]', { + state: 'hidden', + }); + await page.waitForLoadState('networkidle'); + + await expect(page.getByTestId('feed-reply-card')).toHaveCount(10); + + for (let i = 0; i < 10; i++) { + await expect( + page.locator('.right-container [data-testid="feed-replies"]') + ).toContainText(`Reply message ${i}`); + } + }); + test('Open and Closed Task Tab with approve from Task Feed Card', async ({ page, }) => { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedCardNew/ActivityFeedcardNew.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedCardNew/ActivityFeedcardNew.component.tsx index 8f70aad3d41..9d2ec734214 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedCardNew/ActivityFeedcardNew.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedCardNew/ActivityFeedcardNew.component.tsx @@ -10,10 +10,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Card, Col, Input, Space, Tooltip, Typography } from 'antd'; +import { Card, Col, Input, Skeleton, Space, Tooltip, Typography } from 'antd'; import classNames from 'classnames'; import { compare } from 'fast-json-patch'; -import { isUndefined } from 'lodash'; +import { isUndefined, orderBy } from 'lodash'; import React, { useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Link } from 'react-router-dom'; @@ -75,10 +75,10 @@ const ActivityFeedCardNew = ({ }, [feed.about]); const { t } = useTranslation(); const { currentUser } = useApplicationStore(); - const { selectedThread, postFeed } = useActivityFeedProvider(); + const { selectedThread, postFeed, updateFeed, isPostsLoading } = + useActivityFeedProvider(); const [showFeedEditor, setShowFeedEditor] = useState(false); const [isEditPost, setIsEditPost] = useState(false); - const { updateFeed } = useActivityFeedProvider(); const [, , user] = useUserProfile({ permission: true, name: feed.createdBy ?? '', @@ -183,6 +183,38 @@ const ActivityFeedCardNew = ({ setShowFeedEditor(false); }; + const posts = useMemo(() => { + if (!showThread) { + return null; + } + + if (isPostsLoading) { + return ( + + + + + + ); + } + + const posts = orderBy(feed.posts, ['postTs'], ['desc']); + + return ( + + {posts.map((reply, index, arr) => ( + + ))} + + ); + }, [feed, showThread, closeFeedEditor, isPostsLoading]); + return ( )} - {showThread && feed?.posts && feed?.posts?.length > 0 && ( - - {feed?.posts - ?.slice() - .sort((a, b) => (b.postTs as number) - (a.postTs as number)) - .map((reply, index, arr) => ( - - ))} - - )} + {posts} )} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedCardNew/CommentCard.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedCardNew/CommentCard.component.tsx index 2d4ef8928e7..1d23af41c12 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedCardNew/CommentCard.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedCardNew/CommentCard.component.tsx @@ -134,6 +134,7 @@ const CommentCard = ({ className={classNames('d-flex justify-start relative reply-card', { 'reply-card-border-bottom': !isLastReply, })} + data-testid="feed-reply-card" onMouseEnter={() => setIsHovered(true)} onMouseLeave={() => setIsHovered(false)}>
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedDrawer/ActivityFeedDrawer.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedDrawer/ActivityFeedDrawer.tsx index 3b3023d2bc2..24dce6b649b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedDrawer/ActivityFeedDrawer.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedDrawer/ActivityFeedDrawer.tsx @@ -14,7 +14,7 @@ import { Col, Drawer, Row } from 'antd'; import classNames from 'classnames'; import React, { FC } from 'react'; -import { Thread, ThreadType } from '../../../generated/entity/feed/thread'; +import { ThreadType } from '../../../generated/entity/feed/thread'; import FeedPanelBodyV1 from '../ActivityFeedPanel/FeedPanelBodyV1'; import FeedPanelHeader from '../ActivityFeedPanel/FeedPanelHeader'; import { useActivityFeedProvider } from '../ActivityFeedProvider/ActivityFeedProvider'; @@ -31,6 +31,10 @@ const ActivityFeedDrawer: FC = ({ }) => { const { hideDrawer, selectedThread } = useActivityFeedProvider(); + if (!selectedThread) { + return null; + } + return ( = ({ showThreadIcon: false, showRepliesContainer: false, }} - feed={selectedThread as Thread} + feed={selectedThread} hidePopover={false} /> diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedList/ActivityFeedListV1New.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedList/ActivityFeedListV1New.component.tsx index df4bfd61343..73c63263ca1 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedList/ActivityFeedListV1New.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedList/ActivityFeedListV1New.component.tsx @@ -12,7 +12,7 @@ */ import { Typography } from 'antd'; import classNames from 'classnames'; -import { isEmpty } from 'lodash'; +import { isEmpty, isUndefined } from 'lodash'; import React, { ReactNode, useEffect, useMemo, useState } from 'react'; import { ReactComponent as FeedEmptyIcon } from '../../../assets/svg/ic-task-empty.svg'; import { ERROR_PLACEHOLDER_TYPE } from '../../../enums/common.enum'; @@ -69,11 +69,10 @@ const ActivityFeedListV1New = ({ }, [feedList]); useEffect(() => { - if (onFeedClick) { - onFeedClick( - entityThread.find((feed) => feed.id === selectedThread?.id) ?? - entityThread[0] - ); + const thread = entityThread.find((feed) => feed.id === selectedThread?.id); + + if (onFeedClick && (isUndefined(selectedThread) || isUndefined(thread))) { + onFeedClick(entityThread[0]); } }, [entityThread, selectedThread, onFeedClick]); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedProvider/ActivityFeedProvider.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedProvider/ActivityFeedProvider.tsx index bc4c8782a8c..cce9c3143a7 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedProvider/ActivityFeedProvider.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedProvider/ActivityFeedProvider.tsx @@ -24,6 +24,7 @@ import React, { } from 'react'; import { useTranslation } from 'react-i18next'; import { PAGE_SIZE_LARGE } from '../../../constants/constants'; +import { POST_FEED_PAGE_COUNT } from '../../../constants/Feeds.constants'; import { EntityType } from '../../../enums/entity.enum'; import { FeedFilter } from '../../../enums/mydata.enum'; import { ReactionOperation } from '../../../enums/reactions.enum'; @@ -43,6 +44,7 @@ import { deleteThread, getAllFeeds, getFeedById, + getPostsFeedById, postFeedById, updatePost, updateThread, @@ -71,6 +73,9 @@ const ActivityFeedProvider = ({ children, user }: Props) => { const [entityPaging, setEntityPaging] = useState({} as Paging); const [focusReplyEditor, setFocusReplyEditor] = useState(false); const [loading, setLoading] = useState(false); + const [isPostsLoading, setIsPostsLoading] = useState(false); + const [isTestCaseResolutionLoading, setIsTestCaseResolutionLoading] = + useState(false); const [isDrawerOpen, setIsDrawerOpen] = useState(false); const [selectedThread, setSelectedThread] = useState(); const [testCaseResolutionStatus, setTestCaseResolutionStatus] = useState< @@ -80,6 +85,7 @@ const ActivityFeedProvider = ({ children, user }: Props) => { const { currentUser } = useApplicationStore(); const fetchTestCaseResolution = useCallback(async (id: string) => { + setIsTestCaseResolutionLoading(true); try { const { data } = await getListTestCaseIncidentByStateId(id, { limit: PAGE_SIZE_LARGE, @@ -90,22 +96,40 @@ const ActivityFeedProvider = ({ children, user }: Props) => { ); } catch (error) { setTestCaseResolutionStatus([]); + } finally { + setIsTestCaseResolutionLoading(false); + } + }, []); + + const fetchPostsFeed = useCallback(async (active: Thread) => { + // If the posts count is greater than the page count, fetch the posts + if ( + active?.postsCount && + active?.postsCount > POST_FEED_PAGE_COUNT && + active?.posts?.length !== active?.postsCount + ) { + setIsPostsLoading(true); + try { + const { data } = await getPostsFeedById(active.id); + setSelectedThread((pre) => + pre?.id === active?.id ? { ...active, posts: data } : pre + ); + } finally { + setIsPostsLoading(false); + } } }, []); const setActiveThread = useCallback((active?: Thread) => { setSelectedThread(active); + active && fetchPostsFeed(active); + if ( active && active.task?.type === TaskType.RequestTestCaseFailureResolution && active.task?.testCaseResolutionStatusId ) { - setLoading(true); - fetchTestCaseResolution(active.task.testCaseResolutionStatusId).finally( - () => { - setLoading(false); - } - ); + fetchTestCaseResolution(active.task.testCaseResolutionStatusId); } }, []); @@ -408,6 +432,8 @@ const ActivityFeedProvider = ({ children, user }: Props) => { selectedThread, isDrawerOpen, loading, + isPostsLoading, + isTestCaseResolutionLoading, focusReplyEditor, refreshActivityFeed, deleteFeed, @@ -431,6 +457,8 @@ const ActivityFeedProvider = ({ children, user }: Props) => { selectedThread, isDrawerOpen, loading, + isPostsLoading, + isTestCaseResolutionLoading, focusReplyEditor, refreshActivityFeed, deleteFeed, diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedProvider/ActivityFeedProviderContext.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedProvider/ActivityFeedProviderContext.interface.ts index d89520b90be..c8e914aeea1 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedProvider/ActivityFeedProviderContext.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedProvider/ActivityFeedProviderContext.interface.ts @@ -26,6 +26,8 @@ import { Paging } from '../../../generated/type/paging'; export interface ActivityFeedProviderContextType { loading: boolean; + isPostsLoading?: boolean; + isTestCaseResolutionLoading?: boolean; entityThread: Thread[]; selectedThread: Thread | undefined; isDrawerOpen: boolean; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedTab/ActivityFeedTab.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedTab/ActivityFeedTab.component.tsx index 38d8bbe4d08..114a8f13a89 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedTab/ActivityFeedTab.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedTab/ActivityFeedTab.component.tsx @@ -294,9 +294,11 @@ export const ActivityFeedTab = ({ if (!feed && (isTaskActiveTab || isMentionTabSelected)) { setIsFullWidth(false); } - setActiveThread(feed); + if (selectedThread?.id !== feed?.id) { + setActiveThread(feed); + } }, - [setActiveThread, isTaskActiveTab, isMentionTabSelected] + [setActiveThread, isTaskActiveTab, isMentionTabSelected, selectedThread] ); useEffect(() => { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/TaskFeedCard/TaskFeedCardNew.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/TaskFeedCard/TaskFeedCardNew.component.tsx index 6aae8280d66..fcd1fc2b692 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/TaskFeedCard/TaskFeedCardNew.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/TaskFeedCard/TaskFeedCardNew.component.tsx @@ -336,17 +336,17 @@ const TaskFeedCard = ({ width={20} onClick={isForFeedTab ? showReplies : undefined} /> - {feed.posts && feed.posts?.length > 0 && ( + {feed?.postsCount && feed?.postsCount > 0 && ( )} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/IncidentManager/TestCaseIncidentTab/TestCaseIncidentTab.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/IncidentManager/TestCaseIncidentTab/TestCaseIncidentTab.component.tsx index 23f15968a4b..07acae3b377 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/IncidentManager/TestCaseIncidentTab/TestCaseIncidentTab.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/IncidentManager/TestCaseIncidentTab/TestCaseIncidentTab.component.tsx @@ -33,11 +33,11 @@ import { import { useElementInView } from '../../../../hooks/useElementInView'; import { useFqn } from '../../../../hooks/useFqn'; import { useTestCaseStore } from '../../../../pages/IncidentManager/IncidentManagerDetailPage/useTestCase.store'; -import ActivityFeedListV1 from '../../../ActivityFeed/ActivityFeedList/ActivityFeedListV1.component'; +import ActivityFeedListV1New from '../../../ActivityFeed/ActivityFeedList/ActivityFeedListV1New.component'; import { useActivityFeedProvider } from '../../../ActivityFeed/ActivityFeedProvider/ActivityFeedProvider'; import { TaskFilter } from '../../../ActivityFeed/ActivityFeedTab/ActivityFeedTab.interface'; import Loader from '../../../common/Loader/Loader'; -import { TaskTab } from '../../../Entity/Task/TaskTab/TaskTab.component'; +import { TaskTabNew } from '../../../Entity/Task/TaskTab/TaskTabNew.component'; import './test-case-incident-tab.style.less'; const TestCaseIncidentTab = () => { @@ -83,9 +83,11 @@ const TestCaseIncidentTab = () => { const handleFeedClick = useCallback( (feed: Thread) => { - setActiveThread(feed); + if (selectedThread?.id !== feed?.id) { + setActiveThread(feed); + } }, - [setActiveThread] + [setActiveThread, selectedThread] ); const loader = useMemo(() => (loading ? : null), [loading]); @@ -140,7 +142,7 @@ const TestCaseIncidentTab = () => { className="left-container" data-testid="left-container" id="left-container"> -
+
{
- @@ -187,7 +190,7 @@ const TestCaseIncidentTab = () => { {loader} {selectedThread && !loading && (
- { +jest.mock('../../../Entity/Task/TaskTab/TaskTabNew.component', () => { return { - TaskTab: jest.fn().mockImplementation(({ onAfterClose }) => ( + TaskTabNew: jest.fn().mockImplementation(({ onAfterClose }) => (
TaskTab