mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-11-13 17:32:53 +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
|
<EntityPageInfo
|
||||||
isTagEditable
|
isTagEditable
|
||||||
deleted={deleted}
|
deleted={deleted}
|
||||||
|
entityFieldTasks={getEntityFieldThreadCounts(
|
||||||
|
EntityField.TAGS,
|
||||||
|
entityFieldTaskCount
|
||||||
|
)}
|
||||||
entityFieldThreads={getEntityFieldThreadCounts(
|
entityFieldThreads={getEntityFieldThreadCounts(
|
||||||
EntityField.TAGS,
|
EntityField.TAGS,
|
||||||
entityFieldThreadCount
|
entityFieldThreadCount
|
||||||
|
|||||||
@ -585,8 +585,12 @@ const DatasetDetails: React.FC<DatasetDetailsProps> = ({
|
|||||||
<EntityPageInfo
|
<EntityPageInfo
|
||||||
isTagEditable
|
isTagEditable
|
||||||
deleted={deleted}
|
deleted={deleted}
|
||||||
|
entityFieldTasks={getEntityFieldThreadCounts(
|
||||||
|
EntityField.TAGS,
|
||||||
|
entityFieldTaskCount
|
||||||
|
)}
|
||||||
entityFieldThreads={getEntityFieldThreadCounts(
|
entityFieldThreads={getEntityFieldThreadCounts(
|
||||||
'tags',
|
EntityField.TAGS,
|
||||||
entityFieldThreadCount
|
entityFieldThreadCount
|
||||||
)}
|
)}
|
||||||
entityFqn={datasetFQN}
|
entityFqn={datasetFQN}
|
||||||
|
|||||||
@ -362,6 +362,10 @@ const PipelineDetails = ({
|
|||||||
<EntityPageInfo
|
<EntityPageInfo
|
||||||
isTagEditable
|
isTagEditable
|
||||||
deleted={deleted}
|
deleted={deleted}
|
||||||
|
entityFieldTasks={getEntityFieldThreadCounts(
|
||||||
|
EntityField.TAGS,
|
||||||
|
entityFieldTaskCount
|
||||||
|
)}
|
||||||
entityFieldThreads={getEntityFieldThreadCounts(
|
entityFieldThreads={getEntityFieldThreadCounts(
|
||||||
EntityField.TAGS,
|
EntityField.TAGS,
|
||||||
entityFieldThreadCount
|
entityFieldThreadCount
|
||||||
|
|||||||
@ -390,6 +390,10 @@ const TopicDetails: React.FC<TopicDetailsProps> = ({
|
|||||||
<EntityPageInfo
|
<EntityPageInfo
|
||||||
isTagEditable
|
isTagEditable
|
||||||
deleted={deleted}
|
deleted={deleted}
|
||||||
|
entityFieldTasks={getEntityFieldThreadCounts(
|
||||||
|
EntityField.TAGS,
|
||||||
|
entityFieldTaskCount
|
||||||
|
)}
|
||||||
entityFieldThreads={getEntityFieldThreadCounts(
|
entityFieldThreads={getEntityFieldThreadCounts(
|
||||||
EntityField.TAGS,
|
EntityField.TAGS,
|
||||||
entityFieldThreadCount
|
entityFieldThreadCount
|
||||||
|
|||||||
@ -13,13 +13,17 @@
|
|||||||
|
|
||||||
import { faExclamationCircle, faStar } from '@fortawesome/free-solid-svg-icons';
|
import { faExclamationCircle, faStar } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
import { Popover } from 'antd';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { cloneDeep, isEmpty, isUndefined } from 'lodash';
|
import { cloneDeep, isEmpty, isUndefined } from 'lodash';
|
||||||
import { EntityFieldThreads, EntityTags, ExtraInfo, TagOption } from 'Models';
|
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 { FQN_SEPARATOR_CHAR } from '../../../constants/char.constants';
|
||||||
import { FOLLOWERS_VIEW_CAP } from '../../../constants/constants';
|
import { FOLLOWERS_VIEW_CAP } from '../../../constants/constants';
|
||||||
import { SettledStatus } from '../../../enums/axios.enum';
|
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 { Operation } from '../../../generated/entity/policies/accessControl/rule';
|
||||||
import { EntityReference } from '../../../generated/type/entityReference';
|
import { EntityReference } from '../../../generated/type/entityReference';
|
||||||
import { LabelType, State, TagLabel } from '../../../generated/type/tagLabel';
|
import { LabelType, State, TagLabel } from '../../../generated/type/tagLabel';
|
||||||
@ -32,6 +36,7 @@ import {
|
|||||||
} from '../../../utils/GlossaryUtils';
|
} from '../../../utils/GlossaryUtils';
|
||||||
import SVGIcons, { Icons } from '../../../utils/SvgUtils';
|
import SVGIcons, { Icons } from '../../../utils/SvgUtils';
|
||||||
import { getTagCategories, getTaglist } from '../../../utils/TagsUtils';
|
import { getTagCategories, getTaglist } from '../../../utils/TagsUtils';
|
||||||
|
import { getRequestTagsPath, TASK_ENTITIES } from '../../../utils/TasksUtils';
|
||||||
import TagsContainer from '../../tags-container/tags-container';
|
import TagsContainer from '../../tags-container/tags-container';
|
||||||
import TagsViewer from '../../tags-viewer/tags-viewer';
|
import TagsViewer from '../../tags-viewer/tags-viewer';
|
||||||
import Tags from '../../tags/tags';
|
import Tags from '../../tags/tags';
|
||||||
@ -60,7 +65,8 @@ interface Props {
|
|||||||
version?: string;
|
version?: string;
|
||||||
isVersionSelected?: boolean;
|
isVersionSelected?: boolean;
|
||||||
entityFieldThreads?: EntityFieldThreads[];
|
entityFieldThreads?: EntityFieldThreads[];
|
||||||
onThreadLinkSelect?: (value: string) => void;
|
entityFieldTasks?: EntityFieldThreads[];
|
||||||
|
onThreadLinkSelect?: (value: string, threadType?: ThreadType) => void;
|
||||||
followHandler?: () => void;
|
followHandler?: () => void;
|
||||||
tagsHandler?: (selectedTags?: Array<EntityTags>) => void;
|
tagsHandler?: (selectedTags?: Array<EntityTags>) => void;
|
||||||
versionHandler?: () => void;
|
versionHandler?: () => void;
|
||||||
@ -88,8 +94,11 @@ const EntityPageInfo = ({
|
|||||||
onThreadLinkSelect,
|
onThreadLinkSelect,
|
||||||
entityFqn,
|
entityFqn,
|
||||||
entityType,
|
entityType,
|
||||||
|
entityFieldTasks,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
|
const history = useHistory();
|
||||||
const tagThread = entityFieldThreads?.[0];
|
const tagThread = entityFieldThreads?.[0];
|
||||||
|
const tagTask = entityFieldTasks?.[0];
|
||||||
const [isEditable, setIsEditable] = useState<boolean>(false);
|
const [isEditable, setIsEditable] = useState<boolean>(false);
|
||||||
const [entityFollowers, setEntityFollowers] =
|
const [entityFollowers, setEntityFollowers] =
|
||||||
useState<Array<EntityReference>>(followersList);
|
useState<Array<EntityReference>>(followersList);
|
||||||
@ -101,6 +110,10 @@ const EntityPageInfo = ({
|
|||||||
document.getElementById('version-and-follow-section')?.offsetWidth
|
document.getElementById('version-and-follow-section')?.offsetWidth
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleRequestTags = () => {
|
||||||
|
history.push(getRequestTagsPath(entityType as string, entityFqn as string));
|
||||||
|
};
|
||||||
|
|
||||||
const handleTagSelection = (selectedTags?: Array<EntityTags>) => {
|
const handleTagSelection = (selectedTags?: Array<EntityTags>) => {
|
||||||
if (selectedTags) {
|
if (selectedTags) {
|
||||||
const prevTags =
|
const prevTags =
|
||||||
@ -269,35 +282,74 @@ const EntityPageInfo = ({
|
|||||||
|
|
||||||
const getThreadElements = () => {
|
const getThreadElements = () => {
|
||||||
if (!isUndefined(entityFieldThreads)) {
|
if (!isUndefined(entityFieldThreads)) {
|
||||||
return !isUndefined(tagThread) ? (
|
return !isUndefined(tagThread) &&
|
||||||
<p
|
TASK_ENTITIES.includes(entityType as EntityType) ? (
|
||||||
className="link-text tw-m-0 tw-ml-1 tw-w-8 tw-flex-none"
|
<button
|
||||||
|
className="tw-w-8 tw-h-8 tw-mr-1 tw-flex-none link-text focus:tw-outline-none"
|
||||||
data-testid="tag-thread"
|
data-testid="tag-thread"
|
||||||
onClick={() => onThreadLinkSelect?.(tagThread.entityLink)}>
|
onClick={() => onThreadLinkSelect?.(tagThread.entityLink)}>
|
||||||
<span className="tw-flex">
|
<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">
|
<span className="tw-ml-1" data-testid="tag-thread-count">
|
||||||
{tagThread.count}
|
{tagThread.count}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<p
|
<button
|
||||||
className="link-text tw-self-start tw-w-8 tw-m-0 tw-ml-1 tw-flex-none"
|
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"
|
data-testid="start-tag-thread"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
onThreadLinkSelect?.(
|
onThreadLinkSelect?.(
|
||||||
getEntityFeedLink(entityType, entityFqn, 'tags')
|
getEntityFeedLink(entityType, entityFqn, 'tags')
|
||||||
)
|
)
|
||||||
}>
|
}>
|
||||||
<SVGIcons alt="comments" icon={Icons.COMMENT_PLUS} width="20px" />
|
<SVGIcons alt="comments" icon={Icons.COMMENT_PLUS} />
|
||||||
</p>
|
</button>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return null;
|
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(() => {
|
useEffect(() => {
|
||||||
setEntityFollowers(followersList);
|
setEntityFollowers(followersList);
|
||||||
}, [followersList]);
|
}, [followersList]);
|
||||||
@ -454,7 +506,7 @@ const EntityPageInfo = ({
|
|||||||
position="bottom"
|
position="bottom"
|
||||||
trigger="click">
|
trigger="click">
|
||||||
<div
|
<div
|
||||||
className="tw-inline-block"
|
className="tw-inline-block tw-mr-1"
|
||||||
data-testid="tags-wrapper"
|
data-testid="tags-wrapper"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
// Fetch tags and terms only once
|
// Fetch tags and terms only once
|
||||||
@ -479,14 +531,9 @@ const EntityPageInfo = ({
|
|||||||
}}>
|
}}>
|
||||||
{tags.length || tier ? (
|
{tags.length || tier ? (
|
||||||
<button
|
<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">
|
data-testid="edit-button">
|
||||||
<SVGIcons
|
<SVGIcons alt="edit" icon="icon-edit" title="Edit" />
|
||||||
alt="edit"
|
|
||||||
icon="icon-edit"
|
|
||||||
title="Edit"
|
|
||||||
width="16px"
|
|
||||||
/>
|
|
||||||
</button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<span>
|
<span>
|
||||||
@ -501,7 +548,11 @@ const EntityPageInfo = ({
|
|||||||
</TagsContainer>
|
</TagsContainer>
|
||||||
</div>
|
</div>
|
||||||
</NonAdminAction>
|
</NonAdminAction>
|
||||||
|
<div className="tw--mt-1.5">
|
||||||
|
{getRequestTagsElements()}
|
||||||
|
{getTaskElement()}
|
||||||
{getThreadElements()}
|
{getThreadElements()}
|
||||||
|
</div>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -209,7 +209,9 @@ export const ROUTES = {
|
|||||||
|
|
||||||
// Tasks Routes
|
// Tasks Routes
|
||||||
REQUEST_DESCRIPTION: `/request-description/${PLACEHOLDER_ROUTE_ENTITY_TYPE}/${PLACEHOLDER_ROUTE_ENTITY_FQN}`,
|
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_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}`,
|
TASK_DETAIL: `/tasks/${PLACEHOLDER_TASK_ID}`,
|
||||||
|
|
||||||
ACTIVITY_PUSH_FEED: '/api/v1/push/feed',
|
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 { faChevronDown } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
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 { AxiosError, AxiosResponse } from 'axios';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { compare, Operation } from 'fast-json-patch';
|
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 { observer } from 'mobx-react';
|
||||||
import { EditorContentRef, EntityReference, EntityTags } from 'Models';
|
import { EntityReference } from 'Models';
|
||||||
import React, { Fragment, useEffect, useMemo, useRef, useState } from 'react';
|
import React, { Fragment, useEffect, useMemo, useState } from 'react';
|
||||||
import { useHistory, useParams } from 'react-router-dom';
|
import { useHistory, useParams } from 'react-router-dom';
|
||||||
import AppState from '../../../AppState';
|
import AppState from '../../../AppState';
|
||||||
import { useAuthContext } from '../../../authentication/auth-provider/AuthProvider';
|
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 FeedPanelBody from '../../../components/ActivityFeed/ActivityFeedPanel/FeedPanelBody';
|
||||||
import ActivityThreadPanelBody from '../../../components/ActivityFeed/ActivityThreadPanel/ActivityThreadPanelBody';
|
import ActivityThreadPanelBody from '../../../components/ActivityFeed/ActivityThreadPanel/ActivityThreadPanelBody';
|
||||||
import AssigneeList from '../../../components/common/AssigneeList/AssigneeList';
|
import AssigneeList from '../../../components/common/AssigneeList/AssigneeList';
|
||||||
import Ellipses from '../../../components/common/Ellipses/Ellipses';
|
|
||||||
import ErrorPlaceHolder from '../../../components/common/error-with-placeholder/ErrorPlaceHolder';
|
import ErrorPlaceHolder from '../../../components/common/error-with-placeholder/ErrorPlaceHolder';
|
||||||
import UserPopOverCard from '../../../components/common/PopOverCard/UserPopOverCard';
|
import UserPopOverCard from '../../../components/common/PopOverCard/UserPopOverCard';
|
||||||
import ProfilePicture from '../../../components/common/ProfilePicture/ProfilePicture';
|
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 TitleBreadcrumb from '../../../components/common/title-breadcrumb/title-breadcrumb.component';
|
||||||
import { FQN_SEPARATOR_CHAR } from '../../../constants/char.constants';
|
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 { EntityType } from '../../../enums/entity.enum';
|
||||||
import { CreateThread } from '../../../generated/api/feed/createThread';
|
import { CreateThread } from '../../../generated/api/feed/createThread';
|
||||||
import { Column } from '../../../generated/entity/data/table';
|
|
||||||
import {
|
import {
|
||||||
TaskDetails,
|
TaskDetails,
|
||||||
TaskType,
|
TaskType,
|
||||||
@ -55,8 +52,8 @@ import {
|
|||||||
ThreadTaskStatus,
|
ThreadTaskStatus,
|
||||||
ThreadType,
|
ThreadType,
|
||||||
} from '../../../generated/entity/feed/thread';
|
} from '../../../generated/entity/feed/thread';
|
||||||
|
import { TagLabel } from '../../../generated/type/tagLabel';
|
||||||
import { useAuth } from '../../../hooks/authHooks';
|
import { useAuth } from '../../../hooks/authHooks';
|
||||||
import { getEntityName } from '../../../utils/CommonUtils';
|
|
||||||
import { ENTITY_LINK_SEPARATOR } from '../../../utils/EntityUtils';
|
import { ENTITY_LINK_SEPARATOR } from '../../../utils/EntityUtils';
|
||||||
import {
|
import {
|
||||||
deletePost,
|
deletePost,
|
||||||
@ -67,24 +64,26 @@ import {
|
|||||||
} from '../../../utils/FeedUtils';
|
} from '../../../utils/FeedUtils';
|
||||||
import { getEncodedFqn } from '../../../utils/StringsUtils';
|
import { getEncodedFqn } from '../../../utils/StringsUtils';
|
||||||
import SVGIcons from '../../../utils/SvgUtils';
|
import SVGIcons from '../../../utils/SvgUtils';
|
||||||
import {
|
import { getEntityLink } from '../../../utils/TableUtils';
|
||||||
getEntityLink,
|
|
||||||
getTagsWithoutTier,
|
|
||||||
getTierTags,
|
|
||||||
} from '../../../utils/TableUtils';
|
|
||||||
import {
|
import {
|
||||||
fetchEntityDetail,
|
fetchEntityDetail,
|
||||||
fetchOptions,
|
fetchOptions,
|
||||||
getBreadCrumbList,
|
getBreadCrumbList,
|
||||||
getColumnObject,
|
getColumnObject,
|
||||||
getDescriptionDiff,
|
isDescriptionTask,
|
||||||
|
isTagsTask,
|
||||||
TASK_ACTION_LIST,
|
TASK_ACTION_LIST,
|
||||||
} from '../../../utils/TasksUtils';
|
} from '../../../utils/TasksUtils';
|
||||||
import { getDayTimeByTimeStamp } from '../../../utils/TimeUtils';
|
import { getDayTimeByTimeStamp } from '../../../utils/TimeUtils';
|
||||||
import { showErrorToast, showSuccessToast } from '../../../utils/ToastUtils';
|
import { showErrorToast, showSuccessToast } from '../../../utils/ToastUtils';
|
||||||
import Assignees from '../shared/Assignees';
|
import Assignees from '../shared/Assignees';
|
||||||
import { DescriptionTabs } from '../shared/DescriptionTabs';
|
import ClosedTask from '../shared/ClosedTask';
|
||||||
import { DiffView } from '../shared/DiffView';
|
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 { background, cardStyles, contentStyles } from '../TaskPage.styles';
|
||||||
import {
|
import {
|
||||||
EntityData,
|
EntityData,
|
||||||
@ -102,8 +101,6 @@ const TaskDetailPage = () => {
|
|||||||
|
|
||||||
const { taskId } = useParams<{ [key: string]: string }>();
|
const { taskId } = useParams<{ [key: string]: string }>();
|
||||||
|
|
||||||
const markdownRef = useRef<EditorContentRef>();
|
|
||||||
|
|
||||||
const [taskDetail, setTaskDetail] = useState<Thread>({} as Thread);
|
const [taskDetail, setTaskDetail] = useState<Thread>({} as Thread);
|
||||||
const [taskFeedDetail, setTaskFeedDetail] = useState<Thread>({} as Thread);
|
const [taskFeedDetail, setTaskFeedDetail] = useState<Thread>({} as Thread);
|
||||||
const [entityData, setEntityData] = useState<EntityData>({} as EntityData);
|
const [entityData, setEntityData] = useState<EntityData>({} as EntityData);
|
||||||
@ -116,6 +113,7 @@ const TaskDetailPage = () => {
|
|||||||
const [taskAction, setTaskAction] = useState<TaskAction>(TASK_ACTION_LIST[0]);
|
const [taskAction, setTaskAction] = useState<TaskAction>(TASK_ACTION_LIST[0]);
|
||||||
const [modalVisible, setModalVisible] = useState<boolean>(false);
|
const [modalVisible, setModalVisible] = useState<boolean>(false);
|
||||||
const [comment, setComment] = useState<string>('');
|
const [comment, setComment] = useState<string>('');
|
||||||
|
const [tagsSuggestion, setTagsSuggestion] = useState<TagLabel[]>([]);
|
||||||
|
|
||||||
// get current user details
|
// get current user details
|
||||||
const currentUser = useMemo(
|
const currentUser = useMemo(
|
||||||
@ -123,18 +121,6 @@ const TaskDetailPage = () => {
|
|||||||
[AppState.userDetails, AppState.nonSecureUserDetails]
|
[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(() => {
|
const entityType = useMemo(() => {
|
||||||
return getEntityType(taskDetail.about);
|
return getEntityType(taskDetail.about);
|
||||||
}, [taskDetail]);
|
}, [taskDetail]);
|
||||||
@ -144,24 +130,25 @@ const TaskDetailPage = () => {
|
|||||||
}, [taskDetail]);
|
}, [taskDetail]);
|
||||||
|
|
||||||
const columnObject = useMemo(() => {
|
const columnObject = useMemo(() => {
|
||||||
|
// prepare column from entityField
|
||||||
const column = entityField?.split(ENTITY_LINK_SEPARATOR)?.slice(-2)?.[0];
|
const column = entityField?.split(ENTITY_LINK_SEPARATOR)?.slice(-2)?.[0];
|
||||||
|
|
||||||
|
// prepare column value by replacing double quotes
|
||||||
const columnValue = column?.replaceAll(/^"|"$/g, '') || '';
|
const columnValue = column?.replaceAll(/^"|"$/g, '') || '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get column name by spliting columnValue with FQN Separator
|
||||||
|
*/
|
||||||
const columnName = columnValue.split(FQN_SEPARATOR_CHAR).pop();
|
const columnName = columnValue.split(FQN_SEPARATOR_CHAR).pop();
|
||||||
|
|
||||||
return getColumnObject(columnName as string, entityData.columns || []);
|
return getColumnObject(columnName as string, entityData.columns || []);
|
||||||
}, [taskDetail, entityData]);
|
}, [taskDetail, entityData]);
|
||||||
|
|
||||||
const isRequestDescription = isEqual(
|
// const isRequestTag = isEqual(taskDetail.task?.type, TaskType.RequestTag);
|
||||||
taskDetail.task?.type,
|
// const isUpdateTag = isEqual(taskDetail.task?.type, TaskType.UpdateTag);
|
||||||
TaskType.RequestDescription
|
|
||||||
);
|
|
||||||
|
|
||||||
const isUpdateDescription = isEqual(
|
|
||||||
taskDetail.task?.type,
|
|
||||||
TaskType.UpdateDescription
|
|
||||||
);
|
|
||||||
|
|
||||||
const isOwner = isEqual(entityData.owner?.id, currentUser?.id);
|
const isOwner = isEqual(entityData.owner?.id, currentUser?.id);
|
||||||
|
|
||||||
const isAssignee = taskDetail.task?.assignees?.some((assignee) =>
|
const isAssignee = taskDetail.task?.assignees?.some((assignee) =>
|
||||||
isEqual(assignee.id, currentUser?.id)
|
isEqual(assignee.id, currentUser?.id)
|
||||||
);
|
);
|
||||||
@ -175,6 +162,12 @@ const TaskDetailPage = () => {
|
|||||||
|
|
||||||
const isTaskActionEdit = isEqual(taskAction.key, TaskActionMode.EDIT);
|
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 = () => {
|
const fetchTaskDetail = () => {
|
||||||
getTask(taskId)
|
getTask(taskId)
|
||||||
.then((res: AxiosResponse) => {
|
.then((res: AxiosResponse) => {
|
||||||
@ -305,8 +298,7 @@ const TaskDetailPage = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onTaskResolve = () => {
|
const onTaskResolve = () => {
|
||||||
if (suggestion) {
|
const updateTaskData = (data: Record<string, string>) => {
|
||||||
const data = { newValue: suggestion };
|
|
||||||
updateTask(TaskOperation.RESOLVE, taskDetail.task?.id, data)
|
updateTask(TaskOperation.RESOLVE, taskDetail.task?.id, data)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
showSuccessToast('Task Resolved Successfully');
|
showSuccessToast('Task Resolved Successfully');
|
||||||
@ -318,11 +310,25 @@ const TaskDetailPage = () => {
|
|||||||
);
|
);
|
||||||
})
|
})
|
||||||
.catch((err: AxiosError) => showErrorToast(err));
|
.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 {
|
||||||
|
if (suggestion) {
|
||||||
|
const data = { newValue: suggestion };
|
||||||
|
updateTaskData(data);
|
||||||
} else {
|
} else {
|
||||||
showErrorToast(
|
showErrorToast(
|
||||||
'Cannot accept an empty description. Please add a description.'
|
'Cannot accept an empty description. Please add a description.'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onTaskReject = () => {
|
const onTaskReject = () => {
|
||||||
@ -381,6 +387,7 @@ const TaskDetailPage = () => {
|
|||||||
updateThreadData(threadId, postId, isThread, data, callback);
|
updateThreadData(threadId, postId, isThread, data, callback);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// prepare current description for update description task
|
||||||
const currentDescription = () => {
|
const currentDescription = () => {
|
||||||
if (entityField && !isEmpty(columnObject)) {
|
if (entityField && !isEmpty(columnObject)) {
|
||||||
return columnObject.description || '';
|
return columnObject.description || '';
|
||||||
@ -389,24 +396,47 @@ const TaskDetailPage = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// handle assignees search
|
||||||
const onSearch = (query: string) => {
|
const onSearch = (query: string) => {
|
||||||
fetchOptions(query, setOptions);
|
fetchOptions(query, setOptions);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// handle sider tab change
|
||||||
const onTabChange = (key: string) => {
|
const onTabChange = (key: string) => {
|
||||||
if (isEqual(key, '1')) {
|
if (isEqual(key, PanelTab.TASKS)) {
|
||||||
fetchTaskFeed(taskDetail.id);
|
fetchTaskFeed(taskDetail.id);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// handle task action change
|
||||||
const onTaskActionChange = (key: string) => {
|
const onTaskActionChange = (key: string) => {
|
||||||
setTaskAction(
|
setTaskAction(
|
||||||
TASK_ACTION_LIST.find((action) => isEqual(action.key, key)) as TaskAction
|
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 = () => {
|
const onTaskDetailChange = () => {
|
||||||
if (!isEmpty(taskDetail)) {
|
if (!isEmpty(taskDetail)) {
|
||||||
|
// get entityFQN and fetch entity data
|
||||||
const entityFQN = getEntityFQN(taskDetail.about);
|
const entityFQN = getEntityFQN(taskDetail.about);
|
||||||
|
|
||||||
entityFQN &&
|
entityFQN &&
|
||||||
@ -417,8 +447,8 @@ const TaskDetailPage = () => {
|
|||||||
);
|
);
|
||||||
fetchTaskFeed(taskDetail.id);
|
fetchTaskFeed(taskDetail.id);
|
||||||
|
|
||||||
|
// set task assignees
|
||||||
const taskAssignees = taskDetail.task?.assignees || [];
|
const taskAssignees = taskDetail.task?.assignees || [];
|
||||||
const taskSuggestion = taskDetail.task?.suggestion;
|
|
||||||
if (taskAssignees.length) {
|
if (taskAssignees.length) {
|
||||||
const assigneesArr = taskAssignees.map((assignee) => ({
|
const assigneesArr = taskAssignees.map((assignee) => ({
|
||||||
label: assignee.name as string,
|
label: assignee.name as string,
|
||||||
@ -428,10 +458,9 @@ const TaskDetailPage = () => {
|
|||||||
setAssignees(assigneesArr);
|
setAssignees(assigneesArr);
|
||||||
setOptions(assigneesArr);
|
setOptions(assigneesArr);
|
||||||
}
|
}
|
||||||
if (!taskSuggestion) {
|
|
||||||
setTaskAction(TASK_ACTION_LIST[1]);
|
// set task suggestion on render
|
||||||
}
|
setTaskSuggestionOnRender(taskDetail.task?.suggestion);
|
||||||
setSuggestion(taskSuggestion || '');
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -447,154 +476,18 @@ const TaskDetailPage = () => {
|
|||||||
onTaskDetailChange();
|
onTaskDetailChange();
|
||||||
}, [taskDetail]);
|
}, [taskDetail]);
|
||||||
|
|
||||||
const TaskStatusElement = ({ status }: { status: ThreadTaskStatus }) => {
|
// handle comment modal close
|
||||||
const openCheck = isEqual(status, ThreadTaskStatus.Open);
|
const onCommentModalClose = () => {
|
||||||
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 = () => {
|
|
||||||
setModalVisible(false);
|
setModalVisible(false);
|
||||||
setComment('');
|
setComment('');
|
||||||
};
|
};
|
||||||
|
|
||||||
const hasEditAccess = () => {
|
/**
|
||||||
return isAdminUser || isAuthDisabled || isAssignee || isOwner;
|
*
|
||||||
};
|
* @returns True if has access otherwise false
|
||||||
|
*/
|
||||||
|
const hasEditAccess = () =>
|
||||||
|
isAdminUser || isAuthDisabled || isAssignee || isOwner;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout style={{ ...background, height: '100vh' }}>
|
<Layout style={{ ...background, height: '100vh' }}>
|
||||||
@ -613,7 +506,7 @@ const TaskDetailPage = () => {
|
|||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<EntityDetail />
|
<EntityDetail entityData={entityData} />
|
||||||
|
|
||||||
<Card
|
<Card
|
||||||
data-testid="task-metadata"
|
data-testid="task-metadata"
|
||||||
@ -623,8 +516,8 @@ const TaskDetailPage = () => {
|
|||||||
data-testid="task-title">
|
data-testid="task-title">
|
||||||
{`Task #${taskId}`} {taskDetail.message}
|
{`Task #${taskId}`} {taskDetail.message}
|
||||||
</p>
|
</p>
|
||||||
<p className="tw-flex tw-mb-4" data-testid="task-metadata">
|
<div className="tw-flex tw-mb-4" data-testid="task-metadata">
|
||||||
<TaskStatusElement
|
<TaskStatus
|
||||||
status={taskDetail.task?.status as ThreadTaskStatus}
|
status={taskDetail.task?.status as ThreadTaskStatus}
|
||||||
/>
|
/>
|
||||||
<span className="tw-mx-1.5 tw-inline-block tw-text-gray-400">
|
<span className="tw-mx-1.5 tw-inline-block tw-text-gray-400">
|
||||||
@ -651,7 +544,7 @@ const TaskDetailPage = () => {
|
|||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</div>
|
||||||
|
|
||||||
<ColumnDetail column={columnObject} />
|
<ColumnDetail column={columnObject} />
|
||||||
<div className="tw-flex" data-testid="task-assignees">
|
<div className="tw-flex" data-testid="task-assignees">
|
||||||
@ -718,54 +611,24 @@ const TaskDetailPage = () => {
|
|||||||
<Card
|
<Card
|
||||||
data-testid="task-data"
|
data-testid="task-data"
|
||||||
style={{ ...cardStyles, marginTop: '16px', marginLeft: '24px' }}>
|
style={{ ...cardStyles, marginTop: '16px', marginLeft: '24px' }}>
|
||||||
<div data-testid="task-description-tabs">
|
{isTaskDescription && (
|
||||||
<p className="tw-text-grey-muted">Description:</p>{' '}
|
<DescriptionTask
|
||||||
{!isEmpty(taskDetail) && (
|
currentDescription={currentDescription()}
|
||||||
<Fragment>
|
hasEditAccess={hasEditAccess()}
|
||||||
{isTaskClosed ? (
|
isTaskActionEdit={isTaskActionEdit}
|
||||||
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}
|
suggestion={suggestion}
|
||||||
onChange={onSuggestionChange}
|
taskDetail={taskDetail}
|
||||||
|
onSuggestionChange={onSuggestionChange}
|
||||||
/>
|
/>
|
||||||
) : (
|
|
||||||
<div className="tw-flex tw-border tw-border-main tw-rounded tw-mb-4">
|
|
||||||
{getCurrentDescription()}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Fragment>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
{isTaskTags && (
|
||||||
|
<TagsTask
|
||||||
|
isTaskActionEdit={isTaskActionEdit}
|
||||||
|
setSuggestion={setTagsSuggestion}
|
||||||
|
suggestions={tagsSuggestion}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{hasEditAccess() && !isTaskClosed && (
|
{hasEditAccess() && !isTaskClosed && (
|
||||||
<div
|
<div
|
||||||
className="tw-flex tw-justify-end"
|
className="tw-flex tw-justify-end"
|
||||||
@ -811,57 +674,16 @@ const TaskDetailPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isTaskClosed && (
|
{isTaskClosed && <ClosedTask task={taskDetail.task} />}
|
||||||
<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>
|
|
||||||
)}
|
|
||||||
</Card>
|
</Card>
|
||||||
<Modal
|
<CommentModal
|
||||||
centered
|
comment={comment}
|
||||||
destroyOnClose
|
isVisible={modalVisible}
|
||||||
cancelButtonProps={{
|
setComment={setComment}
|
||||||
type: 'link',
|
taskDetail={taskDetail}
|
||||||
className: 'ant-btn-link-custom',
|
onClose={onCommentModalClose}
|
||||||
}}
|
onConfirm={onTaskReject}
|
||||||
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>
|
|
||||||
</Content>
|
</Content>
|
||||||
|
|
||||||
<Sider
|
<Sider
|
||||||
@ -869,7 +691,7 @@ const TaskDetailPage = () => {
|
|||||||
data-testid="task-right-sider"
|
data-testid="task-right-sider"
|
||||||
width={600}>
|
width={600}>
|
||||||
<Tabs className="ant-tabs-custom-line" onChange={onTabChange}>
|
<Tabs className="ant-tabs-custom-line" onChange={onTabChange}>
|
||||||
<TabPane key="1" tab="Task">
|
<TabPane key={PanelTab.TASKS} tab="Task">
|
||||||
{!isEmpty(taskFeedDetail) ? (
|
{!isEmpty(taskFeedDetail) ? (
|
||||||
<div id="task-feed">
|
<div id="task-feed">
|
||||||
<FeedPanelBody
|
<FeedPanelBody
|
||||||
@ -886,7 +708,7 @@ const TaskDetailPage = () => {
|
|||||||
) : null}
|
) : null}
|
||||||
</TabPane>
|
</TabPane>
|
||||||
|
|
||||||
<TabPane key="2" tab="Conversations">
|
<TabPane key={PanelTab.CONVERSATIONS} tab="Conversations">
|
||||||
{!isEmpty(taskFeedDetail) ? (
|
{!isEmpty(taskFeedDetail) ? (
|
||||||
<ActivityThreadPanelBody
|
<ActivityThreadPanelBody
|
||||||
className="tw-p-0"
|
className="tw-p-0"
|
||||||
|
|||||||
@ -38,7 +38,7 @@ const Assignees: FC<Props> = ({ assignees, onSearch, onChange, options }) => {
|
|||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
showSearch
|
showSearch
|
||||||
className="ant-select-assignee"
|
className="ant-select-custom"
|
||||||
data-testid="select-assignee"
|
data-testid="select-assignee"
|
||||||
defaultActiveFirstOption={false}
|
defaultActiveFirstOption={false}
|
||||||
filterOption={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(
|
const UpdateDescriptionPage = withSuspenseFallback(
|
||||||
React.lazy(
|
React.lazy(
|
||||||
() =>
|
() =>
|
||||||
@ -332,6 +336,7 @@ const AuthenticatedAppRouter: FunctionComponent = () => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Route exact component={TaskDetailPage} path={ROUTES.TASK_DETAIL} />
|
<Route exact component={TaskDetailPage} path={ROUTES.TASK_DETAIL} />
|
||||||
|
<Route exact component={RequestTagsPage} path={ROUTES.REQUEST_TAGS} />
|
||||||
|
|
||||||
<Redirect to={ROUTES.NOT_FOUND} />
|
<Redirect to={ROUTES.NOT_FOUND} />
|
||||||
</Switch>
|
</Switch>
|
||||||
|
|||||||
@ -1183,12 +1183,12 @@ code {
|
|||||||
}
|
}
|
||||||
/* Antd custom button CSS */
|
/* Antd custom button CSS */
|
||||||
|
|
||||||
.ant-select-assignee {
|
.ant-select-custom {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 8px 0px;
|
margin: 8px 0px;
|
||||||
outline: none;
|
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;
|
border-color: #7147e8;
|
||||||
}
|
}
|
||||||
.ant-select-focused {
|
.ant-select-focused {
|
||||||
|
|||||||
@ -31,6 +31,7 @@ import {
|
|||||||
import { EntityType, FqnPart } from '../enums/entity.enum';
|
import { EntityType, FqnPart } from '../enums/entity.enum';
|
||||||
import { ServiceCategory } from '../enums/service.enum';
|
import { ServiceCategory } from '../enums/service.enum';
|
||||||
import { Column, Table } from '../generated/entity/data/table';
|
import { Column, Table } from '../generated/entity/data/table';
|
||||||
|
import { TaskType } from '../generated/entity/feed/thread';
|
||||||
import { EntityReference } from '../generated/type/entityReference';
|
import { EntityReference } from '../generated/type/entityReference';
|
||||||
import {
|
import {
|
||||||
EntityData,
|
EntityData,
|
||||||
@ -65,6 +66,26 @@ export const getRequestDescriptionPath = (
|
|||||||
return { pathname, search: searchParams.toString() };
|
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 = (
|
export const getUpdateDescriptionPath = (
|
||||||
entityType: string,
|
entityType: string,
|
||||||
entityFQN: string,
|
entityFQN: string,
|
||||||
@ -263,3 +284,9 @@ export const TASK_ACTION_LIST = [
|
|||||||
key: TaskActionMode.EDIT,
|
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