From 07d882ee50d3f37cc6f580512bc3bc573d858f9c Mon Sep 17 00:00:00 2001 From: Sachin Chaurasiya Date: Sat, 25 Jun 2022 22:07:10 +0530 Subject: [PATCH] =?UTF-8?q?Revamp=20=E2=9C=A8=20:=20Request/update=20descr?= =?UTF-8?q?iption=20should=20be=20created=20as=20a=20task=20=20(#5637)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Revamp ✨ : Request/update description should be created as a task #5321 * Fix Feed header issue * Add support for profile picture for task assignees * Add support for title input on request/update task * Update component based on task type * Update editor height * Fix unit test * Minor fixes --- .../FeedCardHeader/FeedCardHeader.tsx | 110 ++++++--- .../ActivityFeedList/FeedListBody.tsx | 27 ++- .../ActivityThreadList.tsx | 25 ++- .../ActivityThreadPanel.interface.ts | 1 + .../ActivityThreadPanel.test.tsx | 14 +- .../ActivityThreadPanel.tsx | 2 + .../ActivityThreadPanelBody.tsx | 37 +++- .../ActivityFeed/Shared/TaskBadge.tsx | 9 +- .../components/common/Ellipses/Ellipses.tsx | 12 +- .../common/PopOverCard/UserPopOverCard.tsx | 36 ++- .../RichTextEditor.interface.ts | 1 + .../rich-text-editor/RichTextEditor.tsx | 7 +- .../resources/ui/src/hooks/useAfterMount.ts | 8 +- .../RequestDescriptionPage.tsx | 41 +++- .../TaskDetailPage/TaskDetailPage.tsx | 209 +++++++++++------- .../ui/src/pages/TasksPage/TaskPage.styles.ts | 4 +- .../UpdateDescriptionPage.tsx | 25 ++- .../TasksPage/shared/DescriptionTabs.tsx | 46 +--- .../src/pages/TasksPage/shared/DiffView.tsx | 62 ++++++ .../pages/TasksPage/shared/TaskPageLayout.tsx | 2 +- .../main/resources/ui/src/styles/x-master.css | 9 +- .../main/resources/ui/src/utils/TasksUtils.ts | 4 +- 22 files changed, 480 insertions(+), 211 deletions(-) create mode 100644 openmetadata-ui/src/main/resources/ui/src/pages/TasksPage/shared/DiffView.tsx diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedCard/FeedCardHeader/FeedCardHeader.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedCard/FeedCardHeader/FeedCardHeader.tsx index 7d5ba008d9a..502634f3a44 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedCard/FeedCardHeader/FeedCardHeader.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedCard/FeedCardHeader/FeedCardHeader.tsx @@ -14,9 +14,10 @@ import classNames from 'classnames'; import { isUndefined, toString } from 'lodash'; import React, { FC, Fragment } from 'react'; -import { Link } from 'react-router-dom'; +import { Link, useHistory } from 'react-router-dom'; import AppState from '../../../../AppState'; import { FQN_SEPARATOR_CHAR } from '../../../../constants/char.constants'; +import { getUserPath } from '../../../../constants/constants'; import { EntityType, FqnPart, @@ -34,6 +35,7 @@ import { import { getEntityLink } from '../../../../utils/TableUtils'; import { getTaskDetailPath } from '../../../../utils/TasksUtils'; import { getDayTimeByTimeStamp } from '../../../../utils/TimeUtils'; +import Ellipses from '../../../common/Ellipses/Ellipses'; import EntityPopOverCard from '../../../common/PopOverCard/EntityPopOverCard'; import UserPopOverCard from '../../../common/PopOverCard/UserPopOverCard'; import { FeedHeaderProp } from '../ActivityFeedCard.interface'; @@ -50,6 +52,10 @@ const FeedCardHeader: FC = ({ feedType, taskDetails, }) => { + const history = useHistory(); + const onTitleClickHandler = (name: string) => { + history.push(getUserPath(name)); + }; const entityDisplayName = () => { let displayName; if (entityType === EntityType.TABLE) { @@ -110,43 +116,77 @@ const FeedCardHeader: FC = ({ const getFeedLinkElement = () => { if (!isUndefined(entityFQN) && !isUndefined(entityType)) { return ( - - {getFeedAction(feedType)}{' '} + + + {getFeedAction(feedType)}{' '} + {isEntityFeed ? ( + + {getEntityFieldDisplay(entityField)} + + ) : ( + + {entityType} + + + + + )} + + + ); + } else { + return null; + } + }; + + const getTaskLinkElement = () => { + if (!isUndefined(entityFQN) && !isUndefined(entityType)) { + return ( +
+ created a task + + + + for {isEntityFeed ? ( {getEntityFieldDisplay(entityField)} ) : ( - - {feedType === ThreadType.Conversation ? ( - - {entityType} - - - - - ) : ( - - - )} - + + )} - +
); } else { return null; @@ -157,10 +197,16 @@ const FeedCardHeader: FC = ({
- {createdBy} + onTitleClickHandler(createdBy)}> + {createdBy} + - {getFeedLinkElement()} + {feedType === ThreadType.Conversation + ? getFeedLinkElement() + : getTaskLinkElement()} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedList/FeedListBody.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedList/FeedListBody.tsx index 038ed034c9a..c33d7236e68 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedList/FeedListBody.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedList/FeedListBody.tsx @@ -12,9 +12,12 @@ */ import { Card } from 'antd'; +import { uniqueId } from 'lodash'; import React, { FC, Fragment } from 'react'; import { Post, ThreadType } from '../../../generated/entity/feed/thread'; import { getEntityName } from '../../../utils/CommonUtils'; +import UserPopOverCard from '../../common/PopOverCard/UserPopOverCard'; +import ProfilePicture from '../../common/ProfilePicture/ProfilePicture'; import { leftPanelAntCardStyle } from '../../containers/PageLayout'; import ActivityFeedCard from '../ActivityFeedCard/ActivityFeedCard'; import FeedCardFooter from '../ActivityFeedCard/FeedCardFooter/FeedCardFooter'; @@ -150,12 +153,26 @@ const FeedListBody: FC = ({ ) : null}
{feed.task && ( -
+
Assignees: - - {feed.task.assignees - .map((assignee) => getEntityName(assignee)) - .join(', ')} + + {feed.task.assignees.map((assignee) => ( + + + + + {getEntityName(assignee)} + + + + ))}
)} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityThreadPanel/ActivityThreadList.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityThreadPanel/ActivityThreadList.tsx index 029c527d61e..577285ba4f9 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityThreadPanel/ActivityThreadList.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityThreadPanel/ActivityThreadList.tsx @@ -11,10 +11,13 @@ * limitations under the License. */ import { Card } from 'antd'; +import { uniqueId } from 'lodash'; import React, { FC, Fragment } from 'react'; import { Post, ThreadType } from '../../../generated/entity/feed/thread'; import { getEntityName } from '../../../utils/CommonUtils'; import { getFeedListWithRelativeDays } from '../../../utils/FeedUtils'; +import UserPopOverCard from '../../common/PopOverCard/UserPopOverCard'; +import ProfilePicture from '../../common/ProfilePicture/ProfilePicture'; import { leftPanelAntCardStyle } from '../../containers/PageLayout'; import ActivityFeedCard from '../ActivityFeedCard/ActivityFeedCard'; import FeedCardFooter from '../ActivityFeedCard/FeedCardFooter/FeedCardFooter'; @@ -139,10 +142,24 @@ const ActivityThreadList: FC = ({ Assignees:{' '} - - {thread.task.assignees - .map((assignee) => getEntityName(assignee)) - .join(', ')} + + {thread.task.assignees.map((assignee) => ( + + + + + {getEntityName(assignee)} + + + + ))}
)} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityThreadPanel/ActivityThreadPanel.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityThreadPanel/ActivityThreadPanel.interface.ts index 53404612b45..b539f97cf85 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityThreadPanel/ActivityThreadPanel.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityThreadPanel/ActivityThreadPanel.interface.ts @@ -41,6 +41,7 @@ export interface ActivityThreadPanelBodyProp > { threadType: ThreadType; showHeader?: boolean; + onTabChange?: (key: string) => void; } export interface ActivityThreadListProp diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityThreadPanel/ActivityThreadPanel.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityThreadPanel/ActivityThreadPanel.test.tsx index 6bfd1eec03f..f6a412bc905 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityThreadPanel/ActivityThreadPanel.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityThreadPanel/ActivityThreadPanel.test.tsx @@ -11,7 +11,12 @@ * limitations under the License. */ -import { findByTestId, findByText, render } from '@testing-library/react'; +import { + findAllByText, + findByTestId, + findByText, + render, +} from '@testing-library/react'; import React from 'react'; import ReactDOM from 'react-dom'; import { MemoryRouter } from 'react-router-dom'; @@ -73,10 +78,13 @@ describe('Test ActivityThreadPanel Component', () => { ); const panelOverlay = await findByText(container, /FeedPanelOverlay/i); - const panelThreadList = await findByText(container, /ActivityThreadList/i); + const panelThreadList = await findAllByText( + container, + /ActivityThreadList/i + ); expect(panelOverlay).toBeInTheDocument(); - expect(panelThreadList).toBeInTheDocument(); + expect(panelThreadList).toHaveLength(2); }); it('Should create an observer if IntersectionObserver is available', async () => { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityThreadPanel/ActivityThreadPanel.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityThreadPanel/ActivityThreadPanel.tsx index 8fac73d9213..c20bc2d2091 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityThreadPanel/ActivityThreadPanel.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityThreadPanel/ActivityThreadPanel.tsx @@ -69,6 +69,7 @@ const ActivityThreadPanel: FC = ({ threadType={ThreadType.Task} updateThreadHandler={updateThreadHandler} onCancel={onCancel} + onTabChange={onTabChange} /> @@ -80,6 +81,7 @@ const ActivityThreadPanel: FC = ({ threadType={ThreadType.Conversation} updateThreadHandler={updateThreadHandler} onCancel={onCancel} + onTabChange={onTabChange} /> diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityThreadPanel/ActivityThreadPanelBody.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityThreadPanel/ActivityThreadPanelBody.tsx index d51be575956..1c22a1c2f9f 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityThreadPanel/ActivityThreadPanelBody.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityThreadPanel/ActivityThreadPanelBody.tsx @@ -14,7 +14,7 @@ import { AxiosError, AxiosResponse } from 'axios'; import classNames from 'classnames'; import { Operation } from 'fast-json-patch'; -import { isUndefined } from 'lodash'; +import { isEqual, isUndefined } from 'lodash'; import React, { FC, Fragment, RefObject, useEffect, useState } from 'react'; import AppState from '../../../AppState'; import { getAllFeeds } from '../../../axiosAPIs/feedsAPI'; @@ -22,10 +22,12 @@ import { confirmStateInitialValue } from '../../../constants/feed.constants'; import { observerOptions } from '../../../constants/Mydata.constants'; import { Thread, ThreadType } from '../../../generated/entity/feed/thread'; import { Paging } from '../../../generated/type/paging'; +import { useAfterMount } from '../../../hooks/useAfterMount'; import { useInfiniteScroll } from '../../../hooks/useInfiniteScroll'; import jsonData from '../../../jsons/en'; import { getEntityField } from '../../../utils/FeedUtils'; import { showErrorToast } from '../../../utils/ToastUtils'; +import ErrorPlaceHolder from '../../common/error-with-placeholder/ErrorPlaceHolder'; import Loader from '../../Loader/Loader'; import { ConfirmState } from '../ActivityFeedCard/ActivityFeedCard.interface'; import ActivityFeedEditor from '../ActivityFeedEditor/ActivityFeedEditor'; @@ -45,6 +47,7 @@ const ActivityThreadPanelBody: FC = ({ className, showHeader = true, threadType, + onTabChange, }) => { const [threads, setThreads] = useState([]); const [selectedThread, setSelectedThread] = useState(); @@ -195,6 +198,12 @@ const ActivityThreadPanelBody: FC = ({ fetchMoreThread(isInView as boolean, paging, isThreadLoading); }, [paging, isThreadLoading, isInView]); + useAfterMount(() => { + if (threadType === ThreadType.Task && !isThreadLoading) { + isEqual(threads.length, 0) && onTabChange && onTabChange('2'); + } + }, [threads, isThreadLoading]); + return (
@@ -233,15 +242,23 @@ const ActivityThreadPanelBody: FC = ({ {showNewConversation || threads.length === 0 ? (
-

- You are starting a new conversation -

- + + {threadType === ThreadType.Conversation ? ( + +

+ You are starting a new conversation +

+ +
+ ) : ( + No tasks yet + )} +
) : null} { return ( + className="tw-rounded tw-px-2 tw-absolute tw-left-4 tw--top-3" + style={{ + color: '#485056', + background: '#EBF5FF', + boxShadow: '0px 1px 2px rgba(0, 0, 0, 0.06)', + borderRadius: '4px', + }}> Task ); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/Ellipses/Ellipses.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/Ellipses/Ellipses.tsx index 34645079b6c..96ce4a74865 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/Ellipses/Ellipses.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/Ellipses/Ellipses.tsx @@ -13,17 +13,21 @@ import { Typography } from 'antd'; import { EllipsisConfig } from 'antd/lib/typography/Base'; -import React, { FC } from 'react'; +import classNames from 'classnames'; +import React, { CSSProperties, FC } from 'react'; interface Props extends EllipsisConfig { children: React.ReactNode; + className?: string; + style?: CSSProperties; } -const Ellipses: FC = ({ children, ...props }) => { +const Ellipses: FC = ({ children, className, style, ...props }) => { return ( + className={classNames('ant-typography-ellipsis-custom', className)} + ellipsis={props} + style={style}> {children} ); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/PopOverCard/UserPopOverCard.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/PopOverCard/UserPopOverCard.tsx index cf8284d03d3..f566f98756c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/PopOverCard/UserPopOverCard.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/PopOverCard/UserPopOverCard.tsx @@ -13,6 +13,7 @@ import { Popover } from 'antd'; import { AxiosError, AxiosResponse } from 'axios'; +import { isEmpty } from 'lodash'; import React, { FC, Fragment, HTMLAttributes, useState } from 'react'; import { useHistory } from 'react-router-dom'; import { getUserByName } from '../../../axiosAPIs/userAPI'; @@ -27,22 +28,26 @@ import ProfilePicture from '../ProfilePicture/ProfilePicture'; interface Props extends HTMLAttributes { userName: string; + type?: string; } -const UserPopOverCard: FC = ({ children, userName }) => { +const UserPopOverCard: FC = ({ children, userName, type = 'user' }) => { const history = useHistory(); const [userData, setUserData] = useState({} as User); - const [isLoading, setIsLoading] = useState(true); + const [isLoading, setIsLoading] = useState(false); const getData = () => { - getUserByName(userName, 'profile,roles,teams,follows,owns') - .then((res: AxiosResponse) => { - setUserData(res.data); - }) - .catch((err: AxiosError) => { - showErrorToast(err); - }) - .finally(() => setIsLoading(false)); + if (type === 'user') { + setIsLoading(true); + getUserByName(userName, 'profile,roles,teams,follows,owns') + .then((res: AxiosResponse) => { + setUserData(res.data); + }) + .catch((err: AxiosError) => { + showErrorToast(err); + }) + .finally(() => setIsLoading(false)); + } }; const onTitleClickHandler = (path: string) => { @@ -117,6 +122,7 @@ const UserPopOverCard: FC = ({ children, userName }) => { {displayName !== name ? ( {name} ) : null} + {isEmpty(userData) && {userName}}
); @@ -129,8 +135,14 @@ const UserPopOverCard: FC = ({ children, userName }) => { ) : (
- - + {isEmpty(userData) ? ( + No data available + ) : ( + + + + + )}
)} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/rich-text-editor/RichTextEditor.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/common/rich-text-editor/RichTextEditor.interface.ts index b39fa455db9..15eb439b41e 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/rich-text-editor/RichTextEditor.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/rich-text-editor/RichTextEditor.interface.ts @@ -51,4 +51,5 @@ export interface RichTextEditorProp extends HTMLAttributes { useCommandShortcut?: boolean; readonly?: boolean; height?: string; + onTextChange?: (value: string) => void; } diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/rich-text-editor/RichTextEditor.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/rich-text-editor/RichTextEditor.tsx index f3ec28e2347..38595b2c397 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/rich-text-editor/RichTextEditor.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/rich-text-editor/RichTextEditor.tsx @@ -14,6 +14,7 @@ /* eslint-disable */ import { Editor, Viewer } from '@toast-ui/react-editor'; +import classNames from 'classnames'; import React, { createRef, forwardRef, @@ -37,6 +38,9 @@ const RichTextEditor = forwardRef( initialValue = '', readonly, height, + className, + style, + onTextChange, }: RichTextEditorProp, ref ) => { @@ -49,6 +53,7 @@ const RichTextEditor = forwardRef( ?.getInstance() .getMarkdown() as string; setEditorValue(value); + onTextChange && onTextChange(value); }; useImperativeHandle(ref, () => ({ @@ -62,7 +67,7 @@ const RichTextEditor = forwardRef( }, [initialValue]); return ( -
+
{readonly ? (
void) => { +export const useAfterMount = ( + fnCallback: () => void, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + deps?: ReadonlyArray +) => { const [isMounting, setIsMounting] = useState(true); useEffect(() => { if (!isMounting) { fnCallback(); } - }, [isMounting]); + }, [isMounting, ...(deps || [])]); useEffect(() => { setIsMounting(false); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/TasksPage/RequestDescriptionPage/RequestDescriptionPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/TasksPage/RequestDescriptionPage/RequestDescriptionPage.tsx index 400dea9acf3..5657dc6cc82 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/TasksPage/RequestDescriptionPage/RequestDescriptionPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/TasksPage/RequestDescriptionPage/RequestDescriptionPage.tsx @@ -11,12 +11,13 @@ * limitations under the License. */ -import { Button, Card } from 'antd'; +import { Button, Card, Input } from 'antd'; import { AxiosError, AxiosResponse } from 'axios'; import { capitalize, isNil } from 'lodash'; import { observer } from 'mobx-react'; import { EditorContentRef, EntityTags } from 'Models'; import React, { + ChangeEvent, useCallback, useEffect, useMemo, @@ -70,6 +71,8 @@ const RequestDescription = () => { const [entityData, setEntityData] = useState({} as EntityData); const [options, setOptions] = useState([]); const [assignees, setAssignees] = useState>([]); + const [title, setTitle] = useState(''); + const [suggestion, setSuggestion] = useState(''); const entityTier = useMemo(() => { const tierFQN = getTierTags(entityData.tags || [])?.tagFQN; @@ -128,11 +131,20 @@ const RequestDescription = () => { } }; + const onTitleChange = (e: ChangeEvent) => { + const { value: newValue } = e.target; + setTitle(newValue); + }; + + const onSuggestionChange = (value: string) => { + setSuggestion(value); + }; + const onCreateTask = () => { if (assignees.length) { const data: CreateThread = { from: currentUser?.name as string, - message, + message: title || message, about: getEntityFeedLink(entityType, entityFQN, getTaskAbout()), taskDetails: { assignees: assignees.map((assignee) => ({ @@ -176,6 +188,7 @@ const RequestDescription = () => { setAssignees(defaultAssignee); setOptions(defaultAssignee); } + setTitle(message); }, [entityData]); return ( @@ -191,9 +204,19 @@ const RequestDescription = () => { className="tw-col-span-2" key="request-description" style={{ ...cardStyles }} - title={`Task: ${message}`}> + title="Create Task"> +
+ Title:{' '} + +
+
- Assignees:{' '} + Assignees:{' '} {

- Description:{' '} - - description below will be suggested to the assignees - + Suggest description:{' '}

@@ -223,7 +246,7 @@ const RequestDescription = () => { className="ant-btn-primary-custom" type="primary" onClick={onCreateTask}> - Submit + {suggestion ? 'Suggest' : 'Submit'}
diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/TasksPage/TaskDetailPage/TaskDetailPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/TasksPage/TaskDetailPage/TaskDetailPage.tsx index 2addde51534..93c7ac9438f 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/TasksPage/TaskDetailPage/TaskDetailPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/TasksPage/TaskDetailPage/TaskDetailPage.tsx @@ -16,7 +16,7 @@ import { Button, Card, Layout, Tabs } from 'antd'; import { AxiosError, AxiosResponse } from 'axios'; import classNames from 'classnames'; import { compare, Operation } from 'fast-json-patch'; -import { isEmpty, isEqual, isUndefined, toLower } from 'lodash'; +import { isEmpty, isEqual, isUndefined, toLower, uniqueId } from 'lodash'; import { observer } from 'mobx-react'; import { EditorContentRef, EntityTags } from 'Models'; import React, { Fragment, useEffect, useMemo, useRef, useState } from 'react'; @@ -39,7 +39,7 @@ import Ellipses from '../../../components/common/Ellipses/Ellipses'; import ErrorPlaceHolder from '../../../components/common/error-with-placeholder/ErrorPlaceHolder'; import UserPopOverCard from '../../../components/common/PopOverCard/UserPopOverCard'; import ProfilePicture from '../../../components/common/ProfilePicture/ProfilePicture'; -import RichTextEditorPreviewer from '../../../components/common/rich-text-editor/RichTextEditorPreviewer'; +import RichTextEditor from '../../../components/common/rich-text-editor/RichTextEditor'; import TitleBreadcrumb from '../../../components/common/title-breadcrumb/title-breadcrumb.component'; import { FQN_SEPARATOR_CHAR } from '../../../constants/char.constants'; import { TaskOperation } from '../../../constants/feed.constants'; @@ -47,6 +47,7 @@ import { EntityType } from '../../../enums/entity.enum'; import { CreateThread } from '../../../generated/api/feed/createThread'; import { Column } from '../../../generated/entity/data/table'; import { + TaskType, Thread, ThreadTaskStatus, ThreadType, @@ -61,6 +62,7 @@ import { getEntityType, updateThreadData, } from '../../../utils/FeedUtils'; +import { getEncodedFqn } from '../../../utils/StringsUtils'; import SVGIcons from '../../../utils/SvgUtils'; import { getEntityLink, @@ -72,11 +74,13 @@ import { fetchOptions, getBreadCrumbList, getColumnObject, + getDescriptionDiff, } from '../../../utils/TasksUtils'; import { getDayTimeByTimeStamp } from '../../../utils/TimeUtils'; import { showErrorToast, showSuccessToast } from '../../../utils/ToastUtils'; import Assignees from '../shared/Assignees'; import { DescriptionTabs } from '../shared/DescriptionTabs'; +import { DiffView } from '../shared/DiffView'; import { background, cardStyles, contentStyles } from '../TaskPage.styles'; import { EntityData, Option } from '../TasksPage.interface'; @@ -136,7 +140,7 @@ const TaskDetailPage = () => { }, [taskDetail, entityData]); const currentDescription = () => { - if (entityField) { + if (entityField && !isEmpty(columnObject)) { return columnObject.description || ''; } else { return entityData.description || ''; @@ -239,22 +243,25 @@ const TaskDetailPage = () => { }; const onTaskResolve = () => { - const description = - markdownRef.current?.getEditorContent() || taskDetail.task?.suggestion; + const description = markdownRef.current?.getEditorContent(); - updateTask(TaskOperation.RESOLVE, taskDetail.task?.id, { - newValue: description, - }) - .then(() => { - showSuccessToast('Task Resolved Successfully'); - history.push( - getEntityLink( - entityType as string, - entityData?.fullyQualifiedName as string - ) - ); - }) - .catch((err: AxiosError) => showErrorToast(err)); + const data = { newValue: description }; + + if (description) { + updateTask(TaskOperation.RESOLVE, taskDetail.task?.id, data) + .then(() => { + showSuccessToast('Task Resolved Successfully'); + history.push( + getEntityLink( + entityType as string, + entityData?.fullyQualifiedName as string + ) + ); + }) + .catch((err: AxiosError) => showErrorToast(err)); + } else { + showErrorToast('Cannot accept empty description'); + } }; const onTaskReject = () => { @@ -329,7 +336,7 @@ const TaskDetailPage = () => { entityFQN && fetchEntityDetail( entityType as EntityType, - entityFQN as string, + getEncodedFqn(entityFQN as string), setEntityData ); fetchTaskFeed(taskDetail.id); @@ -439,35 +446,36 @@ const TaskDetailPage = () => { }; const getCurrentDescription = () => { - let markdown; + let newDescription; + let oldDescription; if (taskDetail.task?.status === ThreadTaskStatus.Open) { - markdown = taskDetail.task.suggestion; + newDescription = taskDetail.task.suggestion; + oldDescription = !isEmpty(columnObject) + ? columnObject.description + : entityData.description; } else { - markdown = taskDetail.task?.newValue; + newDescription = taskDetail.task?.newValue; + oldDescription = taskDetail.task?.oldValue; } - return markdown ? ( - - ) : ( - No description + const diffs = getDescriptionDiff( + oldDescription || '', + newDescription || '' ); + + return ; }; + const isOwner = entityData.owner?.id === currentUser?.id; + const isAssignee = taskDetail.task?.assignees?.some( + (assignee) => assignee.id === currentUser?.id + ); + + const isTaskClosed = taskDetail.task?.status === ThreadTaskStatus.Closed; + const isCreator = taskDetail.createdBy === currentUser?.name; + const hasEditAccess = () => { - const isOwner = entityData.owner?.id === currentUser?.id; - const isAssignee = taskDetail.task?.assignees?.some( - (assignee) => assignee.id === currentUser?.id - ); - - const isTaskClosed = taskDetail.task?.status === ThreadTaskStatus.Closed; - - return ( - (isAdminUser || isAuthDisabled || isAssignee || isOwner) && !isTaskClosed - ); + return isAdminUser || isAuthDisabled || isAssignee || isOwner; }; return ( @@ -529,9 +537,7 @@ const TaskDetailPage = () => {
- - Assignees: - + Assignees: {editAssignee ? ( { ) : ( - - {taskDetail.task?.assignees - ?.map((assignee) => getEntityName(assignee)) - ?.join(', ')} + 2, + })}> + {taskDetail.task?.assignees?.map((assignee) => ( + + + + + {getEntityName(assignee)} + + + + ))} - {hasEditAccess() && ( + {(hasEditAccess() || isCreator) && !isTaskClosed && (
- Description:{' '} +

Description:

{' '} {!isEmpty(taskDetail) && ( - {showEdit ? ( - - ) : ( -
- {getCurrentDescription()} - {hasEditAccess() && ( - + {taskDetail.task?.type === TaskType.RequestDescription ? ( + + {taskDetail.task.status === ThreadTaskStatus.Open ? ( + + ) : ( + )} -
+
+ ) : ( + + {showEdit ? ( + + ) : ( +
+ {getCurrentDescription()} + {hasEditAccess() && !isTaskClosed && ( + + )} +
+ )} +
)} )}
- {hasEditAccess() && ( + {hasEditAccess() && !isTaskClosed && (
- + {showEdit && ( + + )}
)} diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/TasksPage/TaskPage.styles.ts b/openmetadata-ui/src/main/resources/ui/src/pages/TasksPage/TaskPage.styles.ts index 1defa7c1750..09d2ff37f9b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/TasksPage/TaskPage.styles.ts +++ b/openmetadata-ui/src/main/resources/ui/src/pages/TasksPage/TaskPage.styles.ts @@ -15,7 +15,7 @@ export const cardStyles = { border: '1px rgb(221, 227, 234) solid', borderRadius: '8px', marginBottom: '20px', - boxShadow: '1px 1px 6px rgb(0 0 0 / 12%)', + boxShadow: '1px 1px 6px rgb(0 0 0 / 6%)', marginTop: '8px', }; @@ -25,4 +25,4 @@ export const contentStyles = { paddingTop: 0, }; -export const background = { background: '#ffffff' }; +export const background = { background: '#F8F9FA' }; diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/TasksPage/UpdateDescriptionPage/UpdateDescriptionPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/TasksPage/UpdateDescriptionPage/UpdateDescriptionPage.tsx index 114ebaab57a..f90252958de 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/TasksPage/UpdateDescriptionPage/UpdateDescriptionPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/TasksPage/UpdateDescriptionPage/UpdateDescriptionPage.tsx @@ -11,11 +11,12 @@ * limitations under the License. */ -import { Button, Card } from 'antd'; +import { Button, Card, Input } from 'antd'; import { AxiosError, AxiosResponse } from 'axios'; import { capitalize, isEmpty, isNil, isUndefined } from 'lodash'; import { EditorContentRef, EntityTags } from 'Models'; import React, { + ChangeEvent, useCallback, useEffect, useMemo, @@ -70,6 +71,7 @@ const UpdateDescription = () => { const [options, setOptions] = useState([]); const [assignees, setAssignees] = useState>([]); const [currentDescription, setCurrentDescription] = useState(''); + const [title, setTitle] = useState(''); const entityTier = useMemo(() => { const tierFQN = getTierTags(entityData.tags || [])?.tagFQN; @@ -138,11 +140,16 @@ const UpdateDescription = () => { } }; + const onTitleChange = (e: ChangeEvent) => { + const { value: newValue } = e.target; + setTitle(newValue); + }; + const onCreateTask = () => { if (assignees.length) { const data: CreateThread = { from: currentUser?.name as string, - message, + message: title || message, about: getEntityFeedLink(entityType, entityFQN, getTaskAbout()), taskDetails: { assignees: assignees.map((assignee) => ({ @@ -186,6 +193,7 @@ const UpdateDescription = () => { setAssignees(defaultAssignee); setOptions(defaultAssignee); } + setTitle(message); }, [entityData]); useEffect(() => { @@ -205,9 +213,18 @@ const UpdateDescription = () => { className="tw-col-span-2" key="update-description" style={{ ...cardStyles }} - title={`Task: ${message}`}> + title="Create Task"> +
+ Title:{' '} + +
- Assignees:{' '} + Assignees:{' '} { - const elements = diffArr.map((diff) => { - if (diff.added) { - return ( - - {diff.value} - - ); - } - if (diff.removed) { - return ( - - {diff.value} - - ); - } - - return
No diff available
; - }); - - return ( -
-
-          {diffArr.length ? (
-            elements
-          ) : (
-            No diff available
-          )}
-        
-
- ); - }; - return ( - + diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/TasksPage/shared/DiffView.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/TasksPage/shared/DiffView.tsx new file mode 100644 index 00000000000..5a97acf43b4 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/pages/TasksPage/shared/DiffView.tsx @@ -0,0 +1,62 @@ +/* + * Copyright 2021 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 classNames from 'classnames'; +import { uniqueId } from 'lodash'; +import { Diff } from 'Models'; +import React from 'react'; + +export const DiffView = ({ + diffArr, + className, +}: { + diffArr: Diff[]; + className?: string; +}) => { + const elements = diffArr.map((diff) => { + if (diff.added) { + return ( + + {diff.value} + + ); + } + if (diff.removed) { + return ( + + {diff.value} + + ); + } + + return {diff.value}; + }); + + return ( +
+
+        {diffArr.length ? (
+          elements
+        ) : (
+          No diff available
+        )}
+      
+
+ ); +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/TasksPage/shared/TaskPageLayout.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/TasksPage/shared/TaskPageLayout.tsx index 801d185fd0b..d2d8c3bd67e 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/TasksPage/shared/TaskPageLayout.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/TasksPage/shared/TaskPageLayout.tsx @@ -22,7 +22,7 @@ const TaskPageLayout: FC = ({ children }) => { const { Content, Sider } = Layout; return ( - + {children} diff --git a/openmetadata-ui/src/main/resources/ui/src/styles/x-master.css b/openmetadata-ui/src/main/resources/ui/src/styles/x-master.css index b8b01b39c0e..f9a65e56dc4 100644 --- a/openmetadata-ui/src/main/resources/ui/src/styles/x-master.css +++ b/openmetadata-ui/src/main/resources/ui/src/styles/x-master.css @@ -1231,7 +1231,7 @@ code { .ant-layout-sider-task-detail { background: #ffffff; border: 1px solid #dde3ea; - box-shadow: -1px 3px 6px rgba(0, 0, 0, 0.16); + box-shadow: -1px 3px 6px rgba(0, 0, 0, 0.06); padding: 16px; padding-top: 0px; overflow-y: auto; @@ -1296,3 +1296,10 @@ code { div.ant-typography-ellipsis-custom { margin-bottom: 0px; } + +.ant-input:hover, +.ant-input:focus { + border-color: #7147e8; + border-right-width: 0px; + box-shadow: none; +} diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/TasksUtils.ts b/openmetadata-ui/src/main/resources/ui/src/utils/TasksUtils.ts index a04e1806058..6dcfc4f41a6 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/TasksUtils.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/TasksUtils.ts @@ -12,7 +12,7 @@ */ import { AxiosError, AxiosResponse } from 'axios'; -import { diffLines } from 'diff'; +import { diffWordsWithSpace } from 'diff'; import { isEqual, isUndefined } from 'lodash'; import { Diff } from 'Models'; import { getDashboardByFqn } from '../axiosAPIs/dashboardAPI'; @@ -92,7 +92,7 @@ export const getDescriptionDiff = ( oldValue: string, newValue: string ): Diff[] => { - return diffLines(oldValue, newValue, { ignoreWhitespace: false }); + return diffWordsWithSpace(oldValue, newValue); }; export const fetchOptions = (