diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedList/ActivityFeedListV1.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedList/ActivityFeedListV1.component.tsx index ec5edd301cb..26953a0d28a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedList/ActivityFeedListV1.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedList/ActivityFeedListV1.component.tsx @@ -27,6 +27,7 @@ interface ActivityFeedListV1Props { onFeedClick?: (feed: Thread) => void; activeFeedId?: string; hidePopover: boolean; + isForFeedTab?: boolean; emptyPlaceholderText: string; } @@ -37,6 +38,7 @@ const ActivityFeedListV1 = ({ onFeedClick, activeFeedId, hidePopover = false, + isForFeedTab = false, emptyPlaceholderText, }: ActivityFeedListV1Props) => { const [entityThread, setEntityThread] = useState([]); @@ -76,6 +78,7 @@ const ActivityFeedListV1 = ({ feed={feed} hidePopover={hidePopover} isActive={activeFeedId === feed.id} + isForFeedTab={isForFeedTab} key={feed.id} showThread={showThread} onFeedClick={onFeedClick} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedPanel/FeedPanelBodyV1.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedPanel/FeedPanelBodyV1.tsx index 34c5164366c..99f9f7a32d1 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedPanel/FeedPanelBodyV1.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedPanel/FeedPanelBodyV1.tsx @@ -26,6 +26,7 @@ interface FeedPanelBodyPropV1 { isOpenInDrawer?: boolean; onFeedClick?: (feed: Thread) => void; isActive?: boolean; + isForFeedTab?: boolean; hidePopover: boolean; } @@ -37,6 +38,7 @@ const FeedPanelBodyV1: FC = ({ onFeedClick, isActive, hidePopover = false, + isForFeedTab = false, }) => { const { t } = useTranslation(); const mainFeed = { @@ -64,6 +66,7 @@ const FeedPanelBodyV1: FC = ({ feed={feed} hidePopover={hidePopover} isActive={isActive} + isForFeedTab={isForFeedTab} isOpenInDrawer={isOpenInDrawer} key={feed.id} post={mainFeed} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedTab/ActivityFeedTab.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedTab/ActivityFeedTab.component.tsx index 6bf78241579..2aa771d61f6 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedTab/ActivityFeedTab.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedTab/ActivityFeedTab.component.tsx @@ -350,6 +350,7 @@ export const ActivityFeedTab = ({ )} { + const history = useHistory(); const { t } = useTranslation(); const timeStamp = feed.threadTs; const taskDetails = feed.task; const postLength = feed?.postsCount ?? 0; const entityType = getEntityType(feed.about) ?? ''; const entityFQN = getEntityFQN(feed.about) ?? ''; + const entityCheck = !isUndefined(entityFQN) && !isUndefined(entityType); const [isEditPost, setIsEditPost] = useState(false); const repliedUsers = [...new Set((feed?.posts ?? []).map((f) => f.from))]; 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 = () => { showDrawer?.(feed); @@ -81,11 +98,21 @@ const TaskFeedCard = ({ setIsEditPost(!isEditPost); }; + const handleTaskLinkClick = () => { + history.push({ + pathname: getTaskDetailPath(feed), + }); + setActiveThread(feed); + }; + const getTaskLinkElement = entityCheck && ( - {`#${taskDetails?.id} `} + - {taskDetails?.type} + {taskDetails?.type} {t('label.for-lowercase')} {isEntityFeed ? ( @@ -93,16 +120,26 @@ const TaskFeedCard = ({ ) : ( <> - {entityType} - - e.stopPropagation()}> - {getNameFromFQN(entityFQN)} - - + {isForFeedTab ? null : ( + <> + {entityType} + + e.stopPropagation()}> + {getNameFromFQN(entityFQN)} + + + + )} + + {!isEmpty(taskField) ? ( + + {taskField} + + ) : null} )} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/InlineEdit/InlineEdit.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/InlineEdit/InlineEdit.component.tsx index e65dc2ea7a0..96520200f01 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/InlineEdit/InlineEdit.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/InlineEdit/InlineEdit.component.tsx @@ -12,6 +12,7 @@ */ import { CheckOutlined, CloseOutlined } from '@ant-design/icons'; import { Button, Space } from 'antd'; +import classNames from 'classnames'; import React from 'react'; import { InlineEditProps } from './InlineEdit.interface'; @@ -20,10 +21,11 @@ const InlineEdit = ({ onCancel, onSave, direction, + className, }: InlineEditProps) => { return ( {children} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/InlineEdit/InlineEdit.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/InlineEdit/InlineEdit.interface.ts index d282196dca0..6b34ed4ca44 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/InlineEdit/InlineEdit.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/InlineEdit/InlineEdit.interface.ts @@ -14,6 +14,7 @@ import { SpaceProps } from 'antd'; import { ReactNode } from 'react'; export interface InlineEditProps { + className?: string; children: ReactNode; onCancel: () => void; // onSave it can be API call or normal function diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Task/TaskTab/TaskTab.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Task/TaskTab/TaskTab.component.tsx index 61845ba1526..4bbf39cf085 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Task/TaskTab/TaskTab.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Task/TaskTab/TaskTab.component.tsx @@ -10,7 +10,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import Icon from '@ant-design/icons'; +import Icon, { DownOutlined } from '@ant-design/icons'; import { Button, Col, @@ -21,40 +21,51 @@ import { Space, Typography, } from 'antd'; +import { useForm } from 'antd/lib/form/Form'; import Modal from 'antd/lib/modal/Modal'; import AppState from 'AppState'; +import { ReactComponent as EditIcon } from 'assets/svg/edit-new.svg'; import { AxiosError } from 'axios'; +import classNames from 'classnames'; import ActivityFeedCardV1 from 'components/ActivityFeed/ActivityFeedCard/ActivityFeedCardV1'; import ActivityFeedEditor from 'components/ActivityFeed/ActivityFeedEditor/ActivityFeedEditor'; import { useActivityFeedProvider } from 'components/ActivityFeed/ActivityFeedProvider/ActivityFeedProvider'; +import AssigneeList from 'components/common/AssigneeList/AssigneeList'; 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 { compare } from 'fast-json-patch'; import { TaskType } from 'generated/api/feed/createThread'; import { TaskDetails, ThreadTaskStatus } from 'generated/entity/feed/thread'; import { TagLabel } from 'generated/type/tagLabel'; import { useAuth } from 'hooks/authHooks'; import { isEmpty, isEqual, isUndefined, noop } from 'lodash'; +import Assignees from 'pages/TasksPage/shared/Assignees'; import DescriptionTask from 'pages/TasksPage/shared/DescriptionTask'; import TagsTask from 'pages/TasksPage/shared/TagsTask'; import { + Option, TaskAction, TaskActionMode, } from 'pages/TasksPage/TasksPage.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 { Link, useHistory } from 'react-router-dom'; -import { updateTask } from 'rest/feedsAPI'; -import { getNameFromFQN } from 'utils/CommonUtils'; -import { getEntityFQN, prepareFeedLink } from 'utils/FeedUtils'; +import { useHistory } from 'react-router-dom'; +import { updateTask, updateThread } from 'rest/feedsAPI'; +import EntityLink from 'utils/EntityLink'; +import { getEntityName } from 'utils/EntityUtils'; +import { getEntityFQN } from 'utils/FeedUtils'; import { getEntityLink } from 'utils/TableUtils'; import { + fetchOptions, isDescriptionTask, isTagsTask, TASK_ACTION_LIST, } from 'utils/TasksUtils'; import { showErrorToast, showSuccessToast } from 'utils/ToastUtils'; +import './task-tab.less'; import { TaskTabProps } from './TaskTab.interface'; import { ReactComponent as TaskCloseIcon } from '/assets/svg/ic-close-task.svg'; import { ReactComponent as TaskOpenIcon } from '/assets/svg/ic-open-task.svg'; @@ -65,6 +76,9 @@ export const TaskTab = ({ entityType, ...rest }: TaskTabProps) => { + const [assigneesForm] = useForm(); + const updatedAssignees = Form.useWatch('assignees', assigneesForm); + const { task: taskDetails } = taskThread; const entityFQN = getEntityFQN(taskThread.about) ?? ''; const entityCheck = !isUndefined(entityFQN) && !isUndefined(entityType); @@ -72,12 +86,24 @@ export const TaskTab = ({ const [form] = Form.useForm(); const history = useHistory(); const { isAdminUser } = useAuth(); - const { postFeed } = useActivityFeedProvider(); + const { postFeed, setActiveThread } = useActivityFeedProvider(); const [taskAction, setTaskAction] = useState(TASK_ACTION_LIST[0]); const isTaskClosed = isEqual(taskDetails?.status, ThreadTaskStatus.Closed); const [showEditTaskModel, setShowEditTaskModel] = useState(false); const [comment, setComment] = useState(''); + const [isEditAssignee, setIsEditAssignee] = useState(false); + const [options, setOptions] = useState([]); + + const initialAssignees = useMemo( + () => + taskDetails?.assignees.map((assignee) => ({ + label: getEntityName(assignee), + value: assignee.id || '', + type: assignee.type, + })) ?? [], + [taskDetails] + ); // get current user details const currentUser = useMemo( @@ -85,6 +111,17 @@ export const TaskTab = ({ [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 isCreator = isEqual(taskThread.createdBy, currentUser?.name); @@ -113,18 +150,8 @@ export const TaskTab = ({ {taskDetails?.type} {t('label.for-lowercase')} - <> - {entityType} - - e.stopPropagation()}> - {getNameFromFQN(entityFQN)} - - - + + {!isEmpty(taskField) ? {taskField} : null} ); @@ -259,6 +286,7 @@ export const TaskTab = ({ ) : ( } menu={{ items: TASK_ACTION_LIST, selectable: true, @@ -304,6 +332,32 @@ export const TaskTab = ({ } }, [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 ( @@ -320,19 +374,78 @@ export const TaskTab = ({ {getTaskLinkElement} -
-
- - {t('label.assignee-plural')}:{' '} - - - +
+
+ {isEditAssignee ? ( +
+ + { + setIsEditAssignee(false); + assigneesForm.setFieldValue( + 'assignees', + initialAssignees + ); + }} + onSave={() => assigneesForm.submit()}> + + assigneesForm.setFieldValue('assignees', values) + } + onSearch={(query) => fetchOptions(query, setOptions)} + /> + + +
+ ) : ( + <> + + {t('label.assignee-plural')}:{' '} + + + {isCreator || hasTaskUpdateAccess() ? ( +
-
+
{t('label.created-by')}:{' '} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Task/TaskTab/task-tab.less b/openmetadata-ui/src/main/resources/ui/src/components/Task/TaskTab/task-tab.less new file mode 100644 index 00000000000..189a4057651 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/Task/TaskTab/task-tab.less @@ -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%; + } +} diff --git a/openmetadata-ui/src/main/resources/ui/src/styles/position.less b/openmetadata-ui/src/main/resources/ui/src/styles/position.less index d58d3f757b4..d0a07d2c103 100644 --- a/openmetadata-ui/src/main/resources/ui/src/styles/position.less +++ b/openmetadata-ui/src/main/resources/ui/src/styles/position.less @@ -142,6 +142,9 @@ .flex-row { flex-direction: row; } +.flex-column { + flex-direction: column; +} // Justify Items .justify-center {