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 (
|
||||
<Fragment>
|
||||
<div id="thread-panel-body">
|
||||
{showHeader && isConversationType ? (
|
||||
{showHeader && isConversationType && (
|
||||
<FeedPanelHeader
|
||||
className="tw-px-4 tw-shadow-sm"
|
||||
entityField={entityField as string}
|
||||
@ -235,7 +235,8 @@ const ActivityThreadPanelBody: FC<ActivityThreadPanelBodyProp> = ({
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
)}
|
||||
{isTaskType && (
|
||||
<div className="tw-flex tw-justify-end tw-mr-2 tw-mt-2">
|
||||
<Switch onChange={onSwitchChange} />
|
||||
<span className="tw-ml-1">Closed Tasks</span>
|
||||
|
@ -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 (
|
||||
<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"
|
||||
data-testid="request-tags"
|
||||
onClick={() => onRequestTagsHandler(cell)}>
|
||||
onClick={() =>
|
||||
hasTags ? onUpdateTagsHandler(cell) : onRequestTagsHandler(cell)
|
||||
}>
|
||||
<Popover
|
||||
destroyTooltipOnHide
|
||||
content="Request tags"
|
||||
content={text}
|
||||
overlayClassName="ant-popover-request-description"
|
||||
trigger="hover"
|
||||
zIndex={9999}>
|
||||
<SVGIcons alt="request-tags" icon={Icons.REQUEST} width="16px" />
|
||||
</Popover>
|
||||
</button>
|
||||
) : null;
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -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<EntityTags>) => {
|
||||
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 ? (
|
||||
<button
|
||||
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"
|
||||
onClick={handleRequestTags}>
|
||||
onClick={hasTags ? handleUpdateTags : handleRequestTags}>
|
||||
<Popover
|
||||
destroyTooltipOnHide
|
||||
content="Request tags"
|
||||
content={text}
|
||||
overlayClassName="ant-popover-request-description"
|
||||
trigger="hover"
|
||||
zIndex={9999}>
|
||||
@ -341,7 +349,7 @@ const EntityPageInfo = ({
|
||||
onThreadLinkSelect?.(tagTask.entityLink, ThreadType.Task)
|
||||
}>
|
||||
<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">
|
||||
{tagTask.count}
|
||||
</span>
|
||||
@ -531,7 +539,7 @@ const EntityPageInfo = ({
|
||||
}}>
|
||||
{tags.length || tier ? (
|
||||
<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">
|
||||
<SVGIcons alt="edit" icon="icon-edit" title="Edit" />
|
||||
</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
|
||||
const onSearch = (query: string) => {
|
||||
fetchOptions(query, setOptions);
|
||||
@ -624,9 +633,12 @@ const TaskDetailPage = () => {
|
||||
|
||||
{isTaskTags && (
|
||||
<TagsTask
|
||||
currentTags={getCurrentTags()}
|
||||
hasEditAccess={hasEditAccess()}
|
||||
isTaskActionEdit={isTaskActionEdit}
|
||||
setSuggestion={setTagsSuggestion}
|
||||
suggestions={tagsSuggestion}
|
||||
task={taskDetail.task}
|
||||
/>
|
||||
)}
|
||||
{hasEditAccess() && !isTaskClosed && (
|
||||
|
@ -37,3 +37,9 @@ export enum TaskActionMode {
|
||||
VIEW = 'view',
|
||||
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 {
|
||||
label: string;
|
||||
value: string;
|
||||
'data-sourceType': string;
|
||||
'data-sourcetype': string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
@ -42,7 +42,7 @@ const TagSuggestion: React.FC<Props> = ({ 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<SelectOption[]>([]);
|
||||
@ -63,7 +63,7 @@ const TagSuggestion: React.FC<Props> = ({ 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<Props> = ({ 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<Props> = ({ onChange, selectedTags }) => {
|
||||
onSearch={handleSearch}>
|
||||
{options.map((d) => (
|
||||
<Option
|
||||
data-sourceType={d['data-sourceType']}
|
||||
data-sourcetype={d['data-sourcetype']}
|
||||
data-testid="tag-option"
|
||||
key={d.value}>
|
||||
{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.
|
||||
*/
|
||||
|
||||
import { Tag } from 'antd';
|
||||
import { uniqueId } from 'lodash';
|
||||
import React, { FC } from 'react';
|
||||
import { diffArrays } from 'diff';
|
||||
import React, { FC, Fragment } from 'react';
|
||||
import {
|
||||
TaskType,
|
||||
Thread,
|
||||
ThreadTaskStatus,
|
||||
} from '../../../generated/entity/feed/thread';
|
||||
import { TagLabel } from '../../../generated/type/tagLabel';
|
||||
import { TagsDiffView } from './TagsDiffView';
|
||||
import { TagsTabs } from './TagsTabs';
|
||||
import TagSuggestion from './TagSuggestion';
|
||||
|
||||
interface TagsTaskProps {
|
||||
task: Thread['task'];
|
||||
isTaskActionEdit: boolean;
|
||||
hasEditAccess: boolean;
|
||||
currentTags: TagLabel[];
|
||||
suggestions: TagLabel[];
|
||||
setSuggestion: (value: TagLabel[]) => void;
|
||||
}
|
||||
@ -27,19 +36,93 @@ const TagsTask: FC<TagsTaskProps> = ({
|
||||
suggestions,
|
||||
setSuggestion,
|
||||
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 (
|
||||
<div data-testid="task-tags-tabs">
|
||||
<p className="tw-text-grey-muted">Tags:</p>{' '}
|
||||
{isTaskActionEdit ? (
|
||||
<TagSuggestion selectedTags={suggestions} onChange={setSuggestion} />
|
||||
) : (
|
||||
<div className="tw-flex tw-flex-wrap tw-mt-2">
|
||||
{suggestions.map((suggestion) => (
|
||||
<Tag key={uniqueId()}>{suggestion.tagFQN}</Tag>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<Fragment>
|
||||
{isTaskClosed ? (
|
||||
getDiffView()
|
||||
) : (
|
||||
<div data-testid="tags-task">
|
||||
{isRequestTag && (
|
||||
<div data-testid="request-tags">
|
||||
{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>
|
||||
);
|
||||
};
|
||||
|
@ -166,6 +166,10 @@ const UpdateDescriptionPage = withSuspenseFallback(
|
||||
)
|
||||
);
|
||||
|
||||
const UpdateTagsPage = withSuspenseFallback(
|
||||
React.lazy(() => import('../pages/TasksPage/UpdateTagPage/UpdateTagPage'))
|
||||
);
|
||||
|
||||
const TaskDetailPage = withSuspenseFallback(
|
||||
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={RequestTagsPage} path={ROUTES.REQUEST_TAGS} />
|
||||
<Route exact component={UpdateTagsPage} path={ROUTES.UPDATE_TAGS} />
|
||||
|
||||
<Redirect to={ROUTES.NOT_FOUND} />
|
||||
</Switch>
|
||||
|
@ -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);
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user