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:
Sachin Chaurasiya 2022-07-15 22:00:57 +05:30 committed by GitHub
parent ccdfef8f0e
commit 4c30b01286
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 631 additions and 29 deletions

View File

@ -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>

View File

@ -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(() => {

View File

@ -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>

View File

@ -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 && (

View File

@ -37,3 +37,9 @@ export enum TaskActionMode {
VIEW = 'view',
EDIT = 'edit',
}
export enum TaskTabs {
CURRENT = 'current',
DIFF = 'diff',
NEW = 'new',
}

View File

@ -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);

View File

@ -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}

View File

@ -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>;
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>

View File

@ -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);