mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-10-11 16:58:38 +00:00
✨ UI: Request/update tag should be created as a task (#6096)
* ✨ 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
This commit is contained in:
parent
ccdfef8f0e
commit
4c30b01286
@ -223,7 +223,7 @@ const ActivityThreadPanelBody: FC<ActivityThreadPanelBodyProp> = ({
|
|||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<div id="thread-panel-body">
|
<div id="thread-panel-body">
|
||||||
{showHeader && isConversationType ? (
|
{showHeader && isConversationType && (
|
||||||
<FeedPanelHeader
|
<FeedPanelHeader
|
||||||
className="tw-px-4 tw-shadow-sm"
|
className="tw-px-4 tw-shadow-sm"
|
||||||
entityField={entityField as string}
|
entityField={entityField as string}
|
||||||
@ -235,7 +235,8 @@ const ActivityThreadPanelBody: FC<ActivityThreadPanelBodyProp> = ({
|
|||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
) : (
|
)}
|
||||||
|
{isTaskType && (
|
||||||
<div className="tw-flex tw-justify-end tw-mr-2 tw-mt-2">
|
<div className="tw-flex tw-justify-end tw-mr-2 tw-mt-2">
|
||||||
<Switch onChange={onSwitchChange} />
|
<Switch onChange={onSwitchChange} />
|
||||||
<span className="tw-ml-1">Closed Tasks</span>
|
<span className="tw-ml-1">Closed Tasks</span>
|
||||||
|
@ -59,6 +59,7 @@ import {
|
|||||||
getRequestDescriptionPath,
|
getRequestDescriptionPath,
|
||||||
getRequestTagsPath,
|
getRequestTagsPath,
|
||||||
getUpdateDescriptionPath,
|
getUpdateDescriptionPath,
|
||||||
|
getUpdateTagsPath,
|
||||||
} from '../../utils/TasksUtils';
|
} from '../../utils/TasksUtils';
|
||||||
import NonAdminAction from '../common/non-admin-action/NonAdminAction';
|
import NonAdminAction from '../common/non-admin-action/NonAdminAction';
|
||||||
import PopOver from '../common/popover/PopOver';
|
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 = (
|
const prepareConstraintIcon = (
|
||||||
columnName: string,
|
columnName: string,
|
||||||
columnConstraint?: string
|
columnConstraint?: string
|
||||||
@ -432,22 +442,25 @@ const EntityTable = ({
|
|||||||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
||||||
const getRequestTagsElement = (cell: any) => {
|
const getRequestTagsElement = (cell: any) => {
|
||||||
const hasTags = !isEmpty(cell.value || []);
|
const hasTags = !isEmpty(cell.value || []);
|
||||||
|
const text = hasTags ? 'Update request tags' : 'Request tags';
|
||||||
|
|
||||||
return !hasTags ? (
|
return (
|
||||||
<button
|
<button
|
||||||
className="tw-w-8 tw-h-8 tw-mr-1 tw-flex-none link-text focus:tw-outline-none tw-opacity-0 group-hover:tw-opacity-100 tw-align-top"
|
className="tw-w-8 tw-h-8 tw-mr-1 tw-flex-none link-text focus:tw-outline-none tw-opacity-0 group-hover:tw-opacity-100 tw-align-top"
|
||||||
data-testid="request-tags"
|
data-testid="request-tags"
|
||||||
onClick={() => onRequestTagsHandler(cell)}>
|
onClick={() =>
|
||||||
|
hasTags ? onUpdateTagsHandler(cell) : onRequestTagsHandler(cell)
|
||||||
|
}>
|
||||||
<Popover
|
<Popover
|
||||||
destroyTooltipOnHide
|
destroyTooltipOnHide
|
||||||
content="Request tags"
|
content={text}
|
||||||
overlayClassName="ant-popover-request-description"
|
overlayClassName="ant-popover-request-description"
|
||||||
trigger="hover"
|
trigger="hover"
|
||||||
zIndex={9999}>
|
zIndex={9999}>
|
||||||
<SVGIcons alt="request-tags" icon={Icons.REQUEST} width="16px" />
|
<SVGIcons alt="request-tags" icon={Icons.REQUEST} width="16px" />
|
||||||
</Popover>
|
</Popover>
|
||||||
</button>
|
</button>
|
||||||
) : null;
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -36,7 +36,11 @@ import {
|
|||||||
} from '../../../utils/GlossaryUtils';
|
} from '../../../utils/GlossaryUtils';
|
||||||
import SVGIcons, { Icons } from '../../../utils/SvgUtils';
|
import SVGIcons, { Icons } from '../../../utils/SvgUtils';
|
||||||
import { getTagCategories, getTaglist } from '../../../utils/TagsUtils';
|
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 TagsContainer from '../../tags-container/tags-container';
|
||||||
import TagsViewer from '../../tags-viewer/tags-viewer';
|
import TagsViewer from '../../tags-viewer/tags-viewer';
|
||||||
import Tags from '../../tags/tags';
|
import Tags from '../../tags/tags';
|
||||||
@ -113,6 +117,9 @@ const EntityPageInfo = ({
|
|||||||
const handleRequestTags = () => {
|
const handleRequestTags = () => {
|
||||||
history.push(getRequestTagsPath(entityType as string, entityFqn as string));
|
history.push(getRequestTagsPath(entityType as string, entityFqn as string));
|
||||||
};
|
};
|
||||||
|
const handleUpdateTags = () => {
|
||||||
|
history.push(getUpdateTagsPath(entityType as string, entityFqn as string));
|
||||||
|
};
|
||||||
|
|
||||||
const handleTagSelection = (selectedTags?: Array<EntityTags>) => {
|
const handleTagSelection = (selectedTags?: Array<EntityTags>) => {
|
||||||
if (selectedTags) {
|
if (selectedTags) {
|
||||||
@ -314,15 +321,16 @@ const EntityPageInfo = ({
|
|||||||
|
|
||||||
const getRequestTagsElements = useCallback(() => {
|
const getRequestTagsElements = useCallback(() => {
|
||||||
const hasTags = !isEmpty(tags);
|
const hasTags = !isEmpty(tags);
|
||||||
|
const text = hasTags ? 'Update request tags' : 'Request tags';
|
||||||
|
|
||||||
return onThreadLinkSelect && !hasTags ? (
|
return onThreadLinkSelect ? (
|
||||||
<button
|
<button
|
||||||
className="tw-w-8 tw-h-8 tw-mr-1 tw-flex-none link-text focus:tw-outline-none tw-align-top"
|
className="tw-w-8 tw-h-8 tw-mr-1 tw-flex-none link-text focus:tw-outline-none tw-align-top"
|
||||||
data-testid="request-description"
|
data-testid="request-description"
|
||||||
onClick={handleRequestTags}>
|
onClick={hasTags ? handleUpdateTags : handleRequestTags}>
|
||||||
<Popover
|
<Popover
|
||||||
destroyTooltipOnHide
|
destroyTooltipOnHide
|
||||||
content="Request tags"
|
content={text}
|
||||||
overlayClassName="ant-popover-request-description"
|
overlayClassName="ant-popover-request-description"
|
||||||
trigger="hover"
|
trigger="hover"
|
||||||
zIndex={9999}>
|
zIndex={9999}>
|
||||||
@ -341,7 +349,7 @@ const EntityPageInfo = ({
|
|||||||
onThreadLinkSelect?.(tagTask.entityLink, ThreadType.Task)
|
onThreadLinkSelect?.(tagTask.entityLink, ThreadType.Task)
|
||||||
}>
|
}>
|
||||||
<span className="tw-flex">
|
<span className="tw-flex">
|
||||||
<SVGIcons alt="comments" icon={Icons.TASK_ICON} />
|
<SVGIcons alt="comments" icon={Icons.TASK_ICON} width="16px" />
|
||||||
<span className="tw-ml-1" data-testid="tag-task-count">
|
<span className="tw-ml-1" data-testid="tag-task-count">
|
||||||
{tagTask.count}
|
{tagTask.count}
|
||||||
</span>
|
</span>
|
||||||
@ -531,7 +539,7 @@ const EntityPageInfo = ({
|
|||||||
}}>
|
}}>
|
||||||
{tags.length || tier ? (
|
{tags.length || tier ? (
|
||||||
<button
|
<button
|
||||||
className="tw-w-8 tw-h-auto tw-flex-none focus:tw-outline-none"
|
className="tw-w-auto tw-h-auto tw-flex-none focus:tw-outline-none"
|
||||||
data-testid="edit-button">
|
data-testid="edit-button">
|
||||||
<SVGIcons alt="edit" icon="icon-edit" title="Edit" />
|
<SVGIcons alt="edit" icon="icon-edit" title="Edit" />
|
||||||
</button>
|
</button>
|
||||||
|
@ -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
|
// handle assignees search
|
||||||
const onSearch = (query: string) => {
|
const onSearch = (query: string) => {
|
||||||
fetchOptions(query, setOptions);
|
fetchOptions(query, setOptions);
|
||||||
@ -624,9 +633,12 @@ const TaskDetailPage = () => {
|
|||||||
|
|
||||||
{isTaskTags && (
|
{isTaskTags && (
|
||||||
<TagsTask
|
<TagsTask
|
||||||
|
currentTags={getCurrentTags()}
|
||||||
|
hasEditAccess={hasEditAccess()}
|
||||||
isTaskActionEdit={isTaskActionEdit}
|
isTaskActionEdit={isTaskActionEdit}
|
||||||
setSuggestion={setTagsSuggestion}
|
setSuggestion={setTagsSuggestion}
|
||||||
suggestions={tagsSuggestion}
|
suggestions={tagsSuggestion}
|
||||||
|
task={taskDetail.task}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{hasEditAccess() && !isTaskClosed && (
|
{hasEditAccess() && !isTaskClosed && (
|
||||||
|
@ -37,3 +37,9 @@ export enum TaskActionMode {
|
|||||||
VIEW = 'view',
|
VIEW = 'view',
|
||||||
EDIT = 'edit',
|
EDIT = 'edit',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum TaskTabs {
|
||||||
|
CURRENT = 'current',
|
||||||
|
DIFF = 'diff',
|
||||||
|
NEW = 'new',
|
||||||
|
}
|
||||||
|
@ -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<EntityData>({} as EntityData);
|
||||||
|
const [options, setOptions] = useState<Option[]>([]);
|
||||||
|
const [assignees, setAssignees] = useState<Option[]>([]);
|
||||||
|
const [title, setTitle] = useState<string>('');
|
||||||
|
const [currentTags, setCurrentTags] = useState<TagLabel[]>([]);
|
||||||
|
const [suggestion, setSuggestion] = useState<TagLabel[]>([]);
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div data-testid="column-details">
|
||||||
|
<p className="tw-font-semibold">Column Details</p>
|
||||||
|
<p>
|
||||||
|
<span className="tw-text-grey-muted">Type:</span>{' '}
|
||||||
|
<span>{columnObject.dataTypeDisplay}</span>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
{columnObject?.tags
|
||||||
|
?.map((tag: TagLabel) => `#${tag.tagFQN}`)
|
||||||
|
?.join(' ')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} 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<HTMLInputElement>) => {
|
||||||
|
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 (
|
||||||
|
<TaskPageLayout>
|
||||||
|
<TitleBreadcrumb
|
||||||
|
titleLinks={[
|
||||||
|
...getBreadCrumbList(entityData, entityType as EntityType),
|
||||||
|
{ name: 'Create Task', activeTitle: true, url: '' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<div className="tw-grid tw-grid-cols-3 tw-gap-x-2">
|
||||||
|
<Card
|
||||||
|
className="tw-col-span-2"
|
||||||
|
key="update-tags"
|
||||||
|
style={{ ...cardStyles }}
|
||||||
|
title="Create Task">
|
||||||
|
<div data-testid="title">
|
||||||
|
<span>Title:</span>{' '}
|
||||||
|
<Input
|
||||||
|
placeholder="Task title"
|
||||||
|
style={{ margin: '4px 0px' }}
|
||||||
|
value={title}
|
||||||
|
onChange={onTitleChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div data-testid="assignees">
|
||||||
|
<span>Assignees:</span>{' '}
|
||||||
|
<Assignees
|
||||||
|
assignees={assignees}
|
||||||
|
options={options}
|
||||||
|
onChange={setAssignees}
|
||||||
|
onSearch={onSearch}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{currentTags.length ? (
|
||||||
|
<Fragment>
|
||||||
|
<p data-testid="tags-label">
|
||||||
|
<span>Update tags:</span>{' '}
|
||||||
|
</p>
|
||||||
|
<TagsTabs
|
||||||
|
suggestedTags={suggestion}
|
||||||
|
tags={currentTags}
|
||||||
|
onChange={setSuggestion}
|
||||||
|
/>
|
||||||
|
</Fragment>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<div className="tw-flex tw-justify-end" data-testid="cta-buttons">
|
||||||
|
<Button className="ant-btn-link-custom" type="link" onClick={back}>
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className="ant-btn-primary-custom"
|
||||||
|
type="primary"
|
||||||
|
onClick={onCreateTask}>
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<div className="tw-pl-2" data-testid="entity-details">
|
||||||
|
<h6 className="tw-text-base">{capitalize(entityType)} Details</h6>
|
||||||
|
<div className="tw-flex tw-mb-4">
|
||||||
|
<span className="tw-text-grey-muted">Owner:</span>{' '}
|
||||||
|
<span>
|
||||||
|
{entityData.owner ? (
|
||||||
|
<span className="tw-flex tw-ml-1">
|
||||||
|
<ProfilePicture
|
||||||
|
displayName={getEntityName(entityData.owner)}
|
||||||
|
id=""
|
||||||
|
name={getEntityName(entityData.owner)}
|
||||||
|
width="20"
|
||||||
|
/>
|
||||||
|
<span className="tw-ml-1">
|
||||||
|
{getEntityName(entityData.owner)}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="tw-text-grey-muted tw-ml-1">No Owner</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p data-testid="tier">
|
||||||
|
{entityTier ? (
|
||||||
|
entityTier
|
||||||
|
) : (
|
||||||
|
<span className="tw-text-grey-muted">No Tier</span>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p data-testid="tags">{entityTags}</p>
|
||||||
|
|
||||||
|
{getColumnDetails()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TaskPageLayout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default observer(UpdateTag);
|
@ -29,7 +29,7 @@ const { Option } = Select;
|
|||||||
interface SelectOption {
|
interface SelectOption {
|
||||||
label: string;
|
label: string;
|
||||||
value: string;
|
value: string;
|
||||||
'data-sourceType': string;
|
'data-sourcetype': string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -42,7 +42,7 @@ const TagSuggestion: React.FC<Props> = ({ onChange, selectedTags }) => {
|
|||||||
selectedTags.map((tag) => ({
|
selectedTags.map((tag) => ({
|
||||||
label: tag.tagFQN,
|
label: tag.tagFQN,
|
||||||
value: 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<SelectOption[]>([]);
|
const [options, setOptions] = useState<SelectOption[]>([]);
|
||||||
@ -63,7 +63,7 @@ const TagSuggestion: React.FC<Props> = ({ onChange, selectedTags }) => {
|
|||||||
uniqueOptions.map((op: any) => ({
|
uniqueOptions.map((op: any) => ({
|
||||||
label: op.fullyQualifiedName as string,
|
label: op.fullyQualifiedName as string,
|
||||||
value: 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<Props> = ({ onChange, selectedTags }) => {
|
|||||||
const newTags = (option as SelectOption[]).map((value) => ({
|
const newTags = (option as SelectOption[]).map((value) => ({
|
||||||
labelType: LabelType.Manual,
|
labelType: LabelType.Manual,
|
||||||
state: State.Suggested,
|
state: State.Suggested,
|
||||||
source: isEqual(value['data-sourceType'], 'tag')
|
source: isEqual(value['data-sourcetype'], 'tag')
|
||||||
? TagSource.Tag
|
? TagSource.Tag
|
||||||
: TagSource.Glossary,
|
: TagSource.Glossary,
|
||||||
tagFQN: value.value,
|
tagFQN: value.value,
|
||||||
@ -111,7 +111,7 @@ const TagSuggestion: React.FC<Props> = ({ onChange, selectedTags }) => {
|
|||||||
onSearch={handleSearch}>
|
onSearch={handleSearch}>
|
||||||
{options.map((d) => (
|
{options.map((d) => (
|
||||||
<Option
|
<Option
|
||||||
data-sourceType={d['data-sourceType']}
|
data-sourcetype={d['data-sourcetype']}
|
||||||
data-testid="tag-option"
|
data-testid="tag-option"
|
||||||
key={d.value}>
|
key={d.value}>
|
||||||
{d.label}
|
{d.label}
|
||||||
|
@ -0,0 +1,77 @@
|
|||||||
|
/*
|
||||||
|
* 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 { Tag } from 'antd';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { ArrayChange } from 'diff';
|
||||||
|
import { uniqueId } from 'lodash';
|
||||||
|
import React from 'react';
|
||||||
|
import { TagLabel } from '../../../generated/type/tagLabel';
|
||||||
|
|
||||||
|
export const TagsDiffView = ({
|
||||||
|
diffArr,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
diffArr: ArrayChange<TagLabel>[];
|
||||||
|
className?: string;
|
||||||
|
}) => {
|
||||||
|
const elements = diffArr.map((diff) => {
|
||||||
|
if (diff.added) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="tw-my-2 tw-flex tw-flex-wrap tw-gap-y-1"
|
||||||
|
key={uniqueId()}>
|
||||||
|
{diff.value.map((tag) => (
|
||||||
|
<Tag
|
||||||
|
key={uniqueId()}
|
||||||
|
style={{
|
||||||
|
background: 'rgba(0, 131, 118, 0.2)',
|
||||||
|
color: '#008376',
|
||||||
|
}}>
|
||||||
|
{tag.tagFQN}
|
||||||
|
</Tag>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (diff.removed) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="tw-my-2 tw-flex tw-flex-wrap tw-gap-y-1"
|
||||||
|
key={uniqueId()}>
|
||||||
|
{diff.value.map((tag) => (
|
||||||
|
<Tag
|
||||||
|
key={uniqueId()}
|
||||||
|
style={{ color: 'grey', textDecoration: 'line-through' }}>
|
||||||
|
{tag.tagFQN}
|
||||||
|
</Tag>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="tw-my-2 tw-flex tw-flex-wrap tw-gap-y-1" key={uniqueId()}>
|
||||||
|
{diff.value.length ? (
|
||||||
|
diff.value.map((tag) => <Tag key={uniqueId()}>{tag.tagFQN}</Tag>)
|
||||||
|
) : (
|
||||||
|
<div className="tw-text-grey-muted tw-text-center">
|
||||||
|
No diff available
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return <div className={classNames('tw-w-full', className)}>{elements}</div>;
|
||||||
|
};
|
@ -0,0 +1,66 @@
|
|||||||
|
/*
|
||||||
|
* 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 { Tabs, Tag } from 'antd';
|
||||||
|
import { ArrayChange, diffArrays } from 'diff';
|
||||||
|
import { uniqueId } from 'lodash';
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { TagLabel } from '../../../generated/type/tagLabel';
|
||||||
|
import { TaskTabs } from '../TasksPage.interface';
|
||||||
|
import { TagsDiffView } from './TagsDiffView';
|
||||||
|
import TagSuggestion from './TagSuggestion';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
tags: TagLabel[];
|
||||||
|
suggestedTags: TagLabel[];
|
||||||
|
onChange: (value: TagLabel[]) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TagsTabs = ({ tags, suggestedTags, onChange }: Props) => {
|
||||||
|
const { TabPane } = Tabs;
|
||||||
|
|
||||||
|
const [diffs, setDiffs] = useState<ArrayChange<TagLabel>[]>([]);
|
||||||
|
const [activeTab, setActiveTab] = useState<string>(TaskTabs.NEW);
|
||||||
|
|
||||||
|
const onTabChange = (key: string) => {
|
||||||
|
setActiveTab(key);
|
||||||
|
if (key === TaskTabs.DIFF) {
|
||||||
|
setDiffs(diffArrays(tags, suggestedTags));
|
||||||
|
} else {
|
||||||
|
setDiffs([]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tabs
|
||||||
|
activeKey={activeTab}
|
||||||
|
className="ant-tabs-description"
|
||||||
|
size="small"
|
||||||
|
type="card"
|
||||||
|
onChange={onTabChange}>
|
||||||
|
<TabPane key={TaskTabs.CURRENT} tab="Current">
|
||||||
|
<div className="tw-my-2 tw-flex tw-flex-wrap tw-gap-y-1">
|
||||||
|
{tags.map((tag) => (
|
||||||
|
<Tag key={uniqueId()}>{tag.tagFQN}</Tag>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</TabPane>
|
||||||
|
<TabPane key={TaskTabs.DIFF} tab="Diff">
|
||||||
|
<TagsDiffView diffArr={diffs} />
|
||||||
|
</TabPane>
|
||||||
|
<TabPane key={TaskTabs.NEW} tab="New">
|
||||||
|
<TagSuggestion selectedTags={suggestedTags} onChange={onChange} />
|
||||||
|
</TabPane>
|
||||||
|
</Tabs>
|
||||||
|
);
|
||||||
|
};
|
@ -11,14 +11,23 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Tag } from 'antd';
|
import { diffArrays } from 'diff';
|
||||||
import { uniqueId } from 'lodash';
|
import React, { FC, Fragment } from 'react';
|
||||||
import React, { FC } from 'react';
|
import {
|
||||||
|
TaskType,
|
||||||
|
Thread,
|
||||||
|
ThreadTaskStatus,
|
||||||
|
} from '../../../generated/entity/feed/thread';
|
||||||
import { TagLabel } from '../../../generated/type/tagLabel';
|
import { TagLabel } from '../../../generated/type/tagLabel';
|
||||||
|
import { TagsDiffView } from './TagsDiffView';
|
||||||
|
import { TagsTabs } from './TagsTabs';
|
||||||
import TagSuggestion from './TagSuggestion';
|
import TagSuggestion from './TagSuggestion';
|
||||||
|
|
||||||
interface TagsTaskProps {
|
interface TagsTaskProps {
|
||||||
|
task: Thread['task'];
|
||||||
isTaskActionEdit: boolean;
|
isTaskActionEdit: boolean;
|
||||||
|
hasEditAccess: boolean;
|
||||||
|
currentTags: TagLabel[];
|
||||||
suggestions: TagLabel[];
|
suggestions: TagLabel[];
|
||||||
setSuggestion: (value: TagLabel[]) => void;
|
setSuggestion: (value: TagLabel[]) => void;
|
||||||
}
|
}
|
||||||
@ -27,19 +36,93 @@ const TagsTask: FC<TagsTaskProps> = ({
|
|||||||
suggestions,
|
suggestions,
|
||||||
setSuggestion,
|
setSuggestion,
|
||||||
isTaskActionEdit,
|
isTaskActionEdit,
|
||||||
|
hasEditAccess,
|
||||||
|
task,
|
||||||
|
currentTags,
|
||||||
}) => {
|
}) => {
|
||||||
|
const isRequestTag = task?.type === TaskType.RequestTag;
|
||||||
|
|
||||||
|
const isUpdateTag = task?.type === TaskType.UpdateTag;
|
||||||
|
|
||||||
|
const isTaskClosed = task?.status === ThreadTaskStatus.Closed;
|
||||||
|
|
||||||
|
const getDiffView = () => {
|
||||||
|
const oldValue = task?.oldValue;
|
||||||
|
const newValue = task?.newValue;
|
||||||
|
if (!oldValue && !newValue) {
|
||||||
|
return (
|
||||||
|
<div className="tw-border tw-border-main tw-p-2 tw-rounded tw-my-1 tw-mb-3">
|
||||||
|
<span className="tw-p-2 tw-text-grey-muted">No Tags</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<TagsDiffView
|
||||||
|
diffArr={diffArrays(
|
||||||
|
JSON.parse(oldValue ?? '[]'),
|
||||||
|
JSON.parse(newValue ?? '[]')
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @returns Suggested tags diff
|
||||||
|
*/
|
||||||
|
const getSuggestedTagDiff = () => {
|
||||||
|
const newTags = task?.suggestion;
|
||||||
|
const oldTags = task?.oldValue;
|
||||||
|
|
||||||
|
return !newTags && !oldTags ? (
|
||||||
|
<span className="tw-p-2 tw-text-grey-muted">No Suggestion</span>
|
||||||
|
) : (
|
||||||
|
<TagsDiffView
|
||||||
|
diffArr={diffArrays(
|
||||||
|
JSON.parse(oldTags ?? '[]'),
|
||||||
|
JSON.parse(newTags ?? '[]')
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div data-testid="task-tags-tabs">
|
<div data-testid="task-tags-tabs">
|
||||||
<p className="tw-text-grey-muted">Tags:</p>{' '}
|
<p className="tw-text-grey-muted">Tags:</p>{' '}
|
||||||
{isTaskActionEdit ? (
|
<Fragment>
|
||||||
<TagSuggestion selectedTags={suggestions} onChange={setSuggestion} />
|
{isTaskClosed ? (
|
||||||
) : (
|
getDiffView()
|
||||||
<div className="tw-flex tw-flex-wrap tw-mt-2">
|
) : (
|
||||||
{suggestions.map((suggestion) => (
|
<div data-testid="tags-task">
|
||||||
<Tag key={uniqueId()}>{suggestion.tagFQN}</Tag>
|
{isRequestTag && (
|
||||||
))}
|
<div data-testid="request-tags">
|
||||||
</div>
|
{isTaskActionEdit && hasEditAccess ? (
|
||||||
)}
|
<TagSuggestion
|
||||||
|
selectedTags={suggestions}
|
||||||
|
onChange={setSuggestion}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
getSuggestedTagDiff()
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{isUpdateTag && (
|
||||||
|
<div data-testid="update-tags">
|
||||||
|
{isTaskActionEdit && hasEditAccess ? (
|
||||||
|
<TagsTabs
|
||||||
|
suggestedTags={suggestions}
|
||||||
|
tags={currentTags}
|
||||||
|
onChange={setSuggestion}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
getSuggestedTagDiff()
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Fragment>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -166,6 +166,10 @@ const UpdateDescriptionPage = withSuspenseFallback(
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const UpdateTagsPage = withSuspenseFallback(
|
||||||
|
React.lazy(() => import('../pages/TasksPage/UpdateTagPage/UpdateTagPage'))
|
||||||
|
);
|
||||||
|
|
||||||
const TaskDetailPage = withSuspenseFallback(
|
const TaskDetailPage = withSuspenseFallback(
|
||||||
React.lazy(() => import('../pages/TasksPage/TaskDetailPage/TaskDetailPage'))
|
React.lazy(() => import('../pages/TasksPage/TaskDetailPage/TaskDetailPage'))
|
||||||
);
|
);
|
||||||
@ -337,6 +341,7 @@ const AuthenticatedAppRouter: FunctionComponent = () => {
|
|||||||
|
|
||||||
<Route exact component={TaskDetailPage} path={ROUTES.TASK_DETAIL} />
|
<Route exact component={TaskDetailPage} path={ROUTES.TASK_DETAIL} />
|
||||||
<Route exact component={RequestTagsPage} path={ROUTES.REQUEST_TAGS} />
|
<Route exact component={RequestTagsPage} path={ROUTES.REQUEST_TAGS} />
|
||||||
|
<Route exact component={UpdateTagsPage} path={ROUTES.UPDATE_TAGS} />
|
||||||
|
|
||||||
<Redirect to={ROUTES.NOT_FOUND} />
|
<Redirect to={ROUTES.NOT_FOUND} />
|
||||||
</Switch>
|
</Switch>
|
||||||
|
@ -106,6 +106,26 @@ export const getUpdateDescriptionPath = (
|
|||||||
return { pathname, search: searchParams.toString() };
|
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) => {
|
export const getTaskDetailPath = (taskId: string) => {
|
||||||
const pathname = ROUTES.TASK_DETAIL.replace(PLACEHOLDER_TASK_ID, taskId);
|
const pathname = ROUTES.TASK_DETAIL.replace(PLACEHOLDER_TASK_ID, taskId);
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user