supported separate call for open and close task in activity feed (#17451)

This commit is contained in:
Ashish Gupta 2024-08-15 07:59:50 +05:30 committed by GitHub
parent fc07324254
commit 64c527a734
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 433 additions and 72 deletions

View File

@ -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();

View File

@ -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')

View File

@ -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`);
};

View File

@ -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(() => <p>Entity ActivityFeedDrawer</p>)
);
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(
<ActivityFeedProvider>
<DummyChildrenComponent />
</ActivityFeedProvider>
);
expect(screen.getByTestId('loading')).toBeInTheDocument();
});
it('should call getFeedData with open task for user', async () => {
await act(async () => {
render(
<ActivityFeedProvider>
<DummyChildrenComponent />
</ActivityFeedProvider>
);
});
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(
<ActivityFeedProvider>
<DummyChildrenTaskCloseComponent />
</ActivityFeedProvider>
);
});
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(
<ActivityFeedProvider>
<DummyChildrenEntityComponent />
</ActivityFeedProvider>
);
});
expect(getAllFeeds).toHaveBeenCalledWith(
undefined,
undefined,
'Conversation',
'ALL',
undefined,
undefined,
undefined
);
});
it('should call postFeed with button click', async () => {
await act(async () => {
render(
<ActivityFeedProvider>
<DummyChildrenComponent />
</ActivityFeedProvider>
);
});
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(
<ActivityFeedProvider>
<DummyChildrenComponent />
</ActivityFeedProvider>
);
});
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(
<ActivityFeedProvider>
<DummyChildrenDeletePostComponent />
</ActivityFeedProvider>
);
});
fireEvent.click(screen.getByTestId('delete-feed'));
expect(deleteThread).not.toHaveBeenCalled();
expect(deletePostById).toHaveBeenCalledWith('123', '456');
});
});

View File

@ -187,7 +187,7 @@ const ActivityFeedProvider = ({ children, user }: Props) => {
after,
type,
feedFilterType,
taskStatus,
type === ThreadType.Task ? taskStatus : undefined,
userId,
limit
);

View File

@ -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 <p data-testid="loading">{t('label.loading')}</p>;
}
return (
<div>
<button data-testid="post-feed" onClick={handlePostFeed}>
{t('label.post-feed-button')}
</button>
<button data-testid="delete-feed" onClick={handleDeleteFeed}>
{t('label.delete-feed-button')}
</button>
</div>
);
};
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 <p>{t('label.children')}</p>;
};
export const DummyChildrenEntityComponent = () => {
const { t } = useTranslation();
const { getFeedData } = useActivityFeedProvider();
useEffect(() => {
getFeedData(
FeedFilter.ALL,
undefined,
ThreadType.Conversation,
EntityType.TABLE,
'admin',
ThreadTaskStatus.Open
);
}, []);
return <p>{t('label.children')}</p>;
};
export const DummyChildrenDeletePostComponent = () => {
const { t } = useTranslation();
const { deleteFeed } = useActivityFeedProvider();
const handleDeleteFeed = () => {
deleteFeed('123', '456', false);
};
return (
<button data-testid="delete-feed" onClick={handleDeleteFeed}>
{t('delete-feed-button')}
</button>
);
};

View File

@ -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<TaskFilter>('open');
const [taskFilter, setTaskFilter] = useState<ThreadTaskStatus>(
ThreadTaskStatus.Open
);
const [count, setCount] = useState<FeedCounts>(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();
}}>
{' '}
<TaskIcon className="m-r-xss" width={14} /> {openTasks}{' '}
<TaskIcon className="m-r-xss" width={14} /> {count.openTaskCount}{' '}
{t('label.open')}
</Typography.Text>
<Typography.Text
className={classNames('cursor-pointer d-flex items-center', {
'font-medium': taskFilter === 'close',
'font-medium': taskFilter === ThreadTaskStatus.Closed,
})}
data-testid="closed-task"
onClick={() => {
handleUpdateTaskFilter('close');
handleUpdateTaskFilter(ThreadTaskStatus.Closed);
setActiveThread();
}}>
{' '}
<CheckIcon className="m-r-xss" width={14} /> {closedTasks}{' '}
{t('label.closed')}
<CheckIcon className="m-r-xss" width={14} />{' '}
{count.closedTaskCount} {t('label.closed')}
</Typography.Text>
</div>
)}
@ -452,7 +434,7 @@ export const ActivityFeedTab = ({
showRepliesContainer: true,
}}
emptyPlaceholderText={placeholderText}
feedList={threads}
feedList={entityThread}
isForFeedTab={isForFeedTab}
isLoading={false}
selectedThread={selectedThread}