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 65212408607..60ea0bd2d00 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 @@ -28,18 +28,23 @@ import { import { updateDescription } from '../../utils/entity'; import { clickOnLogo } from '../../utils/sidebar'; import { + checkTaskCount, createDescriptionTask, createTagTask, TaskDetails, + TASK_OPEN_FETCH_LINK, } from '../../utils/task'; import { performUserLogin } from '../../utils/user'; const entity = new TableClass(); const entity2 = new TableClass(); +const entity3 = new TableClass(); const user1 = new UserClass(); const user2 = new UserClass(); test.describe('Activity feed', () => { + test.slow(); + // use the admin user to login test.use({ storageState: 'playwright/.auth/admin.json' }); @@ -48,6 +53,7 @@ test.describe('Activity feed', () => { await entity.create(apiContext); await entity2.create(apiContext); + await entity3.create(apiContext); await user1.create(apiContext); await afterAction(); @@ -61,6 +67,7 @@ test.describe('Activity feed', () => { const { apiContext, afterAction } = await createNewPage(browser); await entity.delete(apiContext); await entity2.delete(apiContext); + await entity3.delete(apiContext); await user1.delete(apiContext); await afterAction(); @@ -101,7 +108,7 @@ test.describe('Activity feed', () => { ) ).not.toBeVisible(); - const entityPageTaskTab = page.waitForResponse('/api/v1/feed?*&type=Task'); + const entityPageTaskTab = page.waitForResponse(TASK_OPEN_FETCH_LINK); const tagsTask = page.getByTestId('redirect-task-button-link').first(); const tagsTaskContent = await tagsTask.innerText(); @@ -135,9 +142,7 @@ test.describe('Activity feed', () => { await toastNotification(page, /Task resolved successfully/); - const closedTask = await page.getByTestId('closed-task').textContent(); - - expect(closedTask).toContain('2 Closed'); + await checkTaskCount(page, 0, 2); }); test('User should be able to reply and delete comment in feeds in ActivityFeed', async ({ @@ -289,9 +294,7 @@ test.describe('Activity feed', () => { await toastNotification(page, /Task resolved successfully/); - const closedTask = await page.getByTestId('closed-task').textContent(); - - expect(closedTask).toContain('2 Closed'); + await checkTaskCount(page, 0, 2); }); test('Comment and Close Task should work in Task Flow', async ({ page }) => { @@ -351,13 +354,75 @@ test.describe('Activity feed', () => { await toastNotification(page, 'Task closed successfully.'); - const openTask = await page.getByTestId('open-task').textContent(); + await checkTaskCount(page, 0, 1); + }); - expect(openTask).toContain('0 Open'); + test('Open and Closed Task tab', async ({ page }) => { + const value: TaskDetails = { + term: entity3.entity.name, + assignee: user1.responseData.name, + }; + await entity3.visitEntityPage(page); - const closedTask = await page.getByTestId('closed-task').textContent(); + await page.getByTestId('request-description').click(); - expect(closedTask).toContain('1 Closed'); + // create description task + const openTaskAfterDescriptionResponse = + page.waitForResponse(TASK_OPEN_FETCH_LINK); + await createDescriptionTask(page, value); + await openTaskAfterDescriptionResponse; + + // open task count after description + const openTask1 = await page.getByTestId('open-task').textContent(); + + expect(openTask1).toContain('1 Open'); + + await page.getByTestId('schema').click(); + + await page.getByTestId('request-entity-tags').click(); + + // create tag task + const openTaskAfterTagResponse = page.waitForResponse(TASK_OPEN_FETCH_LINK); + await createTagTask(page, { ...value, tag: 'PII.None' }); + await openTaskAfterTagResponse; + + // open task count after description + await checkTaskCount(page, 2, 0); + + // Close one task. + await page.fill( + '[data-testid="editor-wrapper"] .ql-editor', + 'Closing the task with comment' + ); + const commentWithCloseTask = page.waitForResponse( + '/api/v1/feed/tasks/*/close' + ); + await page.getByRole('button', { name: 'down' }).click(); + await page.waitForSelector('.ant-dropdown', { + state: 'visible', + }); + await page.getByRole('menuitem', { name: 'close' }).click(); + await commentWithCloseTask; + + const waitForCountFetch = page.waitForResponse('/api/v1/feed/count?*'); + + await toastNotification(page, 'Task closed successfully.'); + + await waitForCountFetch; + + // open task count after closing one task + await checkTaskCount(page, 1, 1); + + // switch to closed task tab + const closedTaskResponse = page.waitForResponse( + '/api/v1/feed?*&type=Task&taskStatus=Closed' + ); + await page.getByTestId('closed-task').click(); + await closedTaskResponse; + + expect(page.getByTestId('markdown-parser')).toContainText( + 'Closing the task with comment' + ); }); }); @@ -467,9 +532,7 @@ test.describe('Activity feed with Data Steward User', () => { ) ).not.toBeVisible(); - const entityPageTaskTab = page2.waitForResponse( - '/api/v1/feed?*&type=Task' - ); + const entityPageTaskTab = page2.waitForResponse(TASK_OPEN_FETCH_LINK); const tagsTask = page2.getByTestId('redirect-task-button-link').first(); const tagsTaskContent = await tagsTask.innerText(); @@ -583,9 +646,7 @@ test.describe('Activity feed with Data Steward User', () => { ) ).not.toBeVisible(); - const entityPageTaskTab = page2.waitForResponse( - '/api/v1/feed?*&type=Task' - ); + const entityPageTaskTab = page2.waitForResponse(TASK_OPEN_FETCH_LINK); const tagsTask = page2.getByTestId('redirect-task-button-link').first(); const tagsTaskContent = await tagsTask.innerText(); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/glossary.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/glossary.ts index dab69b6afa7..42a173bf8b8 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/glossary.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/glossary.ts @@ -33,6 +33,7 @@ import { } from './common'; import { addMultiOwner } from './entity'; import { sidebarClick } from './sidebar'; +import { TASK_OPEN_FETCH_LINK } from './task'; export const descriptionBox = '.toastui-editor-md-container > .toastui-editor > .ProseMirror'; @@ -466,9 +467,7 @@ export const fillGlossaryTermDetails = async ( const validateGlossaryTermTask = async (page: Page, term: GlossaryTermData) => { await page.click('[data-testid="activity_feed"]'); - const taskFeeds = page.waitForResponse( - '/api/v1/feed?entityLink=**&type=Task' - ); + const taskFeeds = page.waitForResponse(TASK_OPEN_FETCH_LINK); await page .getByTestId('global-setting-left-panel') .getByText('Tasks') diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/task.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/task.ts index 1f28fea9759..a0886947207 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/task.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/task.ts @@ -25,6 +25,8 @@ export type TaskDetails = { const tag = 'PII.None'; +export const TASK_OPEN_FETCH_LINK = '/api/v1/feed**&type=Task&taskStatus=Open'; + export const createDescriptionTask = async ( page: Page, value: TaskDetails, @@ -146,3 +148,17 @@ export const createTagTask = async ( await toastNotification(page, /Task created successfully./); }; + +export const checkTaskCount = async ( + page: Page, + openTask = 0, + closedTask = 0 +) => { + const openTaskElement = await page.getByTestId('open-task').textContent(); + + expect(openTaskElement).toContain(`${openTask} Open`); + + const closedTaskElement = await page.getByTestId('closed-task').textContent(); + + expect(closedTaskElement).toContain(`${closedTask} Closed`); +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedProvider/ActivityFeedProvider.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedProvider/ActivityFeedProvider.test.tsx new file mode 100644 index 00000000000..072fb915f27 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedProvider/ActivityFeedProvider.test.tsx @@ -0,0 +1,192 @@ +/* + * 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 { act, fireEvent, render, screen } from '@testing-library/react'; +import React from 'react'; +import { mockUserData } from '../../../mocks/MyDataPage.mock'; +import { + deletePostById, + deleteThread, + getAllFeeds, + postFeedById, +} from '../../../rest/feedsAPI'; +import ActivityFeedProvider from './ActivityFeedProvider'; +import { + DummyChildrenComponent, + DummyChildrenDeletePostComponent, + DummyChildrenEntityComponent, + DummyChildrenTaskCloseComponent, +} from './DummyTestComponent'; + +jest.mock('../../../hooks/useApplicationStore', () => ({ + useApplicationStore: jest.fn(() => ({ + currentUser: mockUserData, + })), +})); + +jest.mock('../ActivityFeedDrawer/ActivityFeedDrawer', () => + jest.fn().mockImplementation(() =>

Entity ActivityFeedDrawer

) +); + +jest.mock('../../../rest/feedsAPI', () => ({ + deletePostById: jest.fn(), + deleteThread: jest.fn(), + getAllFeeds: jest.fn(), + getFeedById: jest.fn(), + postFeedById: jest.fn(), + updatePost: jest.fn(), + updateThread: jest.fn(), +})); + +jest.mock('../../../utils/EntityUtils', () => ({ + getListTestCaseIncidentByStateId: jest.fn(), +})); + +jest.mock('../../../rest/userAPI', () => ({ + getUsers: jest.fn(), +})); + +jest.mock('../../../utils/EntityUtils', () => ({ + getEntityFeedLink: jest.fn(), + getEntityReferenceListFromEntities: jest.fn(), +})); + +jest.mock('../../../utils/ToastUtils', () => ({ + showErrorToast: jest.fn(), +})); + +jest.mock('../../../utils/FeedUtils', () => ({ + getUpdatedThread: jest.fn(), +})); + +describe('ActivityFeedProvider', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should show loading indicator in initial fetch', async () => { + render( + + + + ); + + expect(screen.getByTestId('loading')).toBeInTheDocument(); + }); + + it('should call getFeedData with open task for user', async () => { + await act(async () => { + render( + + + + ); + }); + + expect(getAllFeeds).toHaveBeenCalledWith( + undefined, + undefined, + 'Task', + 'OWNER_OR_FOLLOWS', + 'Open', + undefined, + undefined + ); + }); + + it('should call getFeedData with closed task and afterThread for user', async () => { + await act(async () => { + render( + + + + ); + }); + + expect(getAllFeeds).toHaveBeenCalledWith( + undefined, + 'after-234', + 'Task', + 'OWNER_OR_FOLLOWS', + 'Closed', + undefined, + undefined + ); + }); + + it('should call getFeedData for table entity', async () => { + await act(async () => { + render( + + + + ); + }); + + expect(getAllFeeds).toHaveBeenCalledWith( + undefined, + undefined, + 'Conversation', + 'ALL', + undefined, + undefined, + undefined + ); + }); + + it('should call postFeed with button click', async () => { + await act(async () => { + render( + + + + ); + }); + + fireEvent.click(screen.getByTestId('post-feed')); + + expect(postFeedById).toHaveBeenCalledWith('123', { + from: 'Test User', + message: 'New Post Feed added', + }); + }); + + it('should call deleteThread with button click when isThread is true', async () => { + await act(async () => { + render( + + + + ); + }); + + fireEvent.click(screen.getByTestId('delete-feed')); + + expect(deleteThread).toHaveBeenCalledWith('123'); + expect(deletePostById).not.toHaveBeenCalled(); + }); + + it('should call deletePostId with button click when isThread is false', async () => { + await act(async () => { + render( + + + + ); + }); + + fireEvent.click(screen.getByTestId('delete-feed')); + + expect(deleteThread).not.toHaveBeenCalled(); + expect(deletePostById).toHaveBeenCalledWith('123', '456'); + }); +}); 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 bcf0667013e..ce774b468b3 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 @@ -187,7 +187,7 @@ const ActivityFeedProvider = ({ children, user }: Props) => { after, type, feedFilterType, - taskStatus, + type === ThreadType.Task ? taskStatus : undefined, userId, limit ); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedProvider/DummyTestComponent.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedProvider/DummyTestComponent.tsx new file mode 100644 index 00000000000..727aed41f35 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedProvider/DummyTestComponent.tsx @@ -0,0 +1,111 @@ +/* + * 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 React, { useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import { EntityType } from '../../../enums/entity.enum'; +import { FeedFilter } from '../../../enums/mydata.enum'; +import { + ThreadTaskStatus, + ThreadType, +} from '../../../generated/entity/feed/thread'; +import { useActivityFeedProvider } from './ActivityFeedProvider'; + +export const DummyChildrenComponent = () => { + const { t } = useTranslation(); + const { postFeed, getFeedData, deleteFeed, loading } = + useActivityFeedProvider(); + const handlePostFeed = () => { + postFeed('New Post Feed added', '123'); + }; + + const handleDeleteFeed = () => { + deleteFeed('123', '456', true); + }; + + useEffect(() => { + getFeedData( + FeedFilter.OWNER_OR_FOLLOWS, + undefined, + ThreadType.Task, + EntityType.USER, + 'admin', + ThreadTaskStatus.Open + ); + }, []); + + if (loading) { + return

{t('label.loading')}

; + } + + return ( +
+ + +
+ ); +}; + +export const DummyChildrenTaskCloseComponent = () => { + const { t } = useTranslation(); + const { getFeedData } = useActivityFeedProvider(); + + useEffect(() => { + getFeedData( + FeedFilter.OWNER_OR_FOLLOWS, + 'after-234', + ThreadType.Task, + EntityType.USER, + 'admin', + ThreadTaskStatus.Closed + ); + }, []); + + return

{t('label.children')}

; +}; + +export const DummyChildrenEntityComponent = () => { + const { t } = useTranslation(); + const { getFeedData } = useActivityFeedProvider(); + + useEffect(() => { + getFeedData( + FeedFilter.ALL, + undefined, + ThreadType.Conversation, + EntityType.TABLE, + 'admin', + ThreadTaskStatus.Open + ); + }, []); + + return

{t('label.children')}

; +}; + +export const DummyChildrenDeletePostComponent = () => { + const { t } = useTranslation(); + const { deleteFeed } = useActivityFeedProvider(); + + const handleDeleteFeed = () => { + deleteFeed('123', '456', false); + }; + + return ( + + ); +}; 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 16fda89ba28..1a56297a1c9 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 @@ -72,7 +72,6 @@ import './activity-feed-tab.less'; import { ActivityFeedTabProps, ActivityFeedTabs, - TaskFilter, } from './ActivityFeedTab.interface'; export const ActivityFeedTab = ({ @@ -101,7 +100,9 @@ export const ActivityFeedTab = ({ tab = EntityTabs.ACTIVITY_FEED, subTab: activeTab = ActivityFeedTabs.ALL, } = useParams<{ tab: EntityTabs; subTab: ActivityFeedTabs }>(); - const [taskFilter, setTaskFilter] = useState('open'); + const [taskFilter, setTaskFilter] = useState( + ThreadTaskStatus.Open + ); const [count, setCount] = useState(FEED_COUNT_INITIAL_DATA); const { @@ -228,9 +229,9 @@ export const ActivityFeedTab = ({ const handleFeedFetchFromFeedList = useCallback( (after?: string) => { - getFeedData(feedFilter, after, threadType, entityType, fqn); + getFeedData(feedFilter, after, threadType, entityType, fqn, taskFilter); }, - [threadType, feedFilter, entityType, fqn, getFeedData] + [threadType, feedFilter, entityType, fqn, taskFilter, getFeedData] ); const refetchFeedData = useCallback(() => { @@ -239,10 +240,18 @@ export const ActivityFeedTab = ({ isActivityFeedTab && refetchFeed ) { - getFeedData(feedFilter, undefined, threadType, entityType, fqn); + getFeedData( + feedFilter, + undefined, + threadType, + entityType, + fqn, + taskFilter + ); } }, [ fqn, + taskFilter, feedFilter, threadType, entityType, @@ -263,7 +272,14 @@ export const ActivityFeedTab = ({ useEffect(() => { if (fqn) { - getFeedData(feedFilter, undefined, threadType, entityType, fqn); + getFeedData( + feedFilter, + undefined, + threadType, + entityType, + fqn, + taskFilter + ); } }, [feedFilter, threadType, fqn]); @@ -289,46 +305,14 @@ export const ActivityFeedTab = ({ }); }; - const threads = useMemo(() => { - if (isTaskActiveTab) { - return entityThread.filter( - (thread) => - taskFilter === 'open' - ? thread.task?.status === ThreadTaskStatus.Open - : thread.task?.status === ThreadTaskStatus.Closed, - [] - ); - } - - return entityThread; - }, [activeTab, entityThread, taskFilter]); - - const [openTasks, closedTasks] = useMemo(() => { - if (isTaskActiveTab) { - return entityThread.reduce( - (acc, curr) => { - if (curr.task?.status === ThreadTaskStatus.Open) { - acc[0] = acc[0] + 1; - } else { - acc[1] = acc[1] + 1; - } - - return acc; - }, - [0, 0] - ); - } - - return [0, 0]; - }, [entityThread, activeTab]); - - const handleUpdateTaskFilter = (filter: TaskFilter) => { + const handleUpdateTaskFilter = (filter: ThreadTaskStatus) => { setTaskFilter(filter); + getFeedData(feedFilter, undefined, threadType, entityType, fqn, filter); }; const handleAfterTaskClose = () => { handleFeedFetchFromFeedList(); - handleUpdateTaskFilter('close'); + handleUpdateTaskFilter(ThreadTaskStatus.Closed); }; return ( @@ -417,30 +401,28 @@ export const ActivityFeedTab = ({ className={classNames( 'cursor-pointer p-l-xss d-flex items-center', { - 'font-medium': taskFilter === 'open', + 'font-medium': taskFilter === ThreadTaskStatus.Open, } )} data-testid="open-task" onClick={() => { - handleUpdateTaskFilter('open'); + handleUpdateTaskFilter(ThreadTaskStatus.Open); setActiveThread(); }}> - {' '} - {openTasks}{' '} + {count.openTaskCount}{' '} {t('label.open')} { - handleUpdateTaskFilter('close'); + handleUpdateTaskFilter(ThreadTaskStatus.Closed); setActiveThread(); }}> - {' '} - {closedTasks}{' '} - {t('label.closed')} + {' '} + {count.closedTaskCount} {t('label.closed')} )} @@ -452,7 +434,7 @@ export const ActivityFeedTab = ({ showRepliesContainer: true, }} emptyPlaceholderText={placeholderText} - feedList={threads} + feedList={entityThread} isForFeedTab={isForFeedTab} isLoading={false} selectedThread={selectedThread}