mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-11-03 12:08:31 +00:00
✨ Activity Feed: Show "Request Tag" button for Tags (#5862)
* ✨ Activity Feed: Show "Request Tag" button for Tags
* Fix alignment issues
* Add condition to show task for supported entities.
* Refactor Task detail page
* Refactor task description.
* Add flow for resolve task
This commit is contained in:
parent
33dd4602b9
commit
d9445fccf3
@ -466,6 +466,10 @@ const DashboardDetails = ({
|
||||
<EntityPageInfo
|
||||
isTagEditable
|
||||
deleted={deleted}
|
||||
entityFieldTasks={getEntityFieldThreadCounts(
|
||||
EntityField.TAGS,
|
||||
entityFieldTaskCount
|
||||
)}
|
||||
entityFieldThreads={getEntityFieldThreadCounts(
|
||||
EntityField.TAGS,
|
||||
entityFieldThreadCount
|
||||
|
||||
@ -585,8 +585,12 @@ const DatasetDetails: React.FC<DatasetDetailsProps> = ({
|
||||
<EntityPageInfo
|
||||
isTagEditable
|
||||
deleted={deleted}
|
||||
entityFieldTasks={getEntityFieldThreadCounts(
|
||||
EntityField.TAGS,
|
||||
entityFieldTaskCount
|
||||
)}
|
||||
entityFieldThreads={getEntityFieldThreadCounts(
|
||||
'tags',
|
||||
EntityField.TAGS,
|
||||
entityFieldThreadCount
|
||||
)}
|
||||
entityFqn={datasetFQN}
|
||||
|
||||
@ -362,6 +362,10 @@ const PipelineDetails = ({
|
||||
<EntityPageInfo
|
||||
isTagEditable
|
||||
deleted={deleted}
|
||||
entityFieldTasks={getEntityFieldThreadCounts(
|
||||
EntityField.TAGS,
|
||||
entityFieldTaskCount
|
||||
)}
|
||||
entityFieldThreads={getEntityFieldThreadCounts(
|
||||
EntityField.TAGS,
|
||||
entityFieldThreadCount
|
||||
|
||||
@ -390,6 +390,10 @@ const TopicDetails: React.FC<TopicDetailsProps> = ({
|
||||
<EntityPageInfo
|
||||
isTagEditable
|
||||
deleted={deleted}
|
||||
entityFieldTasks={getEntityFieldThreadCounts(
|
||||
EntityField.TAGS,
|
||||
entityFieldTaskCount
|
||||
)}
|
||||
entityFieldThreads={getEntityFieldThreadCounts(
|
||||
EntityField.TAGS,
|
||||
entityFieldThreadCount
|
||||
|
||||
@ -13,13 +13,17 @@
|
||||
|
||||
import { faExclamationCircle, faStar } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { Popover } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
import { cloneDeep, isEmpty, isUndefined } from 'lodash';
|
||||
import { EntityFieldThreads, EntityTags, ExtraInfo, TagOption } from 'Models';
|
||||
import React, { Fragment, useEffect, useState } from 'react';
|
||||
import React, { Fragment, useCallback, useEffect, useState } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { FQN_SEPARATOR_CHAR } from '../../../constants/char.constants';
|
||||
import { FOLLOWERS_VIEW_CAP } from '../../../constants/constants';
|
||||
import { SettledStatus } from '../../../enums/axios.enum';
|
||||
import { EntityType } from '../../../enums/entity.enum';
|
||||
import { ThreadType } from '../../../generated/entity/feed/thread';
|
||||
import { Operation } from '../../../generated/entity/policies/accessControl/rule';
|
||||
import { EntityReference } from '../../../generated/type/entityReference';
|
||||
import { LabelType, State, TagLabel } from '../../../generated/type/tagLabel';
|
||||
@ -32,6 +36,7 @@ import {
|
||||
} from '../../../utils/GlossaryUtils';
|
||||
import SVGIcons, { Icons } from '../../../utils/SvgUtils';
|
||||
import { getTagCategories, getTaglist } from '../../../utils/TagsUtils';
|
||||
import { getRequestTagsPath, TASK_ENTITIES } from '../../../utils/TasksUtils';
|
||||
import TagsContainer from '../../tags-container/tags-container';
|
||||
import TagsViewer from '../../tags-viewer/tags-viewer';
|
||||
import Tags from '../../tags/tags';
|
||||
@ -60,7 +65,8 @@ interface Props {
|
||||
version?: string;
|
||||
isVersionSelected?: boolean;
|
||||
entityFieldThreads?: EntityFieldThreads[];
|
||||
onThreadLinkSelect?: (value: string) => void;
|
||||
entityFieldTasks?: EntityFieldThreads[];
|
||||
onThreadLinkSelect?: (value: string, threadType?: ThreadType) => void;
|
||||
followHandler?: () => void;
|
||||
tagsHandler?: (selectedTags?: Array<EntityTags>) => void;
|
||||
versionHandler?: () => void;
|
||||
@ -88,8 +94,11 @@ const EntityPageInfo = ({
|
||||
onThreadLinkSelect,
|
||||
entityFqn,
|
||||
entityType,
|
||||
entityFieldTasks,
|
||||
}: Props) => {
|
||||
const history = useHistory();
|
||||
const tagThread = entityFieldThreads?.[0];
|
||||
const tagTask = entityFieldTasks?.[0];
|
||||
const [isEditable, setIsEditable] = useState<boolean>(false);
|
||||
const [entityFollowers, setEntityFollowers] =
|
||||
useState<Array<EntityReference>>(followersList);
|
||||
@ -101,6 +110,10 @@ const EntityPageInfo = ({
|
||||
document.getElementById('version-and-follow-section')?.offsetWidth
|
||||
);
|
||||
|
||||
const handleRequestTags = () => {
|
||||
history.push(getRequestTagsPath(entityType as string, entityFqn as string));
|
||||
};
|
||||
|
||||
const handleTagSelection = (selectedTags?: Array<EntityTags>) => {
|
||||
if (selectedTags) {
|
||||
const prevTags =
|
||||
@ -269,35 +282,74 @@ const EntityPageInfo = ({
|
||||
|
||||
const getThreadElements = () => {
|
||||
if (!isUndefined(entityFieldThreads)) {
|
||||
return !isUndefined(tagThread) ? (
|
||||
<p
|
||||
className="link-text tw-m-0 tw-ml-1 tw-w-8 tw-flex-none"
|
||||
return !isUndefined(tagThread) &&
|
||||
TASK_ENTITIES.includes(entityType as EntityType) ? (
|
||||
<button
|
||||
className="tw-w-8 tw-h-8 tw-mr-1 tw-flex-none link-text focus:tw-outline-none"
|
||||
data-testid="tag-thread"
|
||||
onClick={() => onThreadLinkSelect?.(tagThread.entityLink)}>
|
||||
<span className="tw-flex">
|
||||
<SVGIcons alt="comments" icon={Icons.COMMENT} width="20px" />
|
||||
<SVGIcons alt="comments" icon={Icons.COMMENT} />
|
||||
<span className="tw-ml-1" data-testid="tag-thread-count">
|
||||
{tagThread.count}
|
||||
</span>
|
||||
</span>
|
||||
</p>
|
||||
</button>
|
||||
) : (
|
||||
<p
|
||||
className="link-text tw-self-start tw-w-8 tw-m-0 tw-ml-1 tw-flex-none"
|
||||
<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="start-tag-thread"
|
||||
onClick={() =>
|
||||
onThreadLinkSelect?.(
|
||||
getEntityFeedLink(entityType, entityFqn, 'tags')
|
||||
)
|
||||
}>
|
||||
<SVGIcons alt="comments" icon={Icons.COMMENT_PLUS} width="20px" />
|
||||
</p>
|
||||
<SVGIcons alt="comments" icon={Icons.COMMENT_PLUS} />
|
||||
</button>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const getRequestTagsElements = useCallback(() => {
|
||||
const hasTags = !isEmpty(tags);
|
||||
|
||||
return onThreadLinkSelect && !hasTags ? (
|
||||
<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}>
|
||||
<Popover
|
||||
destroyTooltipOnHide
|
||||
content="Request tags"
|
||||
overlayClassName="ant-popover-request-description"
|
||||
trigger="hover"
|
||||
zIndex={9999}>
|
||||
<SVGIcons alt="request-tags" icon={Icons.REQUEST} />
|
||||
</Popover>
|
||||
</button>
|
||||
) : null;
|
||||
}, [tags]);
|
||||
|
||||
const getTaskElement = useCallback(() => {
|
||||
return !isUndefined(tagTask) ? (
|
||||
<button
|
||||
className="tw-w-8 tw-h-8 tw-mr-1 tw-flex-none link-text focus:tw-outline-none"
|
||||
data-testid="tag-task"
|
||||
onClick={() =>
|
||||
onThreadLinkSelect?.(tagTask.entityLink, ThreadType.Task)
|
||||
}>
|
||||
<span className="tw-flex">
|
||||
<SVGIcons alt="comments" icon={Icons.TASK_ICON} />
|
||||
<span className="tw-ml-1" data-testid="tag-task-count">
|
||||
{tagTask.count}
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
) : null;
|
||||
}, [tagTask]);
|
||||
|
||||
useEffect(() => {
|
||||
setEntityFollowers(followersList);
|
||||
}, [followersList]);
|
||||
@ -454,7 +506,7 @@ const EntityPageInfo = ({
|
||||
position="bottom"
|
||||
trigger="click">
|
||||
<div
|
||||
className="tw-inline-block"
|
||||
className="tw-inline-block tw-mr-1"
|
||||
data-testid="tags-wrapper"
|
||||
onClick={() => {
|
||||
// Fetch tags and terms only once
|
||||
@ -479,14 +531,9 @@ const EntityPageInfo = ({
|
||||
}}>
|
||||
{tags.length || tier ? (
|
||||
<button
|
||||
className=" tw-ml-1 focus:tw-outline-none"
|
||||
className="tw-w-8 tw-h-auto tw-flex-none focus:tw-outline-none"
|
||||
data-testid="edit-button">
|
||||
<SVGIcons
|
||||
alt="edit"
|
||||
icon="icon-edit"
|
||||
title="Edit"
|
||||
width="16px"
|
||||
/>
|
||||
<SVGIcons alt="edit" icon="icon-edit" title="Edit" />
|
||||
</button>
|
||||
) : (
|
||||
<span>
|
||||
@ -501,7 +548,11 @@ const EntityPageInfo = ({
|
||||
</TagsContainer>
|
||||
</div>
|
||||
</NonAdminAction>
|
||||
{getThreadElements()}
|
||||
<div className="tw--mt-1.5">
|
||||
{getRequestTagsElements()}
|
||||
{getTaskElement()}
|
||||
{getThreadElements()}
|
||||
</div>
|
||||
</Fragment>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -209,7 +209,9 @@ export const ROUTES = {
|
||||
|
||||
// Tasks Routes
|
||||
REQUEST_DESCRIPTION: `/request-description/${PLACEHOLDER_ROUTE_ENTITY_TYPE}/${PLACEHOLDER_ROUTE_ENTITY_FQN}`,
|
||||
REQUEST_TAGS: `/request-tags/${PLACEHOLDER_ROUTE_ENTITY_TYPE}/${PLACEHOLDER_ROUTE_ENTITY_FQN}`,
|
||||
UPDATE_DESCRIPTION: `/update-description/${PLACEHOLDER_ROUTE_ENTITY_TYPE}/${PLACEHOLDER_ROUTE_ENTITY_FQN}`,
|
||||
UPDATE_TAGS: `/update-tags/${PLACEHOLDER_ROUTE_ENTITY_TYPE}/${PLACEHOLDER_ROUTE_ENTITY_FQN}`,
|
||||
TASK_DETAIL: `/tasks/${PLACEHOLDER_TASK_ID}`,
|
||||
|
||||
ACTIVITY_PUSH_FEED: '/api/v1/push/feed',
|
||||
|
||||
@ -0,0 +1,282 @@
|
||||
/*
|
||||
* 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, isNil } from 'lodash';
|
||||
import { observer } from 'mobx-react';
|
||||
import { EntityTags } from 'Models';
|
||||
import React, {
|
||||
ChangeEvent,
|
||||
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 TagSuggestion from '../shared/TagSuggestion';
|
||||
import TaskPageLayout from '../shared/TaskPageLayout';
|
||||
import { cardStyles } from '../TaskPage.styles';
|
||||
import { EntityData, Option } from '../TasksPage.interface';
|
||||
|
||||
const RequestTag = () => {
|
||||
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 [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 = `Request tags for ${getSanitizeValue || entityType}`;
|
||||
|
||||
// get current user details
|
||||
const currentUser = useMemo(
|
||||
() => AppState.getCurrentUserDetails(),
|
||||
[AppState.userDetails, AppState.nonSecureUserDetails]
|
||||
);
|
||||
|
||||
const back = () => history.goBack();
|
||||
|
||||
const getColumnDetails = useCallback(() => {
|
||||
if (!isNil(field) && !isNil(value) && field === EntityField.COLUMNS) {
|
||||
const column = getSanitizeValue.split(FQN_SEPARATOR_CHAR).slice(-1);
|
||||
|
||||
const columnObject = getColumnObject(column[0], entityData.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) => `#${tag.tagFQN}`)?.join(' ')}</p>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}, [entityData.columns]);
|
||||
|
||||
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.RequestTag,
|
||||
oldValue: '[]',
|
||||
},
|
||||
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]);
|
||||
|
||||
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="request-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>
|
||||
|
||||
<p data-testid="tags-label">
|
||||
<span>Suggest tags:</span>{' '}
|
||||
</p>
|
||||
<TagSuggestion selectedTags={suggestion} onChange={setSuggestion} />
|
||||
|
||||
<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}>
|
||||
{suggestion ? 'Suggest' : '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(RequestTag);
|
||||
@ -13,14 +13,14 @@
|
||||
|
||||
import { faChevronDown } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { Button, Card, Dropdown, Layout, Menu, Modal, Tabs } from 'antd';
|
||||
import { Button, Card, Dropdown, Layout, Menu, Tabs } from 'antd';
|
||||
import { AxiosError, AxiosResponse } from 'axios';
|
||||
import classNames from 'classnames';
|
||||
import { compare, Operation } from 'fast-json-patch';
|
||||
import { isEmpty, isEqual, isUndefined, toLower } from 'lodash';
|
||||
import { isEmpty, isEqual, toLower } from 'lodash';
|
||||
import { observer } from 'mobx-react';
|
||||
import { EditorContentRef, EntityReference, EntityTags } from 'Models';
|
||||
import React, { Fragment, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { EntityReference } from 'Models';
|
||||
import React, { Fragment, useEffect, useMemo, useState } from 'react';
|
||||
import { useHistory, useParams } from 'react-router-dom';
|
||||
import AppState from '../../../AppState';
|
||||
import { useAuthContext } from '../../../authentication/auth-provider/AuthProvider';
|
||||
@ -37,17 +37,14 @@ import ActivityFeedEditor from '../../../components/ActivityFeed/ActivityFeedEdi
|
||||
import FeedPanelBody from '../../../components/ActivityFeed/ActivityFeedPanel/FeedPanelBody';
|
||||
import ActivityThreadPanelBody from '../../../components/ActivityFeed/ActivityThreadPanel/ActivityThreadPanelBody';
|
||||
import AssigneeList from '../../../components/common/AssigneeList/AssigneeList';
|
||||
import Ellipses from '../../../components/common/Ellipses/Ellipses';
|
||||
import ErrorPlaceHolder from '../../../components/common/error-with-placeholder/ErrorPlaceHolder';
|
||||
import UserPopOverCard from '../../../components/common/PopOverCard/UserPopOverCard';
|
||||
import ProfilePicture from '../../../components/common/ProfilePicture/ProfilePicture';
|
||||
import RichTextEditor from '../../../components/common/rich-text-editor/RichTextEditor';
|
||||
import TitleBreadcrumb from '../../../components/common/title-breadcrumb/title-breadcrumb.component';
|
||||
import { FQN_SEPARATOR_CHAR } from '../../../constants/char.constants';
|
||||
import { TaskOperation } from '../../../constants/feed.constants';
|
||||
import { PanelTab, TaskOperation } from '../../../constants/feed.constants';
|
||||
import { EntityType } from '../../../enums/entity.enum';
|
||||
import { CreateThread } from '../../../generated/api/feed/createThread';
|
||||
import { Column } from '../../../generated/entity/data/table';
|
||||
import {
|
||||
TaskDetails,
|
||||
TaskType,
|
||||
@ -55,8 +52,8 @@ import {
|
||||
ThreadTaskStatus,
|
||||
ThreadType,
|
||||
} from '../../../generated/entity/feed/thread';
|
||||
import { TagLabel } from '../../../generated/type/tagLabel';
|
||||
import { useAuth } from '../../../hooks/authHooks';
|
||||
import { getEntityName } from '../../../utils/CommonUtils';
|
||||
import { ENTITY_LINK_SEPARATOR } from '../../../utils/EntityUtils';
|
||||
import {
|
||||
deletePost,
|
||||
@ -67,24 +64,26 @@ import {
|
||||
} from '../../../utils/FeedUtils';
|
||||
import { getEncodedFqn } from '../../../utils/StringsUtils';
|
||||
import SVGIcons from '../../../utils/SvgUtils';
|
||||
import {
|
||||
getEntityLink,
|
||||
getTagsWithoutTier,
|
||||
getTierTags,
|
||||
} from '../../../utils/TableUtils';
|
||||
import { getEntityLink } from '../../../utils/TableUtils';
|
||||
import {
|
||||
fetchEntityDetail,
|
||||
fetchOptions,
|
||||
getBreadCrumbList,
|
||||
getColumnObject,
|
||||
getDescriptionDiff,
|
||||
isDescriptionTask,
|
||||
isTagsTask,
|
||||
TASK_ACTION_LIST,
|
||||
} from '../../../utils/TasksUtils';
|
||||
import { getDayTimeByTimeStamp } from '../../../utils/TimeUtils';
|
||||
import { showErrorToast, showSuccessToast } from '../../../utils/ToastUtils';
|
||||
import Assignees from '../shared/Assignees';
|
||||
import { DescriptionTabs } from '../shared/DescriptionTabs';
|
||||
import { DiffView } from '../shared/DiffView';
|
||||
import ClosedTask from '../shared/ClosedTask';
|
||||
import ColumnDetail from '../shared/ColumnDetail';
|
||||
import CommentModal from '../shared/CommentModal';
|
||||
import DescriptionTask from '../shared/DescriptionTask';
|
||||
import EntityDetail from '../shared/EntityDetail';
|
||||
import TagsTask from '../shared/TagsTask';
|
||||
import TaskStatus from '../shared/TaskStatus';
|
||||
import { background, cardStyles, contentStyles } from '../TaskPage.styles';
|
||||
import {
|
||||
EntityData,
|
||||
@ -102,8 +101,6 @@ const TaskDetailPage = () => {
|
||||
|
||||
const { taskId } = useParams<{ [key: string]: string }>();
|
||||
|
||||
const markdownRef = useRef<EditorContentRef>();
|
||||
|
||||
const [taskDetail, setTaskDetail] = useState<Thread>({} as Thread);
|
||||
const [taskFeedDetail, setTaskFeedDetail] = useState<Thread>({} as Thread);
|
||||
const [entityData, setEntityData] = useState<EntityData>({} as EntityData);
|
||||
@ -116,6 +113,7 @@ const TaskDetailPage = () => {
|
||||
const [taskAction, setTaskAction] = useState<TaskAction>(TASK_ACTION_LIST[0]);
|
||||
const [modalVisible, setModalVisible] = useState<boolean>(false);
|
||||
const [comment, setComment] = useState<string>('');
|
||||
const [tagsSuggestion, setTagsSuggestion] = useState<TagLabel[]>([]);
|
||||
|
||||
// get current user details
|
||||
const currentUser = useMemo(
|
||||
@ -123,18 +121,6 @@ const TaskDetailPage = () => {
|
||||
[AppState.userDetails, AppState.nonSecureUserDetails]
|
||||
);
|
||||
|
||||
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 entityType = useMemo(() => {
|
||||
return getEntityType(taskDetail.about);
|
||||
}, [taskDetail]);
|
||||
@ -144,24 +130,25 @@ const TaskDetailPage = () => {
|
||||
}, [taskDetail]);
|
||||
|
||||
const columnObject = useMemo(() => {
|
||||
// prepare column from entityField
|
||||
const column = entityField?.split(ENTITY_LINK_SEPARATOR)?.slice(-2)?.[0];
|
||||
|
||||
// prepare column value by replacing double quotes
|
||||
const columnValue = column?.replaceAll(/^"|"$/g, '') || '';
|
||||
|
||||
/**
|
||||
* Get column name by spliting columnValue with FQN Separator
|
||||
*/
|
||||
const columnName = columnValue.split(FQN_SEPARATOR_CHAR).pop();
|
||||
|
||||
return getColumnObject(columnName as string, entityData.columns || []);
|
||||
}, [taskDetail, entityData]);
|
||||
|
||||
const isRequestDescription = isEqual(
|
||||
taskDetail.task?.type,
|
||||
TaskType.RequestDescription
|
||||
);
|
||||
|
||||
const isUpdateDescription = isEqual(
|
||||
taskDetail.task?.type,
|
||||
TaskType.UpdateDescription
|
||||
);
|
||||
// const isRequestTag = isEqual(taskDetail.task?.type, TaskType.RequestTag);
|
||||
// const isUpdateTag = isEqual(taskDetail.task?.type, TaskType.UpdateTag);
|
||||
|
||||
const isOwner = isEqual(entityData.owner?.id, currentUser?.id);
|
||||
|
||||
const isAssignee = taskDetail.task?.assignees?.some((assignee) =>
|
||||
isEqual(assignee.id, currentUser?.id)
|
||||
);
|
||||
@ -175,6 +162,12 @@ const TaskDetailPage = () => {
|
||||
|
||||
const isTaskActionEdit = isEqual(taskAction.key, TaskActionMode.EDIT);
|
||||
|
||||
const isTaskDescription = isDescriptionTask(
|
||||
taskDetail.task?.type as TaskType
|
||||
);
|
||||
|
||||
const isTaskTags = isTagsTask(taskDetail.task?.type as TaskType);
|
||||
|
||||
const fetchTaskDetail = () => {
|
||||
getTask(taskId)
|
||||
.then((res: AxiosResponse) => {
|
||||
@ -305,8 +298,7 @@ const TaskDetailPage = () => {
|
||||
};
|
||||
|
||||
const onTaskResolve = () => {
|
||||
if (suggestion) {
|
||||
const data = { newValue: suggestion };
|
||||
const updateTaskData = (data: Record<string, string>) => {
|
||||
updateTask(TaskOperation.RESOLVE, taskDetail.task?.id, data)
|
||||
.then(() => {
|
||||
showSuccessToast('Task Resolved Successfully');
|
||||
@ -318,10 +310,24 @@ const TaskDetailPage = () => {
|
||||
);
|
||||
})
|
||||
.catch((err: AxiosError) => showErrorToast(err));
|
||||
};
|
||||
|
||||
if (isTaskTags) {
|
||||
if (!isEmpty(tagsSuggestion)) {
|
||||
const data = { newValue: JSON.stringify(tagsSuggestion || '[]') };
|
||||
updateTaskData(data);
|
||||
} else {
|
||||
showErrorToast('Cannot accept an empty tag list. Please add a tags.');
|
||||
}
|
||||
} else {
|
||||
showErrorToast(
|
||||
'Cannot accept an empty description. Please add a description.'
|
||||
);
|
||||
if (suggestion) {
|
||||
const data = { newValue: suggestion };
|
||||
updateTaskData(data);
|
||||
} else {
|
||||
showErrorToast(
|
||||
'Cannot accept an empty description. Please add a description.'
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -381,6 +387,7 @@ const TaskDetailPage = () => {
|
||||
updateThreadData(threadId, postId, isThread, data, callback);
|
||||
};
|
||||
|
||||
// prepare current description for update description task
|
||||
const currentDescription = () => {
|
||||
if (entityField && !isEmpty(columnObject)) {
|
||||
return columnObject.description || '';
|
||||
@ -389,24 +396,47 @@ const TaskDetailPage = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// handle assignees search
|
||||
const onSearch = (query: string) => {
|
||||
fetchOptions(query, setOptions);
|
||||
};
|
||||
|
||||
// handle sider tab change
|
||||
const onTabChange = (key: string) => {
|
||||
if (isEqual(key, '1')) {
|
||||
if (isEqual(key, PanelTab.TASKS)) {
|
||||
fetchTaskFeed(taskDetail.id);
|
||||
}
|
||||
};
|
||||
|
||||
// handle task action change
|
||||
const onTaskActionChange = (key: string) => {
|
||||
setTaskAction(
|
||||
TASK_ACTION_LIST.find((action) => isEqual(action.key, key)) as TaskAction
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param taskSuggestion suggestion value
|
||||
* Based on task type set's the suggestion for task
|
||||
*/
|
||||
const setTaskSuggestionOnRender = (taskSuggestion: string | undefined) => {
|
||||
if (isTaskTags) {
|
||||
const tagsSuggestion = JSON.parse(taskSuggestion || '[]');
|
||||
isEmpty(tagsSuggestion) && setTaskAction(TASK_ACTION_LIST[1]);
|
||||
setTagsSuggestion(tagsSuggestion);
|
||||
} else {
|
||||
if (!taskSuggestion) {
|
||||
setTaskAction(TASK_ACTION_LIST[1]);
|
||||
}
|
||||
setSuggestion(taskSuggestion || '');
|
||||
}
|
||||
};
|
||||
|
||||
// handle task details change
|
||||
const onTaskDetailChange = () => {
|
||||
if (!isEmpty(taskDetail)) {
|
||||
// get entityFQN and fetch entity data
|
||||
const entityFQN = getEntityFQN(taskDetail.about);
|
||||
|
||||
entityFQN &&
|
||||
@ -417,8 +447,8 @@ const TaskDetailPage = () => {
|
||||
);
|
||||
fetchTaskFeed(taskDetail.id);
|
||||
|
||||
// set task assignees
|
||||
const taskAssignees = taskDetail.task?.assignees || [];
|
||||
const taskSuggestion = taskDetail.task?.suggestion;
|
||||
if (taskAssignees.length) {
|
||||
const assigneesArr = taskAssignees.map((assignee) => ({
|
||||
label: assignee.name as string,
|
||||
@ -428,10 +458,9 @@ const TaskDetailPage = () => {
|
||||
setAssignees(assigneesArr);
|
||||
setOptions(assigneesArr);
|
||||
}
|
||||
if (!taskSuggestion) {
|
||||
setTaskAction(TASK_ACTION_LIST[1]);
|
||||
}
|
||||
setSuggestion(taskSuggestion || '');
|
||||
|
||||
// set task suggestion on render
|
||||
setTaskSuggestionOnRender(taskDetail.task?.suggestion);
|
||||
}
|
||||
};
|
||||
|
||||
@ -447,154 +476,18 @@ const TaskDetailPage = () => {
|
||||
onTaskDetailChange();
|
||||
}, [taskDetail]);
|
||||
|
||||
const TaskStatusElement = ({ status }: { status: ThreadTaskStatus }) => {
|
||||
const openCheck = isEqual(status, ThreadTaskStatus.Open);
|
||||
const closedCheck = isEqual(status, ThreadTaskStatus.Closed);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<div
|
||||
className={classNames(
|
||||
'tw-rounded-3xl tw-px-2 tw-p-0',
|
||||
{
|
||||
'tw-bg-task-status-bg': openCheck,
|
||||
},
|
||||
{ 'tw-bg-gray-100': closedCheck }
|
||||
)}>
|
||||
<span
|
||||
className={classNames(
|
||||
'tw-inline-block tw-w-2 tw-h-2 tw-rounded-full',
|
||||
{
|
||||
'tw-bg-task-status-fg': openCheck,
|
||||
},
|
||||
{
|
||||
'tw-bg-gray-500': closedCheck,
|
||||
}
|
||||
)}
|
||||
/>
|
||||
<span
|
||||
className={classNames(
|
||||
'tw-ml-1',
|
||||
{ 'tw-text-task-status-fg': openCheck },
|
||||
{ 'tw-text-gray-500': closedCheck }
|
||||
)}>
|
||||
{status}
|
||||
</span>
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
const ColumnDetail = ({ column }: { column: Column }) => {
|
||||
return !isEmpty(column) && !isUndefined(column) ? (
|
||||
<div className="tw-mb-4" data-testid="column-details">
|
||||
<div className="tw-flex">
|
||||
<span className="tw-text-grey-muted tw-flex-none tw-mr-1">
|
||||
Column type:
|
||||
</span>{' '}
|
||||
<Ellipses tooltip rows={1}>
|
||||
{column.dataTypeDisplay}
|
||||
</Ellipses>
|
||||
</div>
|
||||
{column.tags && column.tags.length ? (
|
||||
<div className="tw-flex tw-mt-4">
|
||||
<SVGIcons
|
||||
alt="icon-tag"
|
||||
className="tw-mr-1"
|
||||
icon="icon-tag-grey"
|
||||
width="12"
|
||||
/>
|
||||
<div>{column.tags.map((tag) => `#${tag.tagFQN}`)?.join(' ')}</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : null;
|
||||
};
|
||||
|
||||
const EntityDetail = () => {
|
||||
return (
|
||||
<div data-testid="entityDetail">
|
||||
<div className="tw-flex tw-ml-6">
|
||||
<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>
|
||||
<span className="tw-mx-1.5 tw-inline-block tw-text-gray-400">|</span>
|
||||
<p data-testid="tier">
|
||||
{entityTier ? (
|
||||
entityTier
|
||||
) : (
|
||||
<span className="tw-text-grey-muted">No Tier</span>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<p className="tw-ml-6" data-testid="tags">
|
||||
{entityTags}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getDiffView = () => {
|
||||
const oldValue = taskDetail.task?.oldValue;
|
||||
const newValue = taskDetail.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 Description</span>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<DiffView
|
||||
className="tw-border tw-border-main tw-p-2 tw-rounded tw-my-1 tw-mb-3"
|
||||
diffArr={getDescriptionDiff(
|
||||
taskDetail?.task?.oldValue || '',
|
||||
taskDetail?.task?.newValue || ''
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const getCurrentDescription = () => {
|
||||
const newDescription = taskDetail?.task?.suggestion;
|
||||
const oldDescription = taskDetail?.task?.oldValue;
|
||||
|
||||
const diffs = getDescriptionDiff(
|
||||
oldDescription || '',
|
||||
newDescription || ''
|
||||
);
|
||||
|
||||
return !newDescription && !oldDescription ? (
|
||||
<span className="tw-p-2 tw-text-grey-muted">No Suggestion</span>
|
||||
) : (
|
||||
<DiffView className="tw-p-2" diffArr={diffs} />
|
||||
);
|
||||
};
|
||||
|
||||
const onModalClose = () => {
|
||||
// handle comment modal close
|
||||
const onCommentModalClose = () => {
|
||||
setModalVisible(false);
|
||||
setComment('');
|
||||
};
|
||||
|
||||
const hasEditAccess = () => {
|
||||
return isAdminUser || isAuthDisabled || isAssignee || isOwner;
|
||||
};
|
||||
/**
|
||||
*
|
||||
* @returns True if has access otherwise false
|
||||
*/
|
||||
const hasEditAccess = () =>
|
||||
isAdminUser || isAuthDisabled || isAssignee || isOwner;
|
||||
|
||||
return (
|
||||
<Layout style={{ ...background, height: '100vh' }}>
|
||||
@ -613,7 +506,7 @@ const TaskDetailPage = () => {
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<EntityDetail />
|
||||
<EntityDetail entityData={entityData} />
|
||||
|
||||
<Card
|
||||
data-testid="task-metadata"
|
||||
@ -623,8 +516,8 @@ const TaskDetailPage = () => {
|
||||
data-testid="task-title">
|
||||
{`Task #${taskId}`} {taskDetail.message}
|
||||
</p>
|
||||
<p className="tw-flex tw-mb-4" data-testid="task-metadata">
|
||||
<TaskStatusElement
|
||||
<div className="tw-flex tw-mb-4" data-testid="task-metadata">
|
||||
<TaskStatus
|
||||
status={taskDetail.task?.status as ThreadTaskStatus}
|
||||
/>
|
||||
<span className="tw-mx-1.5 tw-inline-block tw-text-gray-400">
|
||||
@ -651,7 +544,7 @@ const TaskDetailPage = () => {
|
||||
)}
|
||||
</span>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<ColumnDetail column={columnObject} />
|
||||
<div className="tw-flex" data-testid="task-assignees">
|
||||
@ -718,54 +611,24 @@ const TaskDetailPage = () => {
|
||||
<Card
|
||||
data-testid="task-data"
|
||||
style={{ ...cardStyles, marginTop: '16px', marginLeft: '24px' }}>
|
||||
<div data-testid="task-description-tabs">
|
||||
<p className="tw-text-grey-muted">Description:</p>{' '}
|
||||
{!isEmpty(taskDetail) && (
|
||||
<Fragment>
|
||||
{isTaskClosed ? (
|
||||
getDiffView()
|
||||
) : (
|
||||
<div data-testid="description-task">
|
||||
{isRequestDescription && (
|
||||
<div data-testid="request-description">
|
||||
{isTaskActionEdit && hasEditAccess() ? (
|
||||
<RichTextEditor
|
||||
height="208px"
|
||||
initialValue={suggestion}
|
||||
placeHolder="Add description"
|
||||
style={{ marginTop: '0px' }}
|
||||
onTextChange={onSuggestionChange}
|
||||
/>
|
||||
) : (
|
||||
<div className="tw-flex tw-border tw-border-main tw-rounded tw-mb-4">
|
||||
{getCurrentDescription()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isUpdateDescription && (
|
||||
<div data-testid="update-description">
|
||||
{isTaskActionEdit && hasEditAccess() ? (
|
||||
<DescriptionTabs
|
||||
description={currentDescription()}
|
||||
markdownRef={markdownRef}
|
||||
suggestion={suggestion}
|
||||
onChange={onSuggestionChange}
|
||||
/>
|
||||
) : (
|
||||
<div className="tw-flex tw-border tw-border-main tw-rounded tw-mb-4">
|
||||
{getCurrentDescription()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Fragment>
|
||||
)}
|
||||
</div>
|
||||
{isTaskDescription && (
|
||||
<DescriptionTask
|
||||
currentDescription={currentDescription()}
|
||||
hasEditAccess={hasEditAccess()}
|
||||
isTaskActionEdit={isTaskActionEdit}
|
||||
suggestion={suggestion}
|
||||
taskDetail={taskDetail}
|
||||
onSuggestionChange={onSuggestionChange}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isTaskTags && (
|
||||
<TagsTask
|
||||
isTaskActionEdit={isTaskActionEdit}
|
||||
setSuggestion={setTagsSuggestion}
|
||||
suggestions={tagsSuggestion}
|
||||
/>
|
||||
)}
|
||||
{hasEditAccess() && !isTaskClosed && (
|
||||
<div
|
||||
className="tw-flex tw-justify-end"
|
||||
@ -811,57 +674,16 @@ const TaskDetailPage = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isTaskClosed && (
|
||||
<div className="tw-flex" data-testid="task-closed">
|
||||
<UserPopOverCard userName={taskDetail?.task?.closedBy || ''}>
|
||||
<span className="tw-flex">
|
||||
<ProfilePicture
|
||||
displayName={taskDetail?.task?.closedBy}
|
||||
id=""
|
||||
name={taskDetail?.task?.closedBy || ''}
|
||||
width="20"
|
||||
/>
|
||||
<span className="tw-font-semibold tw-cursor-pointer hover:tw-underline tw-ml-1">
|
||||
{taskDetail?.task?.closedBy}
|
||||
</span>{' '}
|
||||
</span>
|
||||
</UserPopOverCard>
|
||||
<span className="tw-ml-1"> closed this task </span>
|
||||
<span className="tw-ml-1">
|
||||
{toLower(
|
||||
getDayTimeByTimeStamp(
|
||||
taskDetail?.task?.closedAt as number
|
||||
)
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{isTaskClosed && <ClosedTask task={taskDetail.task} />}
|
||||
</Card>
|
||||
<Modal
|
||||
centered
|
||||
destroyOnClose
|
||||
cancelButtonProps={{
|
||||
type: 'link',
|
||||
className: 'ant-btn-link-custom',
|
||||
}}
|
||||
okButtonProps={{
|
||||
disabled: !comment,
|
||||
className: 'ant-btn-primary-custom',
|
||||
}}
|
||||
okText="Close with comment"
|
||||
title={`Close Task #${taskDetail.task?.id} ${taskDetail.message}`}
|
||||
visible={modalVisible}
|
||||
width={700}
|
||||
onCancel={onModalClose}
|
||||
onOk={onTaskReject}>
|
||||
<RichTextEditor
|
||||
height="208px"
|
||||
initialValue={comment}
|
||||
placeHolder="Add comment"
|
||||
style={{ marginTop: '0px' }}
|
||||
onTextChange={setComment}
|
||||
/>
|
||||
</Modal>
|
||||
<CommentModal
|
||||
comment={comment}
|
||||
isVisible={modalVisible}
|
||||
setComment={setComment}
|
||||
taskDetail={taskDetail}
|
||||
onClose={onCommentModalClose}
|
||||
onConfirm={onTaskReject}
|
||||
/>
|
||||
</Content>
|
||||
|
||||
<Sider
|
||||
@ -869,7 +691,7 @@ const TaskDetailPage = () => {
|
||||
data-testid="task-right-sider"
|
||||
width={600}>
|
||||
<Tabs className="ant-tabs-custom-line" onChange={onTabChange}>
|
||||
<TabPane key="1" tab="Task">
|
||||
<TabPane key={PanelTab.TASKS} tab="Task">
|
||||
{!isEmpty(taskFeedDetail) ? (
|
||||
<div id="task-feed">
|
||||
<FeedPanelBody
|
||||
@ -886,7 +708,7 @@ const TaskDetailPage = () => {
|
||||
) : null}
|
||||
</TabPane>
|
||||
|
||||
<TabPane key="2" tab="Conversations">
|
||||
<TabPane key={PanelTab.CONVERSATIONS} tab="Conversations">
|
||||
{!isEmpty(taskFeedDetail) ? (
|
||||
<ActivityThreadPanelBody
|
||||
className="tw-p-0"
|
||||
|
||||
@ -38,7 +38,7 @@ const Assignees: FC<Props> = ({ assignees, onSearch, onChange, options }) => {
|
||||
return (
|
||||
<Select
|
||||
showSearch
|
||||
className="ant-select-assignee"
|
||||
className="ant-select-custom"
|
||||
data-testid="select-assignee"
|
||||
defaultActiveFirstOption={false}
|
||||
filterOption={false}
|
||||
|
||||
@ -0,0 +1,49 @@
|
||||
/*
|
||||
* 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 { toLower } from 'lodash';
|
||||
import React, { FC } from 'react';
|
||||
import UserPopOverCard from '../../../components/common/PopOverCard/UserPopOverCard';
|
||||
import ProfilePicture from '../../../components/common/ProfilePicture/ProfilePicture';
|
||||
import { Thread } from '../../../generated/entity/feed/thread';
|
||||
import { getDayTimeByTimeStamp } from '../../../utils/TimeUtils';
|
||||
|
||||
interface ClosedTaskProps {
|
||||
task: Thread['task'];
|
||||
}
|
||||
|
||||
const ClosedTask: FC<ClosedTaskProps> = ({ task }) => {
|
||||
return (
|
||||
<div className="tw-flex" data-testid="task-closed">
|
||||
<UserPopOverCard userName={task?.closedBy || ''}>
|
||||
<span className="tw-flex">
|
||||
<ProfilePicture
|
||||
displayName={task?.closedBy}
|
||||
id=""
|
||||
name={task?.closedBy || ''}
|
||||
width="20"
|
||||
/>
|
||||
<span className="tw-font-semibold tw-cursor-pointer hover:tw-underline tw-ml-1">
|
||||
{task?.closedBy}
|
||||
</span>{' '}
|
||||
</span>
|
||||
</UserPopOverCard>
|
||||
<span className="tw-ml-1"> closed this task </span>
|
||||
<span className="tw-ml-1">
|
||||
{toLower(getDayTimeByTimeStamp(task?.closedAt as number))}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ClosedTask;
|
||||
@ -0,0 +1,46 @@
|
||||
/*
|
||||
* 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 { isEmpty, isUndefined } from 'lodash';
|
||||
import React from 'react';
|
||||
import Ellipses from '../../../components/common/Ellipses/Ellipses';
|
||||
import { Column } from '../../../generated/entity/data/table';
|
||||
import SVGIcons from '../../../utils/SvgUtils';
|
||||
|
||||
const ColumnDetail = ({ column }: { column: Column }) => {
|
||||
return !isEmpty(column) && !isUndefined(column) ? (
|
||||
<div className="tw-mb-4" data-testid="column-details">
|
||||
<div className="tw-flex">
|
||||
<span className="tw-text-grey-muted tw-flex-none tw-mr-1">
|
||||
Column type:
|
||||
</span>{' '}
|
||||
<Ellipses tooltip rows={1}>
|
||||
{column.dataTypeDisplay}
|
||||
</Ellipses>
|
||||
</div>
|
||||
{column.tags && column.tags.length ? (
|
||||
<div className="tw-flex tw-mt-4">
|
||||
<SVGIcons
|
||||
alt="icon-tag"
|
||||
className="tw-mr-1"
|
||||
icon="icon-tag-grey"
|
||||
width="12"
|
||||
/>
|
||||
<div>{column.tags.map((tag) => `#${tag.tagFQN}`)?.join(' ')}</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : null;
|
||||
};
|
||||
|
||||
export default ColumnDetail;
|
||||
@ -0,0 +1,65 @@
|
||||
/*
|
||||
* 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 { Modal } from 'antd';
|
||||
import React, { FC } from 'react';
|
||||
import RichTextEditor from '../../../components/common/rich-text-editor/RichTextEditor';
|
||||
import { Thread } from '../../../generated/entity/feed/thread';
|
||||
|
||||
interface CommentModalProps {
|
||||
taskDetail: Thread;
|
||||
comment: string;
|
||||
isVisible: boolean;
|
||||
setComment: (value: string) => void;
|
||||
onClose: () => void;
|
||||
onConfirm: () => void;
|
||||
}
|
||||
|
||||
const CommentModal: FC<CommentModalProps> = ({
|
||||
taskDetail,
|
||||
comment,
|
||||
isVisible,
|
||||
setComment,
|
||||
onClose,
|
||||
onConfirm,
|
||||
}) => {
|
||||
return (
|
||||
<Modal
|
||||
centered
|
||||
destroyOnClose
|
||||
cancelButtonProps={{
|
||||
type: 'link',
|
||||
className: 'ant-btn-link-custom',
|
||||
}}
|
||||
okButtonProps={{
|
||||
disabled: !comment,
|
||||
className: 'ant-btn-primary-custom',
|
||||
}}
|
||||
okText="Close with comment"
|
||||
title={`Close Task #${taskDetail.task?.id} ${taskDetail.message}`}
|
||||
visible={isVisible}
|
||||
width={700}
|
||||
onCancel={onClose}
|
||||
onOk={onConfirm}>
|
||||
<RichTextEditor
|
||||
height="208px"
|
||||
initialValue={comment}
|
||||
placeHolder="Add comment"
|
||||
style={{ marginTop: '0px' }}
|
||||
onTextChange={setComment}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default CommentModal;
|
||||
@ -0,0 +1,152 @@
|
||||
/*
|
||||
* 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 { isEqual } from 'lodash';
|
||||
import { EditorContentRef } from 'Models';
|
||||
import React, { FC, Fragment, useRef } from 'react';
|
||||
import RichTextEditor from '../../../components/common/rich-text-editor/RichTextEditor';
|
||||
import {
|
||||
TaskType,
|
||||
Thread,
|
||||
ThreadTaskStatus,
|
||||
} from '../../../generated/entity/feed/thread';
|
||||
import { getDescriptionDiff } from '../../../utils/TasksUtils';
|
||||
import { DescriptionTabs } from './DescriptionTabs';
|
||||
import { DiffView } from './DiffView';
|
||||
|
||||
interface DescriptionTaskProps {
|
||||
taskDetail: Thread;
|
||||
isTaskActionEdit: boolean;
|
||||
hasEditAccess: boolean;
|
||||
suggestion: string;
|
||||
currentDescription: string;
|
||||
onSuggestionChange: (value: string) => void;
|
||||
}
|
||||
|
||||
const DescriptionTask: FC<DescriptionTaskProps> = ({
|
||||
taskDetail,
|
||||
isTaskActionEdit,
|
||||
hasEditAccess,
|
||||
suggestion,
|
||||
currentDescription,
|
||||
onSuggestionChange,
|
||||
}) => {
|
||||
const markdownRef = useRef<EditorContentRef>();
|
||||
|
||||
const isRequestDescription = isEqual(
|
||||
taskDetail.task?.type,
|
||||
TaskType.RequestDescription
|
||||
);
|
||||
|
||||
const isUpdateDescription = isEqual(
|
||||
taskDetail.task?.type,
|
||||
TaskType.UpdateDescription
|
||||
);
|
||||
|
||||
const isTaskClosed = isEqual(
|
||||
taskDetail.task?.status,
|
||||
ThreadTaskStatus.Closed
|
||||
);
|
||||
|
||||
const getDiffView = () => {
|
||||
const oldValue = taskDetail.task?.oldValue;
|
||||
const newValue = taskDetail.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 Description</span>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<DiffView
|
||||
className="tw-border tw-border-main tw-p-2 tw-rounded tw-my-1 tw-mb-3"
|
||||
diffArr={getDescriptionDiff(
|
||||
taskDetail?.task?.oldValue || '',
|
||||
taskDetail?.task?.newValue || ''
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns Suggested description diff
|
||||
*/
|
||||
const getSuggestedDescriptionDiff = () => {
|
||||
const newDescription = taskDetail?.task?.suggestion;
|
||||
const oldDescription = taskDetail?.task?.oldValue;
|
||||
|
||||
const diffs = getDescriptionDiff(
|
||||
oldDescription || '',
|
||||
newDescription || ''
|
||||
);
|
||||
|
||||
return !newDescription && !oldDescription ? (
|
||||
<span className="tw-p-2 tw-text-grey-muted">No Suggestion</span>
|
||||
) : (
|
||||
<DiffView className="tw-p-2" diffArr={diffs} />
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div data-testid="task-description-tabs">
|
||||
<p className="tw-text-grey-muted">Description:</p>{' '}
|
||||
<Fragment>
|
||||
{isTaskClosed ? (
|
||||
getDiffView()
|
||||
) : (
|
||||
<div data-testid="description-task">
|
||||
{isRequestDescription && (
|
||||
<div data-testid="request-description">
|
||||
{isTaskActionEdit && hasEditAccess ? (
|
||||
<RichTextEditor
|
||||
height="208px"
|
||||
initialValue={suggestion}
|
||||
placeHolder="Add description"
|
||||
style={{ marginTop: '0px' }}
|
||||
onTextChange={onSuggestionChange}
|
||||
/>
|
||||
) : (
|
||||
<div className="tw-flex tw-border tw-border-main tw-rounded tw-mb-4">
|
||||
{getSuggestedDescriptionDiff()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isUpdateDescription && (
|
||||
<div data-testid="update-description">
|
||||
{isTaskActionEdit && hasEditAccess ? (
|
||||
<DescriptionTabs
|
||||
description={currentDescription}
|
||||
markdownRef={markdownRef}
|
||||
suggestion={suggestion}
|
||||
onChange={onSuggestionChange}
|
||||
/>
|
||||
) : (
|
||||
<div className="tw-flex tw-border tw-border-main tw-rounded tw-mb-4">
|
||||
{getSuggestedDescriptionDiff()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Fragment>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DescriptionTask;
|
||||
@ -0,0 +1,74 @@
|
||||
/*
|
||||
* 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 { EntityTags } from 'Models';
|
||||
import React, { useMemo } from 'react';
|
||||
import ProfilePicture from '../../../components/common/ProfilePicture/ProfilePicture';
|
||||
import { FQN_SEPARATOR_CHAR } from '../../../constants/char.constants';
|
||||
import { getEntityName } from '../../../utils/CommonUtils';
|
||||
import { getTagsWithoutTier, getTierTags } from '../../../utils/TableUtils';
|
||||
import { EntityData } from '../TasksPage.interface';
|
||||
|
||||
interface EntityDetailProps {
|
||||
entityData: EntityData;
|
||||
}
|
||||
|
||||
const EntityDetail: React.FC<EntityDetailProps> = ({ entityData }) => {
|
||||
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]);
|
||||
|
||||
return (
|
||||
<div data-testid="entityDetail">
|
||||
<div className="tw-flex tw-ml-6">
|
||||
<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>
|
||||
<span className="tw-mx-1.5 tw-inline-block tw-text-gray-400">|</span>
|
||||
<p data-testid="tier">
|
||||
{entityTier ? (
|
||||
entityTier
|
||||
) : (
|
||||
<span className="tw-text-grey-muted">No Tier</span>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<p className="tw-ml-6" data-testid="tags">
|
||||
{entityTags}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EntityDetail;
|
||||
@ -0,0 +1,124 @@
|
||||
/*
|
||||
* 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 { Select } from 'antd';
|
||||
import { AxiosError, AxiosResponse } from 'axios';
|
||||
import { isEmpty, isEqual } from 'lodash';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { getTagSuggestions } from '../../../axiosAPIs/miscAPI';
|
||||
import {
|
||||
LabelType,
|
||||
Source,
|
||||
State,
|
||||
TagLabel,
|
||||
} from '../../../generated/type/tagLabel';
|
||||
import { showErrorToast } from '../../../utils/ToastUtils';
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
interface SelectOption {
|
||||
label: string;
|
||||
value: string;
|
||||
'data-sourceType': string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
onChange: (newTags: TagLabel[]) => void;
|
||||
selectedTags: TagLabel[];
|
||||
}
|
||||
|
||||
const TagSuggestion: React.FC<Props> = ({ onChange, selectedTags }) => {
|
||||
const selectedOptions = () =>
|
||||
selectedTags.map((tag) => ({
|
||||
label: tag.tagFQN,
|
||||
value: tag.tagFQN,
|
||||
'data-sourceType': isEqual(tag.source, 'Tag') ? 'tag' : 'glossaryTerm',
|
||||
}));
|
||||
|
||||
const [options, setOptions] = useState<SelectOption[]>([]);
|
||||
|
||||
const fetchOptions = (query: string) => {
|
||||
getTagSuggestions(query)
|
||||
.then((res: AxiosResponse) => {
|
||||
const suggestOptions =
|
||||
res.data.suggest['metadata-suggest'][0].options ?? [];
|
||||
const uniqueOptions = [
|
||||
...new Set(
|
||||
// eslint-disable-next-line
|
||||
suggestOptions.map((op: any) => op._source)
|
||||
),
|
||||
];
|
||||
setOptions(
|
||||
// eslint-disable-next-line
|
||||
uniqueOptions.map((op: any) => ({
|
||||
label: op.fullyQualifiedName as string,
|
||||
value: op.fullyQualifiedName as string,
|
||||
'data-sourceType': op.entityType,
|
||||
}))
|
||||
);
|
||||
})
|
||||
.catch((err: AxiosError) => showErrorToast(err));
|
||||
};
|
||||
const handleSearch = (newValue: string) => {
|
||||
if (newValue) {
|
||||
fetchOptions(newValue);
|
||||
} else {
|
||||
setOptions([]);
|
||||
}
|
||||
};
|
||||
const handleOnChange = (
|
||||
_values: SelectOption[],
|
||||
option: SelectOption | SelectOption[]
|
||||
) => {
|
||||
const newTags = (option as SelectOption[]).map((value) => ({
|
||||
labelType: LabelType.Manual,
|
||||
state: State.Suggested,
|
||||
source: isEqual(value['data-sourceType'], 'tag')
|
||||
? Source.Tag
|
||||
: Source.Glossary,
|
||||
tagFQN: value.value,
|
||||
}));
|
||||
onChange(newTags);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setOptions(selectedOptions());
|
||||
}, [selectedTags]);
|
||||
|
||||
return (
|
||||
<Select
|
||||
showSearch
|
||||
className="ant-select-custom"
|
||||
data-testid="select-tags"
|
||||
defaultActiveFirstOption={false}
|
||||
filterOption={false}
|
||||
mode="multiple"
|
||||
notFoundContent={null}
|
||||
placeholder="Search to Select"
|
||||
showArrow={false}
|
||||
value={!isEmpty(selectedOptions()) ? selectedOptions() : undefined}
|
||||
onChange={handleOnChange}
|
||||
onSearch={handleSearch}>
|
||||
{options.map((d) => (
|
||||
<Option
|
||||
data-sourceType={d['data-sourceType']}
|
||||
data-testid="tag-option"
|
||||
key={d.value}>
|
||||
{d.label}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
);
|
||||
};
|
||||
|
||||
export default TagSuggestion;
|
||||
@ -0,0 +1,47 @@
|
||||
/*
|
||||
* 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 { uniqueId } from 'lodash';
|
||||
import React, { FC } from 'react';
|
||||
import { TagLabel } from '../../../generated/type/tagLabel';
|
||||
import TagSuggestion from './TagSuggestion';
|
||||
|
||||
interface TagsTaskProps {
|
||||
isTaskActionEdit: boolean;
|
||||
suggestions: TagLabel[];
|
||||
setSuggestion: (value: TagLabel[]) => void;
|
||||
}
|
||||
|
||||
const TagsTask: FC<TagsTaskProps> = ({
|
||||
suggestions,
|
||||
setSuggestion,
|
||||
isTaskActionEdit,
|
||||
}) => {
|
||||
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>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TagsTask;
|
||||
@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright 2021 Collate
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { isEqual } from 'lodash';
|
||||
import React, { Fragment } from 'react';
|
||||
import { ThreadTaskStatus } from '../../../generated/entity/feed/thread';
|
||||
|
||||
const TaskStatus = ({ status }: { status: ThreadTaskStatus }) => {
|
||||
const openCheck = isEqual(status, ThreadTaskStatus.Open);
|
||||
const closedCheck = isEqual(status, ThreadTaskStatus.Closed);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<div
|
||||
className={classNames(
|
||||
'tw-rounded-3xl tw-px-2 tw-p-0',
|
||||
{
|
||||
'tw-bg-task-status-bg': openCheck,
|
||||
},
|
||||
{ 'tw-bg-gray-100': closedCheck }
|
||||
)}>
|
||||
<span
|
||||
className={classNames(
|
||||
'tw-inline-block tw-w-2 tw-h-2 tw-rounded-full',
|
||||
{
|
||||
'tw-bg-task-status-fg': openCheck,
|
||||
},
|
||||
{
|
||||
'tw-bg-gray-500': closedCheck,
|
||||
}
|
||||
)}
|
||||
/>
|
||||
<span
|
||||
className={classNames(
|
||||
'tw-ml-1',
|
||||
{ 'tw-text-task-status-fg': openCheck },
|
||||
{ 'tw-text-gray-500': closedCheck }
|
||||
)}>
|
||||
{status}
|
||||
</span>
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default TaskStatus;
|
||||
@ -155,6 +155,10 @@ const RequestDescriptionPage = withSuspenseFallback(
|
||||
)
|
||||
);
|
||||
|
||||
const RequestTagsPage = withSuspenseFallback(
|
||||
React.lazy(() => import('../pages/TasksPage/RequestTagPage/RequestTagPage'))
|
||||
);
|
||||
|
||||
const UpdateDescriptionPage = withSuspenseFallback(
|
||||
React.lazy(
|
||||
() =>
|
||||
@ -332,6 +336,7 @@ const AuthenticatedAppRouter: FunctionComponent = () => {
|
||||
/>
|
||||
|
||||
<Route exact component={TaskDetailPage} path={ROUTES.TASK_DETAIL} />
|
||||
<Route exact component={RequestTagsPage} path={ROUTES.REQUEST_TAGS} />
|
||||
|
||||
<Redirect to={ROUTES.NOT_FOUND} />
|
||||
</Switch>
|
||||
|
||||
@ -1183,12 +1183,12 @@ code {
|
||||
}
|
||||
/* Antd custom button CSS */
|
||||
|
||||
.ant-select-assignee {
|
||||
.ant-select-custom {
|
||||
width: 100%;
|
||||
margin: 8px 0px;
|
||||
outline: none;
|
||||
}
|
||||
.ant-select-assignee:not(.ant-select-disabled):hover .ant-select-selector {
|
||||
.ant-select-custom:not(.ant-select-disabled):hover .ant-select-selector {
|
||||
border-color: #7147e8;
|
||||
}
|
||||
.ant-select-focused {
|
||||
|
||||
@ -31,6 +31,7 @@ import {
|
||||
import { EntityType, FqnPart } from '../enums/entity.enum';
|
||||
import { ServiceCategory } from '../enums/service.enum';
|
||||
import { Column, Table } from '../generated/entity/data/table';
|
||||
import { TaskType } from '../generated/entity/feed/thread';
|
||||
import { EntityReference } from '../generated/type/entityReference';
|
||||
import {
|
||||
EntityData,
|
||||
@ -65,6 +66,26 @@ export const getRequestDescriptionPath = (
|
||||
return { pathname, search: searchParams.toString() };
|
||||
};
|
||||
|
||||
export const getRequestTagsPath = (
|
||||
entityType: string,
|
||||
entityFQN: string,
|
||||
field?: string,
|
||||
value?: string
|
||||
) => {
|
||||
let pathname = ROUTES.REQUEST_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 getUpdateDescriptionPath = (
|
||||
entityType: string,
|
||||
entityFQN: string,
|
||||
@ -263,3 +284,9 @@ export const TASK_ACTION_LIST = [
|
||||
key: TaskActionMode.EDIT,
|
||||
},
|
||||
];
|
||||
|
||||
export const isDescriptionTask = (taskType: TaskType) =>
|
||||
[TaskType.RequestDescription, TaskType.UpdateDescription].includes(taskType);
|
||||
|
||||
export const isTagsTask = (taskType: TaskType) =>
|
||||
[TaskType.RequestTag, TaskType.UpdateTag].includes(taskType);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user