From 4c30b01286ff2d0d6db2dceb53386d30d1b8f45d Mon Sep 17 00:00:00 2001 From: Sachin Chaurasiya Date: Fri, 15 Jul 2022 22:00:57 +0530 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20UI:=20Request/update=20tag=20should?= =?UTF-8?q?=20be=20created=20as=20a=20task=20(#6096)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ✨ UI: Request/update tag should be created as a task * Add support for tagsdiff in tagtask page * Minor change * Change text from suggest to update * Add support for update tags task at column level tags * Minor fix * Remove ternary operator --- .../ActivityThreadPanelBody.tsx | 5 +- .../EntityTable/EntityTable.component.tsx | 21 +- .../common/entityPageInfo/EntityPageInfo.tsx | 20 +- .../TaskDetailPage/TaskDetailPage.tsx | 12 + .../pages/TasksPage/TasksPage.interface.ts | 6 + .../TasksPage/UpdateTagPage/UpdateTagPage.tsx | 311 ++++++++++++++++++ .../pages/TasksPage/shared/TagSuggestion.tsx | 10 +- .../pages/TasksPage/shared/TagsDiffView.tsx | 77 +++++ .../src/pages/TasksPage/shared/TagsTabs.tsx | 66 ++++ .../src/pages/TasksPage/shared/TagsTask.tsx | 107 +++++- .../ui/src/router/AuthenticatedAppRouter.tsx | 5 + .../main/resources/ui/src/utils/TasksUtils.ts | 20 ++ 12 files changed, 631 insertions(+), 29 deletions(-) create mode 100644 openmetadata-ui/src/main/resources/ui/src/pages/TasksPage/UpdateTagPage/UpdateTagPage.tsx create mode 100644 openmetadata-ui/src/main/resources/ui/src/pages/TasksPage/shared/TagsDiffView.tsx create mode 100644 openmetadata-ui/src/main/resources/ui/src/pages/TasksPage/shared/TagsTabs.tsx 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 6df1f9f9758..d8409c5f386 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 @@ -223,7 +223,7 @@ const ActivityThreadPanelBody: FC = ({ return (
- {showHeader && isConversationType ? ( + {showHeader && isConversationType && ( = ({ : undefined } /> - ) : ( + )} + {isTaskType && (
Closed Tasks diff --git a/openmetadata-ui/src/main/resources/ui/src/components/EntityTable/EntityTable.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/EntityTable/EntityTable.component.tsx index 46a77547ca0..cd2590c688f 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/EntityTable/EntityTable.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/EntityTable/EntityTable.component.tsx @@ -59,6 +59,7 @@ import { getRequestDescriptionPath, getRequestTagsPath, getUpdateDescriptionPath, + getUpdateTagsPath, } from '../../utils/TasksUtils'; import NonAdminAction from '../common/non-admin-action/NonAdminAction'; import PopOver from '../common/popover/PopOver'; @@ -374,6 +375,15 @@ const EntityTable = ({ ); }; + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ + const onUpdateTagsHandler = (cell: any) => { + const field = EntityField.COLUMNS; + const value = getColumnName(cell); + history.push( + getUpdateTagsPath(EntityType.TABLE, entityFqn as string, field, value) + ); + }; + const prepareConstraintIcon = ( columnName: string, columnConstraint?: string @@ -432,22 +442,25 @@ const EntityTable = ({ /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ const getRequestTagsElement = (cell: any) => { const hasTags = !isEmpty(cell.value || []); + const text = hasTags ? 'Update request tags' : 'Request tags'; - return !hasTags ? ( + return ( - ) : null; + ); }; useEffect(() => { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/entityPageInfo/EntityPageInfo.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/entityPageInfo/EntityPageInfo.tsx index d86093a3bc1..91b9f9e12f8 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/entityPageInfo/EntityPageInfo.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/entityPageInfo/EntityPageInfo.tsx @@ -36,7 +36,11 @@ import { } from '../../../utils/GlossaryUtils'; import SVGIcons, { Icons } from '../../../utils/SvgUtils'; import { getTagCategories, getTaglist } from '../../../utils/TagsUtils'; -import { getRequestTagsPath, TASK_ENTITIES } from '../../../utils/TasksUtils'; +import { + getRequestTagsPath, + getUpdateTagsPath, + TASK_ENTITIES, +} from '../../../utils/TasksUtils'; import TagsContainer from '../../tags-container/tags-container'; import TagsViewer from '../../tags-viewer/tags-viewer'; import Tags from '../../tags/tags'; @@ -113,6 +117,9 @@ const EntityPageInfo = ({ const handleRequestTags = () => { history.push(getRequestTagsPath(entityType as string, entityFqn as string)); }; + const handleUpdateTags = () => { + history.push(getUpdateTagsPath(entityType as string, entityFqn as string)); + }; const handleTagSelection = (selectedTags?: Array) => { if (selectedTags) { @@ -314,15 +321,16 @@ const EntityPageInfo = ({ const getRequestTagsElements = useCallback(() => { const hasTags = !isEmpty(tags); + const text = hasTags ? 'Update request tags' : 'Request tags'; - return onThreadLinkSelect && !hasTags ? ( + return onThreadLinkSelect ? ( 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 426fa74df8b..93e02166368 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 @@ -396,6 +396,15 @@ const TaskDetailPage = () => { } }; + // prepare current tags for update tags task + const getCurrentTags = () => { + if (!isEmpty(columnObject) && entityField) { + return columnObject.tags ?? []; + } else { + return entityData.tags ?? []; + } + }; + // handle assignees search const onSearch = (query: string) => { fetchOptions(query, setOptions); @@ -624,9 +633,12 @@ const TaskDetailPage = () => { {isTaskTags && ( )} {hasEditAccess() && !isTaskClosed && ( diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/TasksPage/TasksPage.interface.ts b/openmetadata-ui/src/main/resources/ui/src/pages/TasksPage/TasksPage.interface.ts index 30cfc171316..62a38e04ebf 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/TasksPage/TasksPage.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/pages/TasksPage/TasksPage.interface.ts @@ -37,3 +37,9 @@ export enum TaskActionMode { VIEW = 'view', EDIT = 'edit', } + +export enum TaskTabs { + CURRENT = 'current', + DIFF = 'diff', + NEW = 'new', +} diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/TasksPage/UpdateTagPage/UpdateTagPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/TasksPage/UpdateTagPage/UpdateTagPage.tsx new file mode 100644 index 00000000000..05eaece7108 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/pages/TasksPage/UpdateTagPage/UpdateTagPage.tsx @@ -0,0 +1,311 @@ +/* + * 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 { Button, Card, Input } from 'antd'; +import { AxiosError, AxiosResponse } from 'axios'; +import { capitalize, isEmpty, isNil, isUndefined } from 'lodash'; +import { observer } from 'mobx-react'; +import { EntityTags } from 'Models'; +import React, { + ChangeEvent, + Fragment, + useCallback, + useEffect, + useMemo, + useState, +} from 'react'; +import { useHistory, useLocation, useParams } from 'react-router-dom'; +import AppState from '../../../AppState'; +import { postThread } from '../../../axiosAPIs/feedsAPI'; +import ProfilePicture from '../../../components/common/ProfilePicture/ProfilePicture'; +import TitleBreadcrumb from '../../../components/common/title-breadcrumb/title-breadcrumb.component'; +import { FQN_SEPARATOR_CHAR } from '../../../constants/char.constants'; +import { EntityField } from '../../../constants/feed.constants'; +import { EntityType } from '../../../enums/entity.enum'; +import { + CreateThread, + TaskType, +} from '../../../generated/api/feed/createThread'; +import { ThreadType } from '../../../generated/entity/feed/thread'; +import { TagLabel } from '../../../generated/type/tagLabel'; +import { getEntityName } from '../../../utils/CommonUtils'; +import { + ENTITY_LINK_SEPARATOR, + getEntityFeedLink, +} from '../../../utils/EntityUtils'; +import { getTagsWithoutTier, getTierTags } from '../../../utils/TableUtils'; +import { + fetchEntityDetail, + fetchOptions, + getBreadCrumbList, + getColumnObject, + getTaskDetailPath, +} from '../../../utils/TasksUtils'; +import { showErrorToast, showSuccessToast } from '../../../utils/ToastUtils'; +import Assignees from '../shared/Assignees'; +import { TagsTabs } from '../shared/TagsTabs'; +import TaskPageLayout from '../shared/TaskPageLayout'; +import { cardStyles } from '../TaskPage.styles'; +import { EntityData, Option } from '../TasksPage.interface'; + +const UpdateTag = () => { + const location = useLocation(); + const history = useHistory(); + + const { entityType, entityFQN } = useParams<{ [key: string]: string }>(); + const queryParams = new URLSearchParams(location.search); + + const field = queryParams.get('field'); + const value = queryParams.get('value'); + + const [entityData, setEntityData] = useState({} as EntityData); + const [options, setOptions] = useState([]); + const [assignees, setAssignees] = useState([]); + const [title, setTitle] = useState(''); + const [currentTags, setCurrentTags] = useState([]); + const [suggestion, setSuggestion] = useState([]); + + const entityTier = useMemo(() => { + const tierFQN = getTierTags(entityData.tags || [])?.tagFQN; + + return tierFQN?.split(FQN_SEPARATOR_CHAR)[1]; + }, [entityData.tags]); + + const entityTags = useMemo(() => { + const tags: EntityTags[] = getTagsWithoutTier(entityData.tags || []) || []; + + return tags.map((tag) => `#${tag.tagFQN}`).join(' '); + }, [entityData.tags]); + + const getSanitizeValue = value?.replaceAll(/^"|"$/g, '') || ''; + + const message = `Update tags for ${getSanitizeValue || entityType}`; + + // get current user details + const currentUser = useMemo( + () => AppState.getCurrentUserDetails(), + [AppState.userDetails, AppState.nonSecureUserDetails] + ); + + const back = () => history.goBack(); + + const columnObject = useMemo(() => { + const column = getSanitizeValue.split(FQN_SEPARATOR_CHAR).slice(-1); + + return getColumnObject(column[0], entityData.columns || []); + }, [field, entityData]); + + const getColumnDetails = useCallback(() => { + if (!isNil(field) && !isNil(value) && field === EntityField.COLUMNS) { + return ( +
+

Column Details

+

+ Type:{' '} + {columnObject.dataTypeDisplay} +

+

+ {columnObject?.tags + ?.map((tag: TagLabel) => `#${tag.tagFQN}`) + ?.join(' ')} +

+
+ ); + } else { + return null; + } + }, [entityData.columns]); + + const getTags = () => { + if (!isEmpty(columnObject) && !isUndefined(columnObject)) { + return columnObject.tags ?? []; + } else { + return entityData.tags ?? []; + } + }; + + const onSearch = (query: string) => { + fetchOptions(query, setOptions); + }; + + const getTaskAbout = () => { + if (field && value) { + return `${field}${ENTITY_LINK_SEPARATOR}${value}${ENTITY_LINK_SEPARATOR}tags`; + } else { + return EntityField.TAGS; + } + }; + + 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: title || message, + about: getEntityFeedLink(entityType, entityFQN, getTaskAbout()), + taskDetails: { + assignees: assignees.map((assignee) => ({ + id: assignee.value, + type: assignee.type, + })), + suggestion: JSON.stringify(suggestion), + type: TaskType.UpdateTag, + oldValue: JSON.stringify(currentTags), + }, + type: ThreadType.Task, + }; + postThread(data) + .then((res: AxiosResponse) => { + showSuccessToast('Task Created Successfully'); + history.push(getTaskDetailPath(res.data.task.id)); + }) + .catch((err: AxiosError) => showErrorToast(err)); + } else { + showErrorToast('Cannot create a task without assignee'); + } + }; + + useEffect(() => { + fetchEntityDetail( + entityType as EntityType, + entityFQN as string, + setEntityData + ); + }, [entityFQN, entityType]); + + useEffect(() => { + const owner = entityData.owner; + if (owner) { + const defaultAssignee = [ + { + label: getEntityName(owner), + value: owner.id || '', + type: owner.type, + }, + ]; + setAssignees(defaultAssignee); + setOptions(defaultAssignee); + } + setTitle(message); + }, [entityData]); + + useEffect(() => { + setCurrentTags(getTags()); + setSuggestion(getTags()); + }, [entityData, columnObject]); + + return ( + + +
+ +
+ Title:{' '} + +
+ +
+ Assignees:{' '} + +
+ + {currentTags.length ? ( + +

+ Update tags:{' '} +

+ +
+ ) : null} + +
+ + +
+
+ +
+
{capitalize(entityType)} Details
+
+ Owner:{' '} + + {entityData.owner ? ( + + + + {getEntityName(entityData.owner)} + + + ) : ( + No Owner + )} + +
+ +

+ {entityTier ? ( + entityTier + ) : ( + No Tier + )} +

+ +

{entityTags}

+ + {getColumnDetails()} +
+
+
+ ); +}; + +export default observer(UpdateTag); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/TasksPage/shared/TagSuggestion.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/TasksPage/shared/TagSuggestion.tsx index fef0e4cf6d3..9a9491035f9 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/TasksPage/shared/TagSuggestion.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/TasksPage/shared/TagSuggestion.tsx @@ -29,7 +29,7 @@ const { Option } = Select; interface SelectOption { label: string; value: string; - 'data-sourceType': string; + 'data-sourcetype': string; } interface Props { @@ -42,7 +42,7 @@ const TagSuggestion: React.FC = ({ onChange, selectedTags }) => { selectedTags.map((tag) => ({ label: tag.tagFQN, value: tag.tagFQN, - 'data-sourceType': isEqual(tag.source, 'Tag') ? 'tag' : 'glossaryTerm', + 'data-sourcetype': isEqual(tag.source, 'Tag') ? 'tag' : 'glossaryTerm', })); const [options, setOptions] = useState([]); @@ -63,7 +63,7 @@ const TagSuggestion: React.FC = ({ onChange, selectedTags }) => { uniqueOptions.map((op: any) => ({ label: op.fullyQualifiedName as string, value: op.fullyQualifiedName as string, - 'data-sourceType': op.entityType, + 'data-sourcetype': op.entityType, })) ); }) @@ -83,7 +83,7 @@ const TagSuggestion: React.FC = ({ onChange, selectedTags }) => { const newTags = (option as SelectOption[]).map((value) => ({ labelType: LabelType.Manual, state: State.Suggested, - source: isEqual(value['data-sourceType'], 'tag') + source: isEqual(value['data-sourcetype'], 'tag') ? TagSource.Tag : TagSource.Glossary, tagFQN: value.value, @@ -111,7 +111,7 @@ const TagSuggestion: React.FC = ({ onChange, selectedTags }) => { onSearch={handleSearch}> {options.map((d) => (
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 2ef8d3e05eb..b7fd93f9611 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/TasksUtils.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/TasksUtils.ts @@ -106,6 +106,26 @@ export const getUpdateDescriptionPath = ( return { pathname, search: searchParams.toString() }; }; +export const getUpdateTagsPath = ( + entityType: string, + entityFQN: string, + field?: string, + value?: string +) => { + let pathname = ROUTES.UPDATE_TAGS; + pathname = pathname + .replace(PLACEHOLDER_ROUTE_ENTITY_TYPE, entityType) + .replace(PLACEHOLDER_ROUTE_ENTITY_FQN, entityFQN); + const searchParams = new URLSearchParams(); + + if (!isUndefined(field) && !isUndefined(value)) { + searchParams.append('field', field); + searchParams.append('value', value); + } + + return { pathname, search: searchParams.toString() }; +}; + export const getTaskDetailPath = (taskId: string) => { const pathname = ROUTES.TASK_DETAIL.replace(PLACEHOLDER_TASK_ID, taskId);