diff --git a/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/Glossary.spec.ts b/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/Glossary.spec.ts index d64b2e70e854..02a9859b5ad8 100644 --- a/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/Glossary.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/Glossary.spec.ts @@ -382,7 +382,10 @@ const approveGlossaryTermWorkflow = ({ glossary, glossaryTerm }) => { interceptURL('PUT', '/api/v1/feed/tasks/*/resolve', 'resolveTask'); - cy.get('[data-testid="approve-task"]').click(); + // approve the task + cy.get( + '[data-testid="glossary-accept-reject-task-dropdown"] .ant-btn-compact-first-item > span' + ).click(); verifyResponseStatusCode('@resolveTask', 200); 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 ecbc948cf4a0..ebdde5a1e5d6 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 @@ -16,6 +16,8 @@ import { UserClass } from '../../support/user/UserClass'; import { checkDescriptionInEditModal } from '../../utils/activityFeed'; import { createNewPage, + performAdminLogin, + performUserLogin, redirectToHomePage, toastNotification, visitUserProfilePage, @@ -27,18 +29,21 @@ import { TaskDetails, } from '../../utils/task'; -// use the admin user to login -test.use({ storageState: 'playwright/.auth/admin.json' }); - const entity = new TableClass(); -const user = new UserClass(); +const entity2 = new TableClass(); +const user1 = new UserClass(); +const user2 = new UserClass(); test.describe('Activity feed', () => { + // use the admin user to login + test.use({ storageState: 'playwright/.auth/admin.json' }); + test.beforeAll('Setup pre-requests', async ({ browser }) => { const { apiContext, afterAction } = await createNewPage(browser); await entity.create(apiContext); - await user.create(apiContext); + await entity2.create(apiContext); + await user1.create(apiContext); await afterAction(); }); @@ -50,7 +55,8 @@ test.describe('Activity feed', () => { test.afterAll('Cleanup', async ({ browser }) => { const { apiContext, afterAction } = await createNewPage(browser); await entity.delete(apiContext); - await user.delete(apiContext); + await entity2.delete(apiContext); + await user1.delete(apiContext); await afterAction(); }); @@ -58,7 +64,7 @@ test.describe('Activity feed', () => { test('Assigned task should appear to task tab', async ({ page }) => { const value: TaskDetails = { term: entity.entity.name, - assignee: user.responseData.name, + assignee: user1.responseData.name, }; await entity.visitEntityPage(page); @@ -186,7 +192,7 @@ test.describe('Activity feed', () => { test('Update Description Task on Columns', async ({ page }) => { const firstTaskValue: TaskDetails = { term: entity.entity.name, - assignee: user.responseData.name, + assignee: user1.responseData.name, description: 'Column Description 1', columnName: entity.entity.columns[0].name, oldDescription: entity.entity.columns[0].description, @@ -246,4 +252,225 @@ test.describe('Activity feed', () => { expect(closedTask).toContain('2 Closed'); }); + + test('Comment and Close Task should work in Task Flow', async ({ page }) => { + const value: TaskDetails = { + term: entity2.entity.name, + assignee: user1.responseData.name, + }; + 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'); + + // Check the editor send button is not visible and comment button is disabled when no text is added + expect(page.locator('[data-testid="send-button"]')).not.toBeVisible(); + expect( + await page.locator('[data-testid="comment-button"]').isDisabled() + ).toBeTruthy(); + + await page.fill( + '[data-testid="editor-wrapper"] .ql-editor', + 'Test comment added' + ); + const addComment = page.waitForResponse('/api/v1/feed/*/posts'); + await page.getByTestId('comment-button').click(); + await addComment; + + // Close the task from the Button.Group, should throw error when no comment is added. + await page.getByRole('button', { name: 'down' }).click(); + await page.waitForSelector('.ant-dropdown', { + state: 'visible', + }); + + await page.getByRole('menuitem', { name: 'close' }).click(); + + await toastNotification(page, 'Task cannot be closed without a comment.'); + + // Close the task from the Button.Group, with comment is added. + 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; + + await toastNotification(page, 'Task closed successfully.'); + + const openTask = await page.getByTestId('open-task').textContent(); + + expect(openTask).toContain('0 Open'); + + const closedTask = await page.getByTestId('closed-task').textContent(); + + expect(closedTask).toContain('1 Closed'); + }); +}); + +test.describe('Activity feed with Data Steward User', () => { + test.slow(true); + + test.beforeAll('Setup pre-requests', async ({ browser }) => { + const { afterAction, apiContext } = await performAdminLogin(browser); + + await entity.create(apiContext); + await user1.create(apiContext); + await user2.create(apiContext); + await afterAction(); + }); + + test.afterAll('Cleanup', async ({ browser }) => { + const { afterAction, apiContext } = await performAdminLogin(browser); + await entity.delete(apiContext); + await user1.delete(apiContext); + await user2.delete(apiContext); + + await afterAction(); + }); + + test('Create and Assign Task', async ({ browser }) => { + const { page: page1, afterAction: afterActionUser1 } = + await performUserLogin(browser, user1); + const { page: page2, afterAction: afterActionUser2 } = + await performUserLogin(browser, user2); + + const value: TaskDetails = { + term: entity.entity.name, + assignee: user2.responseData.name, + }; + + await test.step('Create, Close and Assign Task to User 2', async () => { + await redirectToHomePage(page1); + await entity.visitEntityPage(page1); + + // Create 2 task for the same entity, one to close and 2nd for the user2 action + await page1.getByTestId('request-description').click(); + await createDescriptionTask(page1, value); + + await page1.getByTestId('schema').click(); + + await page1.getByTestId('request-entity-tags').click(); + + // create tag task + await createTagTask(page1, { ...value, tag: 'PII.None' }); + + // Should only see the close and comment button + expect( + await page1.locator('[data-testid="comment-button"]').isDisabled() + ).toBeTruthy(); + expect(page1.locator('[data-testid="close-button"]')).toBeVisible(); + expect( + page1.locator('[data-testid="edit-accept-task-dropdown"]') + ).not.toBeVisible(); + + // Close 1st task + await page1.fill( + '[data-testid="editor-wrapper"] .ql-editor', + 'Closing the task with comment' + ); + const commentWithCloseTask = page1.waitForResponse( + '/api/v1/feed/tasks/*/close' + ); + page1.locator('[data-testid="close-button"]').click(); + await commentWithCloseTask; + + // TODO: Ashish - Fix the toast notification once issue is resolved from Backend https://github.com/open-metadata/OpenMetadata/issues/17059 + + // await toastNotification(page1, 'Task closed successfully.'); + await toastNotification( + page1, + 'An exception with message [Cannot invoke "org.openmetadata.schema.type.EntityReference.getName()" because "owner" is null] was thrown while processing request.' + ); + + // TODO: Ashish - Enable them once issue is resolved from Backend https://github.com/open-metadata/OpenMetadata/issues/17059 + // const openTask = await page1.getByTestId('open-task').textContent(); + // expect(openTask).toContain('1 Open'); + // const closedTask = await page1.getByTestId('closed-task').textContent(); + // expect(closedTask).toContain('1 Closed'); + + await afterActionUser1(); + }); + + await test.step('Accept Task By User 2', async () => { + 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(); + + const entityPageTaskTab = page2.waitForResponse( + '/api/v1/feed?*&type=Task' + ); + + const tagsTask = page2.getByTestId('redirect-task-button-link').first(); + const tagsTaskContent = await tagsTask.innerText(); + + expect(tagsTaskContent).toContain('Request tags for'); + + await tagsTask.click(); + await entityPageTaskTab; + + // TODO: Ashish - Enable them once issue is resolved from Backend https://github.com/open-metadata/OpenMetadata/issues/17059 + // Count for task should be 1 both open and closed + + // const openTaskBefore = await page2.getByTestId('open-task').textContent(); + // expect(openTaskBefore).toContain('1 Open'); + + // const closedTaskBefore = await page2 + // .getByTestId('closed-task') + // .textContent(); + // expect(closedTaskBefore).toContain('1 Closed'); + + // Should not see the close button + expect(page2.locator('[data-testid="close-button"]')).not.toBeVisible(); + + expect( + await page2.locator('[data-testid="comment-button"]').isDisabled() + ).toBeTruthy(); + + expect( + page2.locator('[data-testid="edit-accept-task-dropdown"]') + ).toBeVisible(); + + await page2.getByText('Accept Suggestion').click(); + + await toastNotification(page2, /Task resolved successfully/); + + // TODO: Ashish - Enable them once issue is resolved from Backend https://github.com/open-metadata/OpenMetadata/issues/17059 + // const openTask = await page2.getByTestId('open-task').textContent(); + // expect(openTask).toContain('0 Open'); + + const closedTask = await page2.getByTestId('closed-task').textContent(); + + expect(closedTask).toContain('1 Closed'); + + await afterActionUser2(); + }); + }); }); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/common.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/common.ts index 3e1aaa8f0a3f..6d5eb2af60cd 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/common.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/common.ts @@ -150,7 +150,7 @@ export const clickOutside = async (page: Page) => { export const visitUserProfilePage = async (page: Page) => { await page.getByTestId('dropdown-profile').click(); - await page.waitForSelector('.profile-dropdown', { + await page.waitForSelector('[role="menu"].profile-dropdown', { state: 'visible', }); const userResponse = page.waitForResponse( 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 621aafdcc6a4..cb4a3b872eac 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/glossary.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/glossary.ts @@ -553,7 +553,7 @@ export const approveGlossaryTermTask = async ( ) => { await validateGlossaryTermTask(page, term); const taskResolve = page.waitForResponse('/api/v1/feed/tasks/*/resolve'); - await page.click('[data-testid="approve-task"]'); + await page.getByRole('button', { name: 'Approve' }).click(); await taskResolve; // Display toast notification diff --git a/openmetadata-ui/src/main/resources/ui/src/assets/svg/cancel-colored.svg b/openmetadata-ui/src/main/resources/ui/src/assets/svg/cancel-colored.svg new file mode 100644 index 000000000000..fa0637cfcc5d --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/assets/svg/cancel-colored.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/openmetadata-ui/src/main/resources/ui/src/assets/svg/edit-colored.svg b/openmetadata-ui/src/main/resources/ui/src/assets/svg/edit-colored.svg new file mode 100644 index 000000000000..fae0d662ca27 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/assets/svg/edit-colored.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/openmetadata-ui/src/main/resources/ui/src/assets/svg/plus-colored.svg b/openmetadata-ui/src/main/resources/ui/src/assets/svg/plus-colored.svg new file mode 100644 index 000000000000..f6c774e396e2 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/assets/svg/plus-colored.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/openmetadata-ui/src/main/resources/ui/src/assets/svg/success-colored.svg b/openmetadata-ui/src/main/resources/ui/src/assets/svg/success-colored.svg new file mode 100644 index 000000000000..97c7c4510a82 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/assets/svg/success-colored.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedEditor/ActivityFeedEditor.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedEditor/ActivityFeedEditor.tsx index 54e1ec4180a0..d1e21c7db054 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedEditor/ActivityFeedEditor.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedEditor/ActivityFeedEditor.tsx @@ -12,8 +12,15 @@ */ import classNames from 'classnames'; -import React, { FC, HTMLAttributes, useRef, useState } from 'react'; +import React, { + forwardRef, + HTMLAttributes, + useImperativeHandle, + useRef, + useState, +} from 'react'; import { getBackendFormat, HTMLToMarkdown } from '../../../utils/FeedUtils'; +import { editorRef } from '../../common/RichTextEditor/RichTextEditor.interface'; import { FeedEditor } from '../FeedEditor/FeedEditor'; import { KeyHelp } from './KeyHelp'; import { SendButton } from './SendButton'; @@ -33,60 +40,73 @@ export type EditorContentRef = { clearEditorValue: () => string; }; -const ActivityFeedEditor: FC = ({ - className, - editorClass, - onSave, - placeHolder, - defaultValue, - onTextChange, - editAction, - focused = false, -}) => { - const editorRef = useRef(); - const [editorValue, setEditorValue] = useState(''); +const ActivityFeedEditor = forwardRef( + ( + { + className, + editorClass, + onSave, + placeHolder, + defaultValue, + onTextChange, + editAction, + focused = false, + }, + ref + ) => { + const editorRef = useRef(); + const [editorValue, setEditorValue] = useState(''); - const onChangeHandler = (value: string) => { - const markdown = HTMLToMarkdown.turndown(value); - const backendFormat = getBackendFormat(markdown); - setEditorValue(markdown); - onTextChange && onTextChange(backendFormat); - }; + const onChangeHandler = (value: string) => { + const markdown = HTMLToMarkdown.turndown(value); + const backendFormat = getBackendFormat(markdown); + setEditorValue(markdown); + onTextChange && onTextChange(backendFormat); + }; - const onSaveHandler = () => { - if (editorRef.current) { - if (editorRef.current?.getEditorValue()) { - setEditorValue(''); - editorRef.current?.clearEditorValue(); - const message = getBackendFormat(editorRef.current?.getEditorValue()); - onSave && onSave(message); + const onSaveHandler = () => { + if (editorRef.current) { + if (editorRef.current?.getEditorValue()) { + setEditorValue(''); + editorRef.current?.clearEditorValue(); + const message = getBackendFormat(editorRef.current?.getEditorValue()); + onSave && onSave(message); + } } - } - }; + }; - return ( -
e.stopPropagation()}> - - {editAction ? ( - editAction - ) : ( - <> - - - - )} -
- ); -}; + /** + * Handle forward ref logic and provide method access to parent component + */ + useImperativeHandle(ref, () => ({ + ...editorRef.current, + })); + + return ( +
e.stopPropagation()}> + + {editAction ?? ( + <> + + + + )} +
+ ); + } +); export default ActivityFeedEditor; 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 2ed174a86480..3de047d3847e 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 @@ -420,6 +420,7 @@ export const ActivityFeedTab = ({ 'font-medium': taskFilter === 'open', } )} + data-testid="open-task" onClick={() => { handleUpdateTaskFilter('open'); setActiveThread(); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Entity/Task/TaskTab/TaskTab.component.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Entity/Task/TaskTab/TaskTab.component.test.tsx index 2f9a3ea310a6..28f0d91464ea 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Entity/Task/TaskTab/TaskTab.component.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Entity/Task/TaskTab/TaskTab.component.test.tsx @@ -15,7 +15,14 @@ import { render, screen } from '@testing-library/react'; import React from 'react'; import { MemoryRouter } from 'react-router-dom'; import { EntityType } from '../../../../enums/entity.enum'; -import { TASK_COLUMNS, TASK_FEED } from '../../../../mocks/Task.mock'; +import { useAuth } from '../../../../hooks/authHooks'; +import { + MOCK_TASK, + MOCK_TASK_2, + MOCK_TASK_3, + TASK_COLUMNS, + TASK_FEED, +} from '../../../../mocks/Task.mock'; import { mockUserData } from '../../../Settings/Users/mocks/User.mocks'; import { TaskTab } from './TaskTab.component'; import { TaskTabProps } from './TaskTab.interface'; @@ -39,7 +46,12 @@ jest.mock('../../../ActivityFeed/ActivityFeedCardV2/ActivityFeedCardV2', () => { }); jest.mock('../../../ActivityFeed/ActivityFeedEditor/ActivityFeedEditor', () => { - return jest.fn().mockImplementation(() =>

ActivityFeedEditor

); + return jest.fn().mockImplementation(({ editAction }) => ( +
+

ActivityFeedEditor

+ {editAction} +
+ )); }); jest.mock('../../../common/AssigneeList/AssigneeList', () => { @@ -133,11 +145,7 @@ jest.mock( ); jest.mock('../../../../hooks/authHooks', () => ({ - useAuth: () => { - return { - isAdminUser: false, - }; - }, + useAuth: jest.fn().mockReturnValue({ isAdminUser: false }), })); const mockOnAfterClose = jest.fn(); @@ -168,6 +176,108 @@ describe('Test TaskFeedCard component', () => { wrapper: MemoryRouter, }); - expect(screen.getByTestId('task-cta-buttons')).toBeEmptyDOMElement(); + expect(screen.getByTestId('task-cta-buttons')).toHaveTextContent( + 'label.comment' + ); + expect(screen.getByTestId('task-cta-buttons')).not.toHaveTextContent( + 'label.accept-suggestion' + ); + expect(screen.getByTestId('task-cta-buttons')).not.toHaveTextContent( + 'label.add-entity' + ); + expect(screen.getByTestId('task-cta-buttons')).not.toHaveTextContent( + 'label.add-suggestion' + ); + }); + + it('should render close button if the user is creator task', async () => { + render( + , + { + wrapper: MemoryRouter, + } + ); + + expect(screen.getByText('label.close')).toBeInTheDocument(); + }); + + it('should not render close button if the user is not a creator of task', async () => { + render(, { + wrapper: MemoryRouter, + }); + + expect(screen.queryByText('label.close')).not.toBeInTheDocument(); + }); + + it('should not render close button if the user is a creator and even have hasEditAccess of task', async () => { + (useAuth as jest.Mock).mockImplementation(() => ({ + isAdminUser: true, + })); + + render( + , + { + wrapper: MemoryRouter, + } + ); + + expect(screen.queryByText('label.close')).not.toBeInTheDocument(); + }); + + it('should not render close button if the user is a creator and assignee of task', async () => { + render( + , + { + wrapper: MemoryRouter, + } + ); + + expect(screen.queryByText('label.close')).not.toBeInTheDocument(); + }); + + it('should render dropdown button with add and close tag if task created with no tags', async () => { + render( + , + { + wrapper: MemoryRouter, + } + ); + + expect(screen.getByTestId('add-close-task-dropdown')).toBeInTheDocument(); + expect(screen.getByText('label.add-entity')).toBeInTheDocument(); + expect(screen.getByText('label.comment')).toBeInTheDocument(); + }); + + it('should render dropdown button with resolve and reject tag if task is Glossary approval', async () => { + render( + , + { + wrapper: MemoryRouter, + } + ); + + expect( + screen.getByTestId('glossary-accept-reject-task-dropdown') + ).toBeInTheDocument(); + expect(screen.getByText('label.approve')).toBeInTheDocument(); + expect(screen.getByText('label.comment')).toBeInTheDocument(); }); }); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Entity/Task/TaskTab/TaskTab.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Entity/Task/TaskTab/TaskTab.component.tsx index f126cfa0b07a..57ffed8b5ae9 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Entity/Task/TaskTab/TaskTab.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Entity/Task/TaskTab/TaskTab.component.tsx @@ -49,6 +49,8 @@ import { useHistory } from 'react-router-dom'; import { ReactComponent as EditIcon } from '../../../../assets/svg/edit-new.svg'; import { ReactComponent as TaskCloseIcon } from '../../../../assets/svg/ic-close-task.svg'; import { ReactComponent as TaskOpenIcon } from '../../../../assets/svg/ic-open-task.svg'; +import { ReactComponent as AddColored } from '../../../../assets/svg/plus-colored.svg'; + import { DE_ACTIVE_COLOR } from '../../../../constants/constants'; import { TaskOperation } from '../../../../constants/Feeds.constants'; import { TASK_TYPES } from '../../../../constants/Task.constant'; @@ -88,21 +90,25 @@ import { fetchOptions, generateOptions, getTaskDetailPath, + GLOSSARY_TASK_ACTION_LIST, INCIDENT_TASK_ACTION_LIST, isDescriptionTask, isTagsTask, + TASK_ACTION_COMMON_ITEM, TASK_ACTION_LIST, } from '../../../../utils/TasksUtils'; import { showErrorToast, showSuccessToast } from '../../../../utils/ToastUtils'; import ActivityFeedCardV2 from '../../../ActivityFeed/ActivityFeedCardV2/ActivityFeedCardV2'; -import ActivityFeedEditor from '../../../ActivityFeed/ActivityFeedEditor/ActivityFeedEditor'; +import ActivityFeedEditor, { + EditorContentRef, +} from '../../../ActivityFeed/ActivityFeedEditor/ActivityFeedEditor'; import { useActivityFeedProvider } from '../../../ActivityFeed/ActivityFeedProvider/ActivityFeedProvider'; import AssigneeList from '../../../common/AssigneeList/AssigneeList'; import InlineEdit from '../../../common/InlineEdit/InlineEdit.component'; import { OwnerLabel } from '../../../common/OwnerLabel/OwnerLabel.component'; import EntityPopOverCard from '../../../common/PopOverCard/EntityPopOverCard'; import RichTextEditor from '../../../common/RichTextEditor/RichTextEditor'; -import { EditorContentRef } from '../../../Modals/ModalWithMarkdownEditor/ModalWithMarkdownEditor.interface'; +import { EditorContentRef as MarkdownEditorContentRef } from '../../../Modals/ModalWithMarkdownEditor/ModalWithMarkdownEditor.interface'; import TaskTabIncidentManagerHeader from '../TaskTabIncidentManagerHeader/TaskTabIncidentManagerHeader.component'; import './task-tab.less'; import { TaskTabProps } from './TaskTab.interface'; @@ -114,10 +120,11 @@ export const TaskTab = ({ hasGlossaryReviewer, ...rest }: TaskTabProps) => { + const editorRef = useRef(); const history = useHistory(); const [assigneesForm] = useForm(); const { currentUser } = useApplicationStore(); - const markdownRef = useRef(); + const markdownRef = useRef(); const updatedAssignees = Form.useWatch('assignees', assigneesForm); const { permissions } = usePermissionProvider(); const { task: taskDetails } = taskThread; @@ -143,9 +150,56 @@ export const TaskTab = ({ testCaseResolutionStatus, initialAssignees: usersList, } = useActivityFeedProvider(); + + const isTaskDescription = isDescriptionTask(taskDetails?.type as TaskType); + + const isTaskTags = isTagsTask(taskDetails?.type as TaskType); + + const showAddSuggestionButton = useMemo(() => { + const taskType = taskDetails?.type ?? ('' as TaskType); + const parsedSuggestion = [ + TaskType.UpdateDescription, + TaskType.RequestDescription, + ].includes(taskType) + ? taskDetails?.suggestion + : JSON.parse(taskDetails?.suggestion || '[]'); + + return ( + [TaskType.RequestTag, TaskType.RequestDescription].includes(taskType) && + isEmpty(parsedSuggestion) + ); + }, [taskDetails]); + + const noSuggestionTaskMenuOptions = useMemo(() => { + let label; + + if (taskThread.task?.newValue) { + label = t('label.add-suggestion'); + } else if (isTaskTags) { + label = t('label.add-entity', { + entity: t('label.tag-plural'), + }); + } else { + label = t('label.add-entity', { + entity: t('label.description'), + }); + } + + return [ + { + label, + key: TaskActionMode.EDIT, + icon: AddColored, + }, + ...TASK_ACTION_COMMON_ITEM, + ]; + }, [isTaskTags, taskThread.task?.newValue]); + const isTaskTestCaseResult = taskDetails?.type === TaskType.RequestTestCaseFailureResolution; + const isTaskGlossaryApproval = taskDetails?.type === TaskType.RequestApproval; + const latestAction = useMemo(() => { const resolutionStatus = last(testCaseResolutionStatus); @@ -157,10 +211,20 @@ export const TaskTab = ({ default: return INCIDENT_TASK_ACTION_LIST[0]; } + } else if (isTaskGlossaryApproval) { + return GLOSSARY_TASK_ACTION_LIST[0]; + } else if (showAddSuggestionButton) { + return noSuggestionTaskMenuOptions[0]; } else { return TASK_ACTION_LIST[0]; } - }, [testCaseResolutionStatus, isTaskTestCaseResult]); + }, [ + showAddSuggestionButton, + testCaseResolutionStatus, + isTaskGlossaryApproval, + isTaskTestCaseResult, + noSuggestionTaskMenuOptions, + ]); const [taskAction, setTaskAction] = useState(latestAction); const [isActionLoading, setIsActionLoading] = useState(false); @@ -212,11 +276,12 @@ export const TaskTab = ({ assignee.type === 'team' ? checkIfUserPartOfTeam(assignee.id) : false ); - const isTaskDescription = isDescriptionTask(taskDetails?.type as TaskType); - - const isTaskTags = isTagsTask(taskDetails?.type as TaskType); - - const isTaskGlossaryApproval = taskDetails?.type === TaskType.RequestApproval; + const getFormattedMenuOptions = (options: TaskAction[]) => { + return options.map((item) => ({ + ...item, + icon: , + })); + }; const handleTaskLinkClick = () => { history.push({ @@ -341,16 +406,22 @@ export const TaskTab = ({ (!hasGlossaryReviewer && isOwner) || (Boolean(isPartOfAssigneeTeam) && !isCreator); - const onSave = (message: string) => { - postFeed(message, taskThread?.id ?? '').catch(() => { - // ignore since error is displayed in toast in the parent promise. - // Added block for sonar code smell - }); + const onSave = () => { + postFeed(comment, taskThread?.id ?? '') + .catch(() => { + // ignore since error is displayed in toast in the parent promise. + // Added block for sonar code smell + }) + .finally(() => { + editorRef.current?.clearEditorValue(); + }); }; const handleMenuItemClick: MenuProps['onClick'] = (info) => { if (info.key === TaskActionMode.EDIT) { setShowEditTaskModel(true); + } else if (info.key === TaskActionMode.CLOSE) { + onTaskReject(); } else { onTaskResolve(); } @@ -458,7 +529,7 @@ export const TaskTab = ({ } }; - const onTaskDropdownClick = () => { + const onTestCaseTaskDropdownClick = () => { if (taskAction.key === TaskActionMode.RESOLVE) { setShowEditTaskModel(true); } else { @@ -466,13 +537,54 @@ export const TaskTab = ({ } }; + const handleGlossaryTaskMenuClick = (info: MenuInfo) => { + setTaskAction( + GLOSSARY_TASK_ACTION_LIST.find((action) => action.key === info.key) ?? + GLOSSARY_TASK_ACTION_LIST[0] + ); + switch (info.key) { + case TaskActionMode.RESOLVE: + onTaskResolve(); + + break; + + case TaskActionMode.CLOSE: + onTaskReject(); + + break; + } + }; + + const onTaskDropdownClick = () => { + if ( + taskAction.key === TaskActionMode.RESOLVE || + taskAction.key === TaskActionMode.EDIT + ) { + handleMenuItemClick({ key: taskAction.key } as MenuInfo); + } else { + onTaskReject(); + } + }; + + const renderCommentButton = useMemo(() => { + return ( + + ); + }, [comment, onSave]); + const approvalWorkflowActions = useMemo(() => { const hasApprovalAccess = isAssignee || (Boolean(isPartOfAssigneeTeam) && !isCreator); return ( - + icon={} + menu={{ + items: getFormattedMenuOptions(GLOSSARY_TASK_ACTION_LIST), + selectable: true, + selectedKeys: [taskAction.key], + onClick: handleGlossaryTaskMenuClick, + }} + overlayClassName="task-action-dropdown" + onClick={onTaskDropdownClick}> + {taskAction.label} + - - - + {renderCommentButton} ); - }, [taskDetails, onTaskResolve, isAssignee, isPartOfAssigneeTeam]); + }, [ + taskAction, + isAssignee, + isCreator, + isPartOfAssigneeTeam, + renderCommentButton, + handleGlossaryTaskMenuClick, + onTaskDropdownClick, + ]); const testCaseResultFlow = useMemo(() => { const editPermission = checkPermission( @@ -516,32 +632,34 @@ export const TaskTab = ({ const hasApprovalAccess = isAssignee || isCreator || editPermission; return ( - } - loading={isActionLoading} - menu={{ - items: INCIDENT_TASK_ACTION_LIST, - selectable: true, - selectedKeys: [taskAction.key], - onClick: handleTaskMenuClick, - disabled: !hasApprovalAccess, - }} - type="primary" - onClick={onTaskDropdownClick}> - {taskAction.label} - +
+ } + loading={isActionLoading} + menu={{ + items: INCIDENT_TASK_ACTION_LIST, + selectable: true, + selectedKeys: [taskAction.key], + onClick: handleTaskMenuClick, + disabled: !hasApprovalAccess, + }} + onClick={onTestCaseTaskDropdownClick}> + {taskAction.label} + + {renderCommentButton} +
); - }, [taskDetails, isAssignee, isPartOfAssigneeTeam, taskAction]); + }, [ + taskDetails, + isAssignee, + isPartOfAssigneeTeam, + taskAction, + renderCommentButton, + ]); const actionButtons = useMemo(() => { - if (isTaskClosed) { - return null; - } - - const taskType = taskDetails?.type ?? ''; - if (isTaskGlossaryApproval) { return approvalWorkflowActions; } @@ -550,49 +668,47 @@ export const TaskTab = ({ return testCaseResultFlow; } - const parsedSuggestion = [ - 'RequestDescription', - 'UpdateDescription', - ].includes(taskType) - ? taskDetails?.suggestion - : JSON.parse(taskDetails?.suggestion || '[]'); - return ( - {(isCreator || hasEditAccess) && ( - + {isCreator && !hasEditAccess && ( + )} - {hasEditAccess ? ( + {hasEditAccess && ( <> - {['RequestDescription', 'RequestTag'].includes(taskType) && - isEmpty(parsedSuggestion) ? ( - + {showAddSuggestionButton ? ( +
+ } + menu={{ + items: getFormattedMenuOptions(noSuggestionTaskMenuOptions), + selectable: true, + selectedKeys: [taskAction.key], + onClick: handleMenuItemClick, + }} + overlayClassName="task-action-dropdown" + onClick={onTaskDropdownClick}> + {taskAction.label} + +
) : ( } menu={{ - items: TASK_ACTION_LIST, + items: getFormattedMenuOptions(TASK_ACTION_LIST), selectable: true, selectedKeys: [taskAction.key], onClick: handleMenuItemClick, }} - type="primary" + overlayClassName="task-action-dropdown" onClick={() => taskAction.key === TaskActionMode.EDIT ? handleMenuItemClick({ key: taskAction.key } as MenuInfo) @@ -602,22 +718,24 @@ export const TaskTab = ({ )} - ) : ( - <> )} + {renderCommentButton}
); }, [ + onTaskReject, taskDetails, onTaskResolve, handleMenuItemClick, taskAction, isTaskClosed, isTaskGlossaryApproval, + showAddSuggestionButton, isCreator, approvalWorkflowActions, testCaseResultFlow, isTaskTestCaseResult, + renderCommentButton, ]); const initialFormValue = useMemo(() => { @@ -806,10 +924,13 @@ export const TaskTab = ({ {taskDetails?.status === ThreadTaskStatus.Open && ( - + )} - - {actionButtons} {isTaskTestCaseResult ? (