mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-11-12 00:41:32 +00:00
Task improvements (#12361)
* chore(#6741): use antlr parser for fqn and entityLink * chore: use this for class context * chore: address comments * chore: convert js to ts * fix: typescript errors * fix: entity link split method issue * fix: split listener * chore(ui): task UI improvements * feat: add support for edit task assignees * fix: validation issue * minor improvements * chore: address comments * chore: show dropdown button on task action button --------- Co-authored-by: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com>
This commit is contained in:
parent
e200a42926
commit
a47e80baf0
@ -27,6 +27,7 @@ interface ActivityFeedListV1Props {
|
|||||||
onFeedClick?: (feed: Thread) => void;
|
onFeedClick?: (feed: Thread) => void;
|
||||||
activeFeedId?: string;
|
activeFeedId?: string;
|
||||||
hidePopover: boolean;
|
hidePopover: boolean;
|
||||||
|
isForFeedTab?: boolean;
|
||||||
emptyPlaceholderText: string;
|
emptyPlaceholderText: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -37,6 +38,7 @@ const ActivityFeedListV1 = ({
|
|||||||
onFeedClick,
|
onFeedClick,
|
||||||
activeFeedId,
|
activeFeedId,
|
||||||
hidePopover = false,
|
hidePopover = false,
|
||||||
|
isForFeedTab = false,
|
||||||
emptyPlaceholderText,
|
emptyPlaceholderText,
|
||||||
}: ActivityFeedListV1Props) => {
|
}: ActivityFeedListV1Props) => {
|
||||||
const [entityThread, setEntityThread] = useState<Thread[]>([]);
|
const [entityThread, setEntityThread] = useState<Thread[]>([]);
|
||||||
@ -76,6 +78,7 @@ const ActivityFeedListV1 = ({
|
|||||||
feed={feed}
|
feed={feed}
|
||||||
hidePopover={hidePopover}
|
hidePopover={hidePopover}
|
||||||
isActive={activeFeedId === feed.id}
|
isActive={activeFeedId === feed.id}
|
||||||
|
isForFeedTab={isForFeedTab}
|
||||||
key={feed.id}
|
key={feed.id}
|
||||||
showThread={showThread}
|
showThread={showThread}
|
||||||
onFeedClick={onFeedClick}
|
onFeedClick={onFeedClick}
|
||||||
|
|||||||
@ -26,6 +26,7 @@ interface FeedPanelBodyPropV1 {
|
|||||||
isOpenInDrawer?: boolean;
|
isOpenInDrawer?: boolean;
|
||||||
onFeedClick?: (feed: Thread) => void;
|
onFeedClick?: (feed: Thread) => void;
|
||||||
isActive?: boolean;
|
isActive?: boolean;
|
||||||
|
isForFeedTab?: boolean;
|
||||||
hidePopover: boolean;
|
hidePopover: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -37,6 +38,7 @@ const FeedPanelBodyV1: FC<FeedPanelBodyPropV1> = ({
|
|||||||
onFeedClick,
|
onFeedClick,
|
||||||
isActive,
|
isActive,
|
||||||
hidePopover = false,
|
hidePopover = false,
|
||||||
|
isForFeedTab = false,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const mainFeed = {
|
const mainFeed = {
|
||||||
@ -64,6 +66,7 @@ const FeedPanelBodyV1: FC<FeedPanelBodyPropV1> = ({
|
|||||||
feed={feed}
|
feed={feed}
|
||||||
hidePopover={hidePopover}
|
hidePopover={hidePopover}
|
||||||
isActive={isActive}
|
isActive={isActive}
|
||||||
|
isForFeedTab={isForFeedTab}
|
||||||
isOpenInDrawer={isOpenInDrawer}
|
isOpenInDrawer={isOpenInDrawer}
|
||||||
key={feed.id}
|
key={feed.id}
|
||||||
post={mainFeed}
|
post={mainFeed}
|
||||||
|
|||||||
@ -350,6 +350,7 @@ export const ActivityFeedTab = ({
|
|||||||
)}
|
)}
|
||||||
<ActivityFeedListV1
|
<ActivityFeedListV1
|
||||||
hidePopover
|
hidePopover
|
||||||
|
isForFeedTab
|
||||||
activeFeedId={selectedThread?.id}
|
activeFeedId={selectedThread?.id}
|
||||||
emptyPlaceholderText={placeholderText}
|
emptyPlaceholderText={placeholderText}
|
||||||
feedList={threads}
|
feedList={threads}
|
||||||
@ -383,6 +384,7 @@ export const ActivityFeedTab = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<FeedPanelBodyV1
|
<FeedPanelBodyV1
|
||||||
|
isForFeedTab
|
||||||
isOpenInDrawer
|
isOpenInDrawer
|
||||||
showThread
|
showThread
|
||||||
feed={selectedThread}
|
feed={selectedThread}
|
||||||
|
|||||||
@ -11,24 +11,26 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
import Icon from '@ant-design/icons';
|
import Icon from '@ant-design/icons';
|
||||||
import { Col, Row, Tooltip, Typography } from 'antd';
|
import { Button, Col, Row, Tooltip, Typography } from 'antd';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import AssigneeList from 'components/common/AssigneeList/AssigneeList';
|
import AssigneeList from 'components/common/AssigneeList/AssigneeList';
|
||||||
import EntityPopOverCard from 'components/common/PopOverCard/EntityPopOverCard';
|
import EntityPopOverCard from 'components/common/PopOverCard/EntityPopOverCard';
|
||||||
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 { Post, Thread, ThreadTaskStatus } from 'generated/entity/feed/thread';
|
import { Post, Thread, ThreadTaskStatus } from 'generated/entity/feed/thread';
|
||||||
import { isUndefined, noop } from 'lodash';
|
import { isEmpty, isUndefined, noop } from 'lodash';
|
||||||
import React, { useState } from 'react';
|
import React, { useMemo, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link, useHistory } from 'react-router-dom';
|
||||||
import { getNameFromFQN } from 'utils/CommonUtils';
|
import { getNameFromFQN } from 'utils/CommonUtils';
|
||||||
|
import EntityLink from 'utils/EntityLink';
|
||||||
import {
|
import {
|
||||||
getEntityFieldDisplay,
|
getEntityFieldDisplay,
|
||||||
getEntityFQN,
|
getEntityFQN,
|
||||||
getEntityType,
|
getEntityType,
|
||||||
prepareFeedLink,
|
prepareFeedLink,
|
||||||
} from 'utils/FeedUtils';
|
} from 'utils/FeedUtils';
|
||||||
|
import { getTaskDetailPath } from 'utils/TasksUtils';
|
||||||
import {
|
import {
|
||||||
getDateTimeFromMilliSeconds,
|
getDateTimeFromMilliSeconds,
|
||||||
getDayTimeByTimeStamp,
|
getDayTimeByTimeStamp,
|
||||||
@ -48,6 +50,7 @@ interface TaskFeedCardProps {
|
|||||||
isEntityFeed?: boolean;
|
isEntityFeed?: boolean;
|
||||||
isOpenInDrawer?: boolean;
|
isOpenInDrawer?: boolean;
|
||||||
isActive?: boolean;
|
isActive?: boolean;
|
||||||
|
isForFeedTab?: boolean;
|
||||||
hidePopover: boolean;
|
hidePopover: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -59,19 +62,33 @@ const TaskFeedCard = ({
|
|||||||
showThread = true,
|
showThread = true,
|
||||||
isActive,
|
isActive,
|
||||||
hidePopover = false,
|
hidePopover = false,
|
||||||
|
isForFeedTab = false,
|
||||||
}: TaskFeedCardProps) => {
|
}: TaskFeedCardProps) => {
|
||||||
|
const history = useHistory();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const timeStamp = feed.threadTs;
|
const timeStamp = feed.threadTs;
|
||||||
const taskDetails = feed.task;
|
const taskDetails = feed.task;
|
||||||
const postLength = feed?.postsCount ?? 0;
|
const postLength = feed?.postsCount ?? 0;
|
||||||
const entityType = getEntityType(feed.about) ?? '';
|
const entityType = getEntityType(feed.about) ?? '';
|
||||||
const entityFQN = getEntityFQN(feed.about) ?? '';
|
const entityFQN = getEntityFQN(feed.about) ?? '';
|
||||||
|
|
||||||
const entityCheck = !isUndefined(entityFQN) && !isUndefined(entityType);
|
const entityCheck = !isUndefined(entityFQN) && !isUndefined(entityType);
|
||||||
const [isEditPost, setIsEditPost] = useState(false);
|
const [isEditPost, setIsEditPost] = useState(false);
|
||||||
const repliedUsers = [...new Set((feed?.posts ?? []).map((f) => f.from))];
|
const repliedUsers = [...new Set((feed?.posts ?? []).map((f) => f.from))];
|
||||||
const repliedUniqueUsersList = repliedUsers.slice(0, postLength >= 3 ? 2 : 1);
|
const repliedUniqueUsersList = repliedUsers.slice(0, postLength >= 3 ? 2 : 1);
|
||||||
|
|
||||||
const { showDrawer } = useActivityFeedProvider();
|
const { showDrawer, setActiveThread } = useActivityFeedProvider();
|
||||||
|
|
||||||
|
const taskField = useMemo(() => {
|
||||||
|
const entityField = EntityLink.getEntityField(feed.about) ?? '';
|
||||||
|
const columnName = EntityLink.getTableColumnName(feed.about) ?? '';
|
||||||
|
|
||||||
|
if (columnName) {
|
||||||
|
return `${entityField}/${columnName}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return entityField;
|
||||||
|
}, [feed]);
|
||||||
|
|
||||||
const showReplies = () => {
|
const showReplies = () => {
|
||||||
showDrawer?.(feed);
|
showDrawer?.(feed);
|
||||||
@ -81,17 +98,29 @@ const TaskFeedCard = ({
|
|||||||
setIsEditPost(!isEditPost);
|
setIsEditPost(!isEditPost);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleTaskLinkClick = () => {
|
||||||
|
history.push({
|
||||||
|
pathname: getTaskDetailPath(feed),
|
||||||
|
});
|
||||||
|
setActiveThread(feed);
|
||||||
|
};
|
||||||
|
|
||||||
const getTaskLinkElement = entityCheck && (
|
const getTaskLinkElement = entityCheck && (
|
||||||
<Typography.Text>
|
<Typography.Text>
|
||||||
<span>{`#${taskDetails?.id} `}</span>
|
<Button
|
||||||
|
className="p-0"
|
||||||
|
type="link"
|
||||||
|
onClick={handleTaskLinkClick}>{`#${taskDetails?.id} `}</Button>
|
||||||
|
|
||||||
<Typography.Text>{taskDetails?.type}</Typography.Text>
|
<Typography.Text className="p-l-xss">{taskDetails?.type}</Typography.Text>
|
||||||
<span className="m-x-xss">{t('label.for-lowercase')}</span>
|
<span className="m-x-xss">{t('label.for-lowercase')}</span>
|
||||||
{isEntityFeed ? (
|
{isEntityFeed ? (
|
||||||
<span className="tw-heading" data-testid="headerText-entityField">
|
<span className="tw-heading" data-testid="headerText-entityField">
|
||||||
{getEntityFieldDisplay(feed.about)}
|
{getEntityFieldDisplay(feed.about)}
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
|
<>
|
||||||
|
{isForFeedTab ? null : (
|
||||||
<>
|
<>
|
||||||
<span className="p-r-xss">{entityType}</span>
|
<span className="p-r-xss">{entityType}</span>
|
||||||
<EntityPopOverCard entityFQN={entityFQN} entityType={entityType}>
|
<EntityPopOverCard entityFQN={entityFQN} entityType={entityType}>
|
||||||
@ -105,6 +134,14 @@ const TaskFeedCard = ({
|
|||||||
</EntityPopOverCard>
|
</EntityPopOverCard>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{!isEmpty(taskField) ? (
|
||||||
|
<span className={classNames({ 'p-l-xss': !isForFeedTab })}>
|
||||||
|
{taskField}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -12,6 +12,7 @@
|
|||||||
*/
|
*/
|
||||||
import { CheckOutlined, CloseOutlined } from '@ant-design/icons';
|
import { CheckOutlined, CloseOutlined } from '@ant-design/icons';
|
||||||
import { Button, Space } from 'antd';
|
import { Button, Space } from 'antd';
|
||||||
|
import classNames from 'classnames';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { InlineEditProps } from './InlineEdit.interface';
|
import { InlineEditProps } from './InlineEdit.interface';
|
||||||
|
|
||||||
@ -20,10 +21,11 @@ const InlineEdit = ({
|
|||||||
onCancel,
|
onCancel,
|
||||||
onSave,
|
onSave,
|
||||||
direction,
|
direction,
|
||||||
|
className,
|
||||||
}: InlineEditProps) => {
|
}: InlineEditProps) => {
|
||||||
return (
|
return (
|
||||||
<Space
|
<Space
|
||||||
className="w-full"
|
className={classNames(className, 'w-full')}
|
||||||
data-testid="inline-edit-container"
|
data-testid="inline-edit-container"
|
||||||
direction={direction}>
|
direction={direction}>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import { SpaceProps } from 'antd';
|
|||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
|
|
||||||
export interface InlineEditProps {
|
export interface InlineEditProps {
|
||||||
|
className?: string;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
// onSave it can be API call or normal function
|
// onSave it can be API call or normal function
|
||||||
|
|||||||
@ -10,7 +10,7 @@
|
|||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
import Icon from '@ant-design/icons';
|
import Icon, { DownOutlined } from '@ant-design/icons';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Col,
|
Col,
|
||||||
@ -21,40 +21,51 @@ import {
|
|||||||
Space,
|
Space,
|
||||||
Typography,
|
Typography,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
|
import { useForm } from 'antd/lib/form/Form';
|
||||||
import Modal from 'antd/lib/modal/Modal';
|
import Modal from 'antd/lib/modal/Modal';
|
||||||
import AppState from 'AppState';
|
import AppState from 'AppState';
|
||||||
|
import { ReactComponent as EditIcon } from 'assets/svg/edit-new.svg';
|
||||||
import { AxiosError } from 'axios';
|
import { AxiosError } from 'axios';
|
||||||
|
import classNames from 'classnames';
|
||||||
import ActivityFeedCardV1 from 'components/ActivityFeed/ActivityFeedCard/ActivityFeedCardV1';
|
import ActivityFeedCardV1 from 'components/ActivityFeed/ActivityFeedCard/ActivityFeedCardV1';
|
||||||
import ActivityFeedEditor from 'components/ActivityFeed/ActivityFeedEditor/ActivityFeedEditor';
|
import ActivityFeedEditor from 'components/ActivityFeed/ActivityFeedEditor/ActivityFeedEditor';
|
||||||
import { useActivityFeedProvider } from 'components/ActivityFeed/ActivityFeedProvider/ActivityFeedProvider';
|
import { useActivityFeedProvider } from 'components/ActivityFeed/ActivityFeedProvider/ActivityFeedProvider';
|
||||||
|
import AssigneeList from 'components/common/AssigneeList/AssigneeList';
|
||||||
import { OwnerLabel } from 'components/common/OwnerLabel/OwnerLabel.component';
|
import { OwnerLabel } from 'components/common/OwnerLabel/OwnerLabel.component';
|
||||||
import EntityPopOverCard from 'components/common/PopOverCard/EntityPopOverCard';
|
import InlineEdit from 'components/InlineEdit/InlineEdit.component';
|
||||||
|
import { DE_ACTIVE_COLOR } from 'constants/constants';
|
||||||
import { TaskOperation } from 'constants/Feeds.constants';
|
import { TaskOperation } from 'constants/Feeds.constants';
|
||||||
|
import { compare } from 'fast-json-patch';
|
||||||
import { TaskType } from 'generated/api/feed/createThread';
|
import { TaskType } from 'generated/api/feed/createThread';
|
||||||
import { TaskDetails, ThreadTaskStatus } from 'generated/entity/feed/thread';
|
import { TaskDetails, ThreadTaskStatus } from 'generated/entity/feed/thread';
|
||||||
import { TagLabel } from 'generated/type/tagLabel';
|
import { TagLabel } from 'generated/type/tagLabel';
|
||||||
import { useAuth } from 'hooks/authHooks';
|
import { useAuth } from 'hooks/authHooks';
|
||||||
import { isEmpty, isEqual, isUndefined, noop } from 'lodash';
|
import { isEmpty, isEqual, isUndefined, noop } from 'lodash';
|
||||||
|
import Assignees from 'pages/TasksPage/shared/Assignees';
|
||||||
import DescriptionTask from 'pages/TasksPage/shared/DescriptionTask';
|
import DescriptionTask from 'pages/TasksPage/shared/DescriptionTask';
|
||||||
import TagsTask from 'pages/TasksPage/shared/TagsTask';
|
import TagsTask from 'pages/TasksPage/shared/TagsTask';
|
||||||
import {
|
import {
|
||||||
|
Option,
|
||||||
TaskAction,
|
TaskAction,
|
||||||
TaskActionMode,
|
TaskActionMode,
|
||||||
} from 'pages/TasksPage/TasksPage.interface';
|
} from 'pages/TasksPage/TasksPage.interface';
|
||||||
import { MenuInfo } from 'rc-menu/lib/interface';
|
import { MenuInfo } from 'rc-menu/lib/interface';
|
||||||
import React, { useCallback, useMemo, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Link, useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
import { updateTask } from 'rest/feedsAPI';
|
import { updateTask, updateThread } from 'rest/feedsAPI';
|
||||||
import { getNameFromFQN } from 'utils/CommonUtils';
|
import EntityLink from 'utils/EntityLink';
|
||||||
import { getEntityFQN, prepareFeedLink } from 'utils/FeedUtils';
|
import { getEntityName } from 'utils/EntityUtils';
|
||||||
|
import { getEntityFQN } from 'utils/FeedUtils';
|
||||||
import { getEntityLink } from 'utils/TableUtils';
|
import { getEntityLink } from 'utils/TableUtils';
|
||||||
import {
|
import {
|
||||||
|
fetchOptions,
|
||||||
isDescriptionTask,
|
isDescriptionTask,
|
||||||
isTagsTask,
|
isTagsTask,
|
||||||
TASK_ACTION_LIST,
|
TASK_ACTION_LIST,
|
||||||
} from 'utils/TasksUtils';
|
} from 'utils/TasksUtils';
|
||||||
import { showErrorToast, showSuccessToast } from 'utils/ToastUtils';
|
import { showErrorToast, showSuccessToast } from 'utils/ToastUtils';
|
||||||
|
import './task-tab.less';
|
||||||
import { TaskTabProps } from './TaskTab.interface';
|
import { TaskTabProps } from './TaskTab.interface';
|
||||||
import { ReactComponent as TaskCloseIcon } from '/assets/svg/ic-close-task.svg';
|
import { ReactComponent as TaskCloseIcon } from '/assets/svg/ic-close-task.svg';
|
||||||
import { ReactComponent as TaskOpenIcon } from '/assets/svg/ic-open-task.svg';
|
import { ReactComponent as TaskOpenIcon } from '/assets/svg/ic-open-task.svg';
|
||||||
@ -65,6 +76,9 @@ export const TaskTab = ({
|
|||||||
entityType,
|
entityType,
|
||||||
...rest
|
...rest
|
||||||
}: TaskTabProps) => {
|
}: TaskTabProps) => {
|
||||||
|
const [assigneesForm] = useForm();
|
||||||
|
const updatedAssignees = Form.useWatch('assignees', assigneesForm);
|
||||||
|
|
||||||
const { task: taskDetails } = taskThread;
|
const { task: taskDetails } = taskThread;
|
||||||
const entityFQN = getEntityFQN(taskThread.about) ?? '';
|
const entityFQN = getEntityFQN(taskThread.about) ?? '';
|
||||||
const entityCheck = !isUndefined(entityFQN) && !isUndefined(entityType);
|
const entityCheck = !isUndefined(entityFQN) && !isUndefined(entityType);
|
||||||
@ -72,12 +86,24 @@ export const TaskTab = ({
|
|||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const { isAdminUser } = useAuth();
|
const { isAdminUser } = useAuth();
|
||||||
const { postFeed } = useActivityFeedProvider();
|
const { postFeed, setActiveThread } = useActivityFeedProvider();
|
||||||
const [taskAction, setTaskAction] = useState<TaskAction>(TASK_ACTION_LIST[0]);
|
const [taskAction, setTaskAction] = useState<TaskAction>(TASK_ACTION_LIST[0]);
|
||||||
|
|
||||||
const isTaskClosed = isEqual(taskDetails?.status, ThreadTaskStatus.Closed);
|
const isTaskClosed = isEqual(taskDetails?.status, ThreadTaskStatus.Closed);
|
||||||
const [showEditTaskModel, setShowEditTaskModel] = useState(false);
|
const [showEditTaskModel, setShowEditTaskModel] = useState(false);
|
||||||
const [comment, setComment] = useState('');
|
const [comment, setComment] = useState('');
|
||||||
|
const [isEditAssignee, setIsEditAssignee] = useState<boolean>(false);
|
||||||
|
const [options, setOptions] = useState<Option[]>([]);
|
||||||
|
|
||||||
|
const initialAssignees = useMemo(
|
||||||
|
() =>
|
||||||
|
taskDetails?.assignees.map((assignee) => ({
|
||||||
|
label: getEntityName(assignee),
|
||||||
|
value: assignee.id || '',
|
||||||
|
type: assignee.type,
|
||||||
|
})) ?? [],
|
||||||
|
[taskDetails]
|
||||||
|
);
|
||||||
|
|
||||||
// get current user details
|
// get current user details
|
||||||
const currentUser = useMemo(
|
const currentUser = useMemo(
|
||||||
@ -85,6 +111,17 @@ export const TaskTab = ({
|
|||||||
[AppState.userDetails, AppState.nonSecureUserDetails]
|
[AppState.userDetails, AppState.nonSecureUserDetails]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const taskField = useMemo(() => {
|
||||||
|
const entityField = EntityLink.getEntityField(taskThread.about) ?? '';
|
||||||
|
const columnName = EntityLink.getTableColumnName(taskThread.about) ?? '';
|
||||||
|
|
||||||
|
if (columnName) {
|
||||||
|
return `${entityField}/${columnName}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return entityField;
|
||||||
|
}, [taskThread]);
|
||||||
|
|
||||||
const isOwner = isEqual(owner?.id, currentUser?.id);
|
const isOwner = isEqual(owner?.id, currentUser?.id);
|
||||||
const isCreator = isEqual(taskThread.createdBy, currentUser?.name);
|
const isCreator = isEqual(taskThread.createdBy, currentUser?.name);
|
||||||
|
|
||||||
@ -113,18 +150,8 @@ export const TaskTab = ({
|
|||||||
|
|
||||||
<Typography.Text>{taskDetails?.type}</Typography.Text>
|
<Typography.Text>{taskDetails?.type}</Typography.Text>
|
||||||
<span className="m-x-xss">{t('label.for-lowercase')}</span>
|
<span className="m-x-xss">{t('label.for-lowercase')}</span>
|
||||||
<>
|
|
||||||
<span className="p-r-xss">{entityType}</span>
|
{!isEmpty(taskField) ? <span>{taskField}</span> : null}
|
||||||
<EntityPopOverCard entityFQN={entityFQN} entityType={entityType}>
|
|
||||||
<Link
|
|
||||||
className="break-all"
|
|
||||||
data-testid="entitylink"
|
|
||||||
to={prepareFeedLink(entityType, entityFQN)}
|
|
||||||
onClick={(e) => e.stopPropagation()}>
|
|
||||||
{getNameFromFQN(entityFQN)}
|
|
||||||
</Link>
|
|
||||||
</EntityPopOverCard>
|
|
||||||
</>
|
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -259,6 +286,7 @@ export const TaskTab = ({
|
|||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<Dropdown.Button
|
<Dropdown.Button
|
||||||
|
icon={<DownOutlined />}
|
||||||
menu={{
|
menu={{
|
||||||
items: TASK_ACTION_LIST,
|
items: TASK_ACTION_LIST,
|
||||||
selectable: true,
|
selectable: true,
|
||||||
@ -304,6 +332,32 @@ export const TaskTab = ({
|
|||||||
}
|
}
|
||||||
}, [taskDetails, isTaskDescription]);
|
}, [taskDetails, isTaskDescription]);
|
||||||
|
|
||||||
|
const handleAssigneeUpdate = async () => {
|
||||||
|
const updatedTaskThread = {
|
||||||
|
...taskThread,
|
||||||
|
task: {
|
||||||
|
...taskThread.task,
|
||||||
|
assignees: updatedAssignees.map((assignee: Option) => ({
|
||||||
|
id: assignee.value,
|
||||||
|
type: assignee.type,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
const patch = compare(taskThread, updatedTaskThread);
|
||||||
|
const data = await updateThread(taskThread.id, patch);
|
||||||
|
setIsEditAssignee(false);
|
||||||
|
setActiveThread(data);
|
||||||
|
} catch (error) {
|
||||||
|
showErrorToast(error as AxiosError);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
assigneesForm.setFieldValue('assignees', initialAssignees);
|
||||||
|
setOptions(initialAssignees);
|
||||||
|
}, [initialAssignees]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Row className="p-y-sm p-x-md" gutter={[0, 24]}>
|
<Row className="p-y-sm p-x-md" gutter={[0, 24]}>
|
||||||
<Col className="d-flex items-center" span={24}>
|
<Col className="d-flex items-center" span={24}>
|
||||||
@ -320,19 +374,78 @@ export const TaskTab = ({
|
|||||||
{getTaskLinkElement}
|
{getTaskLinkElement}
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={24}>
|
<Col span={24}>
|
||||||
<div className="d-flex justify-between">
|
<div
|
||||||
<div className="flex-center gap-2">
|
className={classNames('d-flex justify-between', {
|
||||||
|
'flex-column': isEditAssignee,
|
||||||
|
})}>
|
||||||
|
<div
|
||||||
|
className={classNames('gap-2', { 'flex-center': !isEditAssignee })}>
|
||||||
|
{isEditAssignee ? (
|
||||||
|
<Form
|
||||||
|
form={assigneesForm}
|
||||||
|
layout="vertical"
|
||||||
|
onFinish={handleAssigneeUpdate}>
|
||||||
|
<Form.Item
|
||||||
|
data-testid="assignees"
|
||||||
|
label={`${t('label.assignee-plural')}:`}
|
||||||
|
name="assignees"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: t('message.field-text-is-required', {
|
||||||
|
fieldText: t('label.assignee-plural'),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
<InlineEdit
|
||||||
|
className="assignees-edit-input"
|
||||||
|
direction="horizontal"
|
||||||
|
onCancel={() => {
|
||||||
|
setIsEditAssignee(false);
|
||||||
|
assigneesForm.setFieldValue(
|
||||||
|
'assignees',
|
||||||
|
initialAssignees
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
onSave={() => assigneesForm.submit()}>
|
||||||
|
<Assignees
|
||||||
|
options={options}
|
||||||
|
value={updatedAssignees}
|
||||||
|
onChange={(values) =>
|
||||||
|
assigneesForm.setFieldValue('assignees', values)
|
||||||
|
}
|
||||||
|
onSearch={(query) => fetchOptions(query, setOptions)}
|
||||||
|
/>
|
||||||
|
</InlineEdit>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
<Typography.Text className="text-grey-muted">
|
<Typography.Text className="text-grey-muted">
|
||||||
{t('label.assignee-plural')}:{' '}
|
{t('label.assignee-plural')}:{' '}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
|
<AssigneeList
|
||||||
<OwnerLabel
|
assignees={taskDetails?.assignees ?? []}
|
||||||
hasPermission={false}
|
className="d-flex gap-1"
|
||||||
owner={taskDetails?.assignees[0]}
|
profilePicType="circle"
|
||||||
onUpdate={noop}
|
profileWidth="24"
|
||||||
|
showUserName={false}
|
||||||
/>
|
/>
|
||||||
|
{isCreator || hasTaskUpdateAccess() ? (
|
||||||
|
<Button
|
||||||
|
className="flex-center p-0"
|
||||||
|
data-testid="edit-assignees"
|
||||||
|
icon={<EditIcon color={DE_ACTIVE_COLOR} width="14px" />}
|
||||||
|
size="small"
|
||||||
|
type="text"
|
||||||
|
onClick={() => setIsEditAssignee(true)}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-center gap-2">
|
<div
|
||||||
|
className={classNames('gap-2', { 'flex-center': !isEditAssignee })}>
|
||||||
<Typography.Text className="text-grey-muted">
|
<Typography.Text className="text-grey-muted">
|
||||||
{t('label.created-by')}:{' '}
|
{t('label.created-by')}:{' '}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
|
|||||||
@ -0,0 +1,17 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 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.
|
||||||
|
*/
|
||||||
|
.assignees-edit-input {
|
||||||
|
.ant-space-item:first-child {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -142,6 +142,9 @@
|
|||||||
.flex-row {
|
.flex-row {
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
}
|
}
|
||||||
|
.flex-column {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
// Justify Items
|
// Justify Items
|
||||||
.justify-center {
|
.justify-center {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user