From 57ed033703f19d7da5844ab0b6b9f72f7541ae87 Mon Sep 17 00:00:00 2001 From: Ashish Gupta Date: Mon, 20 Jan 2025 22:45:17 +0530 Subject: [PATCH] #19406: supported the task filter on landing page widget (#19431) * supported the task filter on landing page widget * added playwright for the test --- .../e2e/Features/ActivityFeed.spec.ts | 94 +++++++++++++++++++ .../FeedsWidget/FeedsWidget.component.tsx | 19 ++-- .../Widgets/FeedsWidget/FeedsWidget.test.tsx | 63 ++++++++++++- .../FeedsFilterPopover.component.tsx | 24 +---- .../FeedsFilterPopover.interface.ts | 2 + .../FeedsFilterPopover.test.tsx | 2 + .../ui/src/constants/Widgets.constant.ts | 51 ++++++++++ .../LandingPageWidget/WidgetsUtils.test.tsx | 56 +++++++++++ .../utils/LandingPageWidget/WidgetsUtils.ts | 35 +++++++ 9 files changed, 317 insertions(+), 29 deletions(-) create mode 100644 openmetadata-ui/src/main/resources/ui/src/constants/Widgets.constant.ts create mode 100644 openmetadata-ui/src/main/resources/ui/src/utils/LandingPageWidget/WidgetsUtils.test.tsx create mode 100644 openmetadata-ui/src/main/resources/ui/src/utils/LandingPageWidget/WidgetsUtils.ts 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 954bd2c3f27..121adeadeec 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 @@ -590,6 +590,100 @@ test.describe('Activity feed', () => { } ); }); + + test('Check Task Filter in Landing Page Widget', async ({ browser }) => { + const { page: page1, afterAction: afterActionUser1 } = + await performUserLogin(browser, user1); + const { page: page2, afterAction: afterActionUser2 } = + await performUserLogin(browser, user2); + + await base.step('Create and Assign Task to User 2', async () => { + await redirectToHomePage(page1); + await entity.visitEntityPage(page1); + + // Create task for the user 2 + await page1.getByTestId('request-description').click(); + await createDescriptionTask(page1, { + term: entity.entity.displayName, + assignee: user2.responseData.name, + }); + + await afterActionUser1(); + }); + + await base.step('Create and Validate Task as per Filters', async () => { + await redirectToHomePage(page2); + await entity.visitEntityPage(page2); + + // Create task for the user 1 + await page2.getByTestId('request-entity-tags').click(); + await createTagTask(page2, { + term: entity.entity.displayName, + tag: 'PII.None', + assignee: user1.responseData.name, + }); + + await redirectToHomePage(page2); + const taskResponse = page2.waitForResponse( + '/api/v1/feed?type=Task&filterType=OWNER&taskStatus=Open&userId=*' + ); + + await page2 + .getByTestId('activity-feed-widget') + .getByText('Tasks') + .click(); + + await taskResponse; + + await expect( + page2.locator( + '[data-testid="activity-feed-widget"] [data-testid="no-data-placeholder"]' + ) + ).not.toBeVisible(); + + // Check the Task based on ALL task filter + await expect(page2.getByTestId('message-container')).toHaveCount(2); + + // Check the Task based on Assigned task filter + await page2.getByTestId('filter-button').click(); + await page2.waitForSelector('.ant-popover ', { state: 'visible' }); + + const taskAssignedResponse = page2.waitForResponse( + '/api/v1/feed?type=Task&filterType=ASSIGNED_TO&taskStatus=Open&userId=*' + ); + await page2.getByText('Assigned').click(); + await page2.getByTestId('selectable-list-update-btn').click(); + + await taskAssignedResponse; + + await expect(page2.getByTestId('message-container')).toHaveCount(1); + + await expect(page2.getByTestId('owner-link')).toContainText( + user2.responseData.displayName + ); + + // Check the Task based on Created by me task filter + + await page2.getByTestId('filter-button').click(); + await page2.waitForSelector('.ant-popover ', { state: 'visible' }); + + const taskCreatedByResponse = page2.waitForResponse( + '/api/v1/feed?type=Task&filterType=ASSIGNED_BY&taskStatus=Open&userId=*' + ); + await page2.getByText('Created By').click(); + await page2.getByTestId('selectable-list-update-btn').click(); + + await taskCreatedByResponse; + + await expect(page2.getByTestId('message-container')).toHaveCount(1); + + await expect(page2.getByTestId('owner-link')).toContainText( + user1.responseData.displayName + ); + + await afterActionUser2(); + }); + }); }); base.describe('Activity feed with Data Consumer User', () => { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/MyData/Widgets/FeedsWidget/FeedsWidget.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/MyData/Widgets/FeedsWidget/FeedsWidget.component.tsx index b7ea93167d0..43cdd5660d7 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/MyData/Widgets/FeedsWidget/FeedsWidget.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/MyData/Widgets/FeedsWidget/FeedsWidget.component.tsx @@ -20,6 +20,7 @@ import { Link, useHistory } from 'react-router-dom'; import { PAGE_SIZE_MEDIUM, ROUTES } from '../../../../constants/constants'; import { FEED_COUNT_INITIAL_DATA } from '../../../../constants/entity.constants'; import { mockFeedData } from '../../../../constants/mockTourData.constants'; +import { TAB_SUPPORTED_FILTER } from '../../../../constants/Widgets.constant'; import { useTourProvider } from '../../../../context/TourProvider/TourProvider'; import { EntityTabs, EntityType } from '../../../../enums/entity.enum'; import { FeedFilter } from '../../../../enums/mydata.enum'; @@ -76,7 +77,7 @@ const FeedsWidget = ({ getFeedData(FeedFilter.MENTIONS); } else if (activeTab === ActivityFeedTabs.TASKS) { getFeedData( - FeedFilter.OWNER, + defaultFilter, undefined, ThreadType.Task, undefined, @@ -106,7 +107,12 @@ const FeedsWidget = ({ [count.openTaskCount, activeTab] ); - const onTabChange = (key: string) => setActiveTab(key as ActivityFeedTabs); + const onTabChange = (key: string) => { + if (key === ActivityFeedTabs.TASKS) { + setDefaultFilter(FeedFilter.OWNER); + } + setActiveTab(key as ActivityFeedTabs); + }; const redirectToUserPage = useCallback(() => { history.push( @@ -259,13 +265,10 @@ const FeedsWidget = ({ ]} tabBarExtraContent={ - {activeTab === ActivityFeedTabs.ALL && ( + {TAB_SUPPORTED_FILTER.includes(activeTab) && ( )} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/MyData/Widgets/FeedsWidget/FeedsWidget.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/MyData/Widgets/FeedsWidget/FeedsWidget.test.tsx index 22958554893..4ad32a7a273 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/MyData/Widgets/FeedsWidget/FeedsWidget.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/MyData/Widgets/FeedsWidget/FeedsWidget.test.tsx @@ -82,7 +82,16 @@ jest.mock( jest.mock( '../../../common/FeedsFilterPopover/FeedsFilterPopover.component', - () => jest.fn().mockImplementation(({ children }) =>

{children}

) + () => + jest.fn().mockImplementation(({ onUpdate }) => ( +
+ + + +
+ )) ); jest.mock('../../../../rest/feedsAPI', () => ({ @@ -193,4 +202,56 @@ describe('FeedsWidget', () => { expect(mockHandleRemoveWidget).toHaveBeenCalledWith(widgetProps.widgetKey); }); + + it('should call api with correct parameters based on the tab selected', () => { + render(); + const tabs = screen.getAllByRole('tab'); + const conversationTab = tabs[0]; + fireEvent.click(conversationTab); + + // initial API call for the Feed + expect(conversationTab.getAttribute('aria-selected')).toBe('true'); + expect(mockUseActivityFeedProviderValue.getFeedData).toHaveBeenCalledWith( + 'OWNER_OR_FOLLOWS', + undefined, + 'Conversation', + undefined, + undefined, + undefined, + 25 + ); + + // Reset mock between checks + mockUseActivityFeedProviderValue.getFeedData.mockReset(); + + // Testing for "Task Tab", to call API with OWNER filter parameters + const taskTab = tabs[2]; + fireEvent.click(taskTab); + + expect(taskTab.getAttribute('aria-selected')).toBe('true'); + expect(mockUseActivityFeedProviderValue.getFeedData).toHaveBeenCalledWith( + 'OWNER', + undefined, + 'Task', + undefined, + undefined, + 'Open' + ); + + // Reset mock for the next check + mockUseActivityFeedProviderValue.getFeedData.mockReset(); + + // Applying the filter for the assigned button + const assignedFilterButton = screen.getByText('assigned_button'); + fireEvent.click(assignedFilterButton); + + expect(mockUseActivityFeedProviderValue.getFeedData).toHaveBeenCalledWith( + 'ASSIGNED_TO', + undefined, + 'Task', + undefined, + undefined, + 'Open' + ); + }); }); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/FeedsFilterPopover/FeedsFilterPopover.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/FeedsFilterPopover/FeedsFilterPopover.component.tsx index f30be520f93..1e01120c2a9 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/FeedsFilterPopover/FeedsFilterPopover.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/FeedsFilterPopover/FeedsFilterPopover.component.tsx @@ -25,10 +25,12 @@ import { ReactComponent as FilterIcon } from '../../../assets/svg/ic-feeds-filte import { FeedFilter } from '../../../enums/mydata.enum'; import { useApplicationStore } from '../../../hooks/useApplicationStore'; +import { getFeedFilterWidgets } from '../../../utils/LandingPageWidget/WidgetsUtils'; import './feeds-filter-popover.less'; import { FeedsFilterPopoverProps } from './FeedsFilterPopover.interface'; const FeedsFilterPopover = ({ + feedTab, defaultFilter, onUpdate, }: FeedsFilterPopoverProps) => { @@ -39,26 +41,8 @@ const FeedsFilterPopover = ({ useState(defaultFilter); const items = useMemo( - () => [ - { - title: t('label.all'), - key: currentUser?.isAdmin - ? FeedFilter.ALL - : FeedFilter.OWNER_OR_FOLLOWS, - description: t('message.feed-filter-all'), - }, - { - title: t('label.my-data'), - key: FeedFilter.OWNER, - description: t('message.feed-filter-owner'), - }, - { - title: t('label.following'), - key: FeedFilter.FOLLOWS, - description: t('message.feed-filter-following'), - }, - ], - [currentUser] + () => getFeedFilterWidgets(feedTab, currentUser?.isAdmin), + [currentUser?.isAdmin, feedTab] ); const onFilterUpdate = useCallback(() => { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/FeedsFilterPopover/FeedsFilterPopover.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/common/FeedsFilterPopover/FeedsFilterPopover.interface.ts index bb1d41f156f..66690980d7c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/FeedsFilterPopover/FeedsFilterPopover.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/FeedsFilterPopover/FeedsFilterPopover.interface.ts @@ -11,8 +11,10 @@ * limitations under the License. */ import { FeedFilter } from '../../../enums/mydata.enum'; +import { ActivityFeedTabs } from '../../ActivityFeed/ActivityFeedTab/ActivityFeedTab.interface'; export interface FeedsFilterPopoverProps { + feedTab: ActivityFeedTabs; defaultFilter: FeedFilter; onUpdate: (value: FeedFilter) => void; } diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/FeedsFilterPopover/FeedsFilterPopover.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/FeedsFilterPopover/FeedsFilterPopover.test.tsx index 77c737e766f..98de8a954e7 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/FeedsFilterPopover/FeedsFilterPopover.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/FeedsFilterPopover/FeedsFilterPopover.test.tsx @@ -13,10 +13,12 @@ import { act, fireEvent, render, screen } from '@testing-library/react'; import React from 'react'; import { FeedFilter } from '../../../enums/mydata.enum'; +import { ActivityFeedTabs } from '../../ActivityFeed/ActivityFeedTab/ActivityFeedTab.interface'; import FeedsFilterPopover from './FeedsFilterPopover.component'; const onUpdateMock = jest.fn(); const mockProps = { + feedTab: ActivityFeedTabs.ALL, defaultFilter: FeedFilter.ALL, onUpdate: onUpdateMock, }; diff --git a/openmetadata-ui/src/main/resources/ui/src/constants/Widgets.constant.ts b/openmetadata-ui/src/main/resources/ui/src/constants/Widgets.constant.ts new file mode 100644 index 00000000000..74ce1b1ff41 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/constants/Widgets.constant.ts @@ -0,0 +1,51 @@ +/* + * Copyright 2025 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 { ActivityFeedTabs } from '../components/ActivityFeed/ActivityFeedTab/ActivityFeedTab.interface'; +import { FeedFilter } from '../enums/mydata.enum'; +import i18n from '../utils/i18next/LocalUtil'; + +export const TAB_SUPPORTED_FILTER = [ + ActivityFeedTabs.ALL, + ActivityFeedTabs.TASKS, +]; + +export const TASK_FEED_FILTER_LIST = [ + { + title: i18n.t('label.all'), + key: FeedFilter.OWNER, + description: i18n.t('message.feed-filter-all'), + }, + { + title: i18n.t('label.assigned'), + key: FeedFilter.ASSIGNED_TO, + description: i18n.t('message.feed-filter-owner'), + }, + { + title: i18n.t('label.created-by'), + key: FeedFilter.ASSIGNED_BY, + description: i18n.t('message.feed-filter-following'), + }, +]; + +export const ACTIVITY_FEED_FILTER_LIST = [ + { + title: i18n.t('label.my-data'), + key: FeedFilter.OWNER, + description: i18n.t('message.feed-filter-owner'), + }, + { + title: i18n.t('label.following'), + key: FeedFilter.FOLLOWS, + description: i18n.t('message.feed-filter-following'), + }, +]; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/LandingPageWidget/WidgetsUtils.test.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/LandingPageWidget/WidgetsUtils.test.tsx new file mode 100644 index 00000000000..5c395673af6 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/utils/LandingPageWidget/WidgetsUtils.test.tsx @@ -0,0 +1,56 @@ +/* + * Copyright 2025 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 { ActivityFeedTabs } from '../../components/ActivityFeed/ActivityFeedTab/ActivityFeedTab.interface'; +import { + ACTIVITY_FEED_FILTER_LIST, + TASK_FEED_FILTER_LIST, +} from '../../constants/Widgets.constant'; +import { FeedFilter } from '../../enums/mydata.enum'; +import i18n from '../i18next/LocalUtil'; +import { getFeedFilterWidgets } from './WidgetsUtils'; + +describe('Widgets Utils', () => { + describe('getFeedFilterWidgets', () => { + it('should return list for Feed Filters', () => { + const response = getFeedFilterWidgets(ActivityFeedTabs.ALL); + + expect(response).toEqual([ + { + title: i18n.t('label.all'), + key: FeedFilter.OWNER_OR_FOLLOWS, + description: i18n.t('message.feed-filter-all'), + }, + ...ACTIVITY_FEED_FILTER_LIST, + ]); + }); + + it('should return list for Feed Filters for Admin', () => { + const response = getFeedFilterWidgets(ActivityFeedTabs.ALL, true); + + expect(response).toEqual([ + { + title: i18n.t('label.all'), + key: FeedFilter.ALL, + description: i18n.t('message.feed-filter-all'), + }, + ...ACTIVITY_FEED_FILTER_LIST, + ]); + }); + + it('should return list for Task Filters', () => { + const response = getFeedFilterWidgets(ActivityFeedTabs.TASKS); + + expect(response).toEqual(TASK_FEED_FILTER_LIST); + }); + }); +}); diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/LandingPageWidget/WidgetsUtils.ts b/openmetadata-ui/src/main/resources/ui/src/utils/LandingPageWidget/WidgetsUtils.ts new file mode 100644 index 00000000000..d8131847369 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/utils/LandingPageWidget/WidgetsUtils.ts @@ -0,0 +1,35 @@ +/* + * Copyright 2025 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 { ActivityFeedTabs } from '../../components/ActivityFeed/ActivityFeedTab/ActivityFeedTab.interface'; +import { + ACTIVITY_FEED_FILTER_LIST, + TASK_FEED_FILTER_LIST, +} from '../../constants/Widgets.constant'; +import { FeedFilter } from '../../enums/mydata.enum'; +import i18n from '../i18next/LocalUtil'; + +export const getFeedFilterWidgets = ( + tab: ActivityFeedTabs, + isAdmin?: boolean +) => { + return tab === ActivityFeedTabs.TASKS + ? TASK_FEED_FILTER_LIST + : [ + { + title: i18n.t('label.all'), + key: isAdmin ? FeedFilter.ALL : FeedFilter.OWNER_OR_FOLLOWS, + description: i18n.t('message.feed-filter-all'), + }, + ...ACTIVITY_FEED_FILTER_LIST, + ]; +};