Revamp : Request/update description should be created as a task (#5637)

* Revamp  : Request/update description should be created as a task #5321

* Fix Feed header issue

* Add support for profile picture for task assignees

* Add support for title input on request/update task

* Update component based on task type

* Update editor height

* Fix unit test

* Minor fixes
This commit is contained in:
Sachin Chaurasiya 2022-06-25 22:07:10 +05:30 committed by GitHub
parent 7ad97d8fed
commit 07d882ee50
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 480 additions and 211 deletions

View File

@ -14,9 +14,10 @@
import classNames from 'classnames';
import { isUndefined, toString } from 'lodash';
import React, { FC, Fragment } from 'react';
import { Link } from 'react-router-dom';
import { Link, useHistory } from 'react-router-dom';
import AppState from '../../../../AppState';
import { FQN_SEPARATOR_CHAR } from '../../../../constants/char.constants';
import { getUserPath } from '../../../../constants/constants';
import {
EntityType,
FqnPart,
@ -34,6 +35,7 @@ import {
import { getEntityLink } from '../../../../utils/TableUtils';
import { getTaskDetailPath } from '../../../../utils/TasksUtils';
import { getDayTimeByTimeStamp } from '../../../../utils/TimeUtils';
import Ellipses from '../../../common/Ellipses/Ellipses';
import EntityPopOverCard from '../../../common/PopOverCard/EntityPopOverCard';
import UserPopOverCard from '../../../common/PopOverCard/UserPopOverCard';
import { FeedHeaderProp } from '../ActivityFeedCard.interface';
@ -50,6 +52,10 @@ const FeedCardHeader: FC<FeedHeaderProp> = ({
feedType,
taskDetails,
}) => {
const history = useHistory();
const onTitleClickHandler = (name: string) => {
history.push(getUserPath(name));
};
const entityDisplayName = () => {
let displayName;
if (entityType === EntityType.TABLE) {
@ -110,6 +116,7 @@ const FeedCardHeader: FC<FeedHeaderProp> = ({
const getFeedLinkElement = () => {
if (!isUndefined(entityFQN) && !isUndefined(entityType)) {
return (
<Ellipses rows={1}>
<span className="tw-pl-1 tw-font-normal" data-testid="headerText">
{getFeedAction(feedType)}{' '}
{isEntityFeed ? (
@ -117,8 +124,6 @@ const FeedCardHeader: FC<FeedHeaderProp> = ({
{getEntityFieldDisplay(entityField)}
</span>
) : (
<Fragment>
{feedType === ThreadType.Conversation ? (
<Fragment>
<span data-testid="entityType">{entityType} </span>
<Link data-testid="entitylink" to={prepareFeedLink()}>
@ -133,20 +138,55 @@ const FeedCardHeader: FC<FeedHeaderProp> = ({
</button>
</Link>
</Fragment>
) : (
)}
</span>
</Ellipses>
);
} else {
return null;
}
};
const getTaskLinkElement = () => {
if (!isUndefined(entityFQN) && !isUndefined(entityType)) {
return (
<div className="tw-flex tw-flex-wrap">
<span className="tw-px-1">created a task</span>
<Link
data-testid="tasklink"
to={getTaskDetailPath(toString(taskDetails?.id)).pathname}>
<button
className="tw-text-info"
disabled={AppState.isTourOpen}>
<button className="tw-text-info" disabled={AppState.isTourOpen}>
{`#${taskDetails?.id}`} <span>{taskDetails?.type}</span>
</button>
</Link>
)}
</Fragment>
)}
<span className="tw-px-1">for</span>
{isEntityFeed ? (
<span className="tw-heading" data-testid="headerText-entityField">
{getEntityFieldDisplay(entityField)}
</span>
) : (
<span className="tw-flex">
<span className="tw-pr-1">{entityType}</span>
<EntityPopOverCard entityFQN={entityFQN} entityType={entityType}>
<Link data-testid="entitylink" to={prepareFeedLink()}>
<button disabled={AppState.isTourOpen}>
<Ellipses
className="tw-w-28"
rows={1}
style={{
color: 'rgb(24, 144, 255)',
cursor: 'pointer',
textDecoration: 'none',
}}>
{entityDisplayName()}
</Ellipses>
</button>
</Link>
</EntityPopOverCard>
</span>
)}
</div>
);
} else {
return null;
@ -157,10 +197,16 @@ const FeedCardHeader: FC<FeedHeaderProp> = ({
<div className={classNames('tw-flex', className)}>
<div className="tw-flex tw-m-0 tw-pl-2 tw-leading-4">
<UserPopOverCard userName={createdBy}>
<span className="thread-author tw-cursor-pointer">{createdBy}</span>
<span
className="thread-author tw-cursor-pointer"
onClick={() => onTitleClickHandler(createdBy)}>
{createdBy}
</span>
</UserPopOverCard>
{getFeedLinkElement()}
{feedType === ThreadType.Conversation
? getFeedLinkElement()
: getTaskLinkElement()}
<span
className="tw-text-grey-muted tw-pl-2 tw-text-xs tw--mb-0.5"
data-testid="timestamp">

View File

@ -12,9 +12,12 @@
*/
import { Card } from 'antd';
import { uniqueId } from 'lodash';
import React, { FC, Fragment } from 'react';
import { Post, ThreadType } from '../../../generated/entity/feed/thread';
import { getEntityName } from '../../../utils/CommonUtils';
import UserPopOverCard from '../../common/PopOverCard/UserPopOverCard';
import ProfilePicture from '../../common/ProfilePicture/ProfilePicture';
import { leftPanelAntCardStyle } from '../../containers/PageLayout';
import ActivityFeedCard from '../ActivityFeedCard/ActivityFeedCard';
import FeedCardFooter from '../ActivityFeedCard/FeedCardFooter/FeedCardFooter';
@ -150,12 +153,26 @@ const FeedListBody: FC<FeedListBodyProp> = ({
) : null}
</div>
{feed.task && (
<div className="tw-border-t tw-border-main tw-py-1">
<div className="tw-border-t tw-border-main tw-py-1 tw-flex">
<span className="tw-text-grey-muted">Assignees: </span>
<span className="tw-ml-0.5 tw-align-middle">
{feed.task.assignees
.map((assignee) => getEntityName(assignee))
.join(', ')}
<span className="tw-ml-0.5 tw-align-middle tw-grid tw-grid-cols-4">
{feed.task.assignees.map((assignee) => (
<UserPopOverCard
key={uniqueId()}
type={assignee.type}
userName={assignee.name || ''}>
<span className="tw-flex tw-m-1.5 tw-mt-0">
<ProfilePicture
id=""
name={getEntityName(assignee)}
width="20"
/>
<span className="tw-ml-1">
{getEntityName(assignee)}
</span>
</span>
</UserPopOverCard>
))}
</span>
</div>
)}

View File

@ -11,10 +11,13 @@
* limitations under the License.
*/
import { Card } from 'antd';
import { uniqueId } from 'lodash';
import React, { FC, Fragment } from 'react';
import { Post, ThreadType } from '../../../generated/entity/feed/thread';
import { getEntityName } from '../../../utils/CommonUtils';
import { getFeedListWithRelativeDays } from '../../../utils/FeedUtils';
import UserPopOverCard from '../../common/PopOverCard/UserPopOverCard';
import ProfilePicture from '../../common/ProfilePicture/ProfilePicture';
import { leftPanelAntCardStyle } from '../../containers/PageLayout';
import ActivityFeedCard from '../ActivityFeedCard/ActivityFeedCard';
import FeedCardFooter from '../ActivityFeedCard/FeedCardFooter/FeedCardFooter';
@ -139,10 +142,24 @@ const ActivityThreadList: FC<ActivityThreadListProp> = ({
<span className="tw-text-grey-muted">
Assignees:{' '}
</span>
<span className="tw-ml-0.5 tw-align-middle">
{thread.task.assignees
.map((assignee) => getEntityName(assignee))
.join(', ')}
<span className="tw-ml-0.5 tw-align-middle tw-grid tw-grid-cols-3">
{thread.task.assignees.map((assignee) => (
<UserPopOverCard
key={uniqueId()}
type={assignee.type}
userName={assignee.name || ''}>
<span className="tw-flex tw-m-1.5 tw-mt-0">
<ProfilePicture
id=""
name={getEntityName(assignee)}
width="20"
/>
<span className="tw-ml-1">
{getEntityName(assignee)}
</span>
</span>
</UserPopOverCard>
))}
</span>
</div>
)}

View File

@ -41,6 +41,7 @@ export interface ActivityThreadPanelBodyProp
> {
threadType: ThreadType;
showHeader?: boolean;
onTabChange?: (key: string) => void;
}
export interface ActivityThreadListProp

View File

@ -11,7 +11,12 @@
* limitations under the License.
*/
import { findByTestId, findByText, render } from '@testing-library/react';
import {
findAllByText,
findByTestId,
findByText,
render,
} from '@testing-library/react';
import React from 'react';
import ReactDOM from 'react-dom';
import { MemoryRouter } from 'react-router-dom';
@ -73,10 +78,13 @@ describe('Test ActivityThreadPanel Component', () => {
);
const panelOverlay = await findByText(container, /FeedPanelOverlay/i);
const panelThreadList = await findByText(container, /ActivityThreadList/i);
const panelThreadList = await findAllByText(
container,
/ActivityThreadList/i
);
expect(panelOverlay).toBeInTheDocument();
expect(panelThreadList).toBeInTheDocument();
expect(panelThreadList).toHaveLength(2);
});
it('Should create an observer if IntersectionObserver is available', async () => {

View File

@ -69,6 +69,7 @@ const ActivityThreadPanel: FC<ActivityThreadPanelProp> = ({
threadType={ThreadType.Task}
updateThreadHandler={updateThreadHandler}
onCancel={onCancel}
onTabChange={onTabChange}
/>
</TabPane>
<TabPane key="2" tab="Conversations">
@ -80,6 +81,7 @@ const ActivityThreadPanel: FC<ActivityThreadPanelProp> = ({
threadType={ThreadType.Conversation}
updateThreadHandler={updateThreadHandler}
onCancel={onCancel}
onTabChange={onTabChange}
/>
</TabPane>
</Tabs>

View File

@ -14,7 +14,7 @@
import { AxiosError, AxiosResponse } from 'axios';
import classNames from 'classnames';
import { Operation } from 'fast-json-patch';
import { isUndefined } from 'lodash';
import { isEqual, isUndefined } from 'lodash';
import React, { FC, Fragment, RefObject, useEffect, useState } from 'react';
import AppState from '../../../AppState';
import { getAllFeeds } from '../../../axiosAPIs/feedsAPI';
@ -22,10 +22,12 @@ import { confirmStateInitialValue } from '../../../constants/feed.constants';
import { observerOptions } from '../../../constants/Mydata.constants';
import { Thread, ThreadType } from '../../../generated/entity/feed/thread';
import { Paging } from '../../../generated/type/paging';
import { useAfterMount } from '../../../hooks/useAfterMount';
import { useInfiniteScroll } from '../../../hooks/useInfiniteScroll';
import jsonData from '../../../jsons/en';
import { getEntityField } from '../../../utils/FeedUtils';
import { showErrorToast } from '../../../utils/ToastUtils';
import ErrorPlaceHolder from '../../common/error-with-placeholder/ErrorPlaceHolder';
import Loader from '../../Loader/Loader';
import { ConfirmState } from '../ActivityFeedCard/ActivityFeedCard.interface';
import ActivityFeedEditor from '../ActivityFeedEditor/ActivityFeedEditor';
@ -45,6 +47,7 @@ const ActivityThreadPanelBody: FC<ActivityThreadPanelBodyProp> = ({
className,
showHeader = true,
threadType,
onTabChange,
}) => {
const [threads, setThreads] = useState<Thread[]>([]);
const [selectedThread, setSelectedThread] = useState<Thread>();
@ -195,6 +198,12 @@ const ActivityThreadPanelBody: FC<ActivityThreadPanelBodyProp> = ({
fetchMoreThread(isInView as boolean, paging, isThreadLoading);
}, [paging, isThreadLoading, isInView]);
useAfterMount(() => {
if (threadType === ThreadType.Task && !isThreadLoading) {
isEqual(threads.length, 0) && onTabChange && onTabChange('2');
}
}, [threads, isThreadLoading]);
return (
<Fragment>
<div id="thread-panel-body">
@ -233,6 +242,9 @@ const ActivityThreadPanelBody: FC<ActivityThreadPanelBodyProp> = ({
<Fragment>
{showNewConversation || threads.length === 0 ? (
<div className="tw-pt-2">
<Fragment>
{threadType === ThreadType.Conversation ? (
<Fragment>
<p className="tw-ml-9 tw-mr-2 tw-my-2">
You are starting a new conversation
</p>
@ -242,6 +254,11 @@ const ActivityThreadPanelBody: FC<ActivityThreadPanelBodyProp> = ({
placeHolder="Enter a message"
onSave={onPostThread}
/>
</Fragment>
) : (
<ErrorPlaceHolder>No tasks yet</ErrorPlaceHolder>
)}
</Fragment>
</div>
) : null}
<ActivityThreadList

View File

@ -16,8 +16,13 @@ import React from 'react';
const TaskBadge = () => {
return (
<span
className="tw-text-grey-muted tw-border tw-border-main tw-rounded tw-px-2 tw-absolute tw-left-4 tw--top-3"
style={{ background: '#DCE3EC' }}>
className="tw-rounded tw-px-2 tw-absolute tw-left-4 tw--top-3"
style={{
color: '#485056',
background: '#EBF5FF',
boxShadow: '0px 1px 2px rgba(0, 0, 0, 0.06)',
borderRadius: '4px',
}}>
Task
</span>
);

View File

@ -13,17 +13,21 @@
import { Typography } from 'antd';
import { EllipsisConfig } from 'antd/lib/typography/Base';
import React, { FC } from 'react';
import classNames from 'classnames';
import React, { CSSProperties, FC } from 'react';
interface Props extends EllipsisConfig {
children: React.ReactNode;
className?: string;
style?: CSSProperties;
}
const Ellipses: FC<Props> = ({ children, ...props }) => {
const Ellipses: FC<Props> = ({ children, className, style, ...props }) => {
return (
<Typography.Paragraph
className="ant-typography-ellipsis-custom"
ellipsis={props}>
className={classNames('ant-typography-ellipsis-custom', className)}
ellipsis={props}
style={style}>
{children}
</Typography.Paragraph>
);

View File

@ -13,6 +13,7 @@
import { Popover } from 'antd';
import { AxiosError, AxiosResponse } from 'axios';
import { isEmpty } from 'lodash';
import React, { FC, Fragment, HTMLAttributes, useState } from 'react';
import { useHistory } from 'react-router-dom';
import { getUserByName } from '../../../axiosAPIs/userAPI';
@ -27,14 +28,17 @@ import ProfilePicture from '../ProfilePicture/ProfilePicture';
interface Props extends HTMLAttributes<HTMLDivElement> {
userName: string;
type?: string;
}
const UserPopOverCard: FC<Props> = ({ children, userName }) => {
const UserPopOverCard: FC<Props> = ({ children, userName, type = 'user' }) => {
const history = useHistory();
const [userData, setUserData] = useState<User>({} as User);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [isLoading, setIsLoading] = useState<boolean>(false);
const getData = () => {
if (type === 'user') {
setIsLoading(true);
getUserByName(userName, 'profile,roles,teams,follows,owns')
.then((res: AxiosResponse) => {
setUserData(res.data);
@ -43,6 +47,7 @@ const UserPopOverCard: FC<Props> = ({ children, userName }) => {
showErrorToast(err);
})
.finally(() => setIsLoading(false));
}
};
const onTitleClickHandler = (path: string) => {
@ -117,6 +122,7 @@ const UserPopOverCard: FC<Props> = ({ children, userName }) => {
{displayName !== name ? (
<span className="tw-text-grey-muted">{name}</span>
) : null}
{isEmpty(userData) && <span>{userName}</span>}
</div>
</div>
);
@ -129,8 +135,14 @@ const UserPopOverCard: FC<Props> = ({ children, userName }) => {
<Loader size="small" />
) : (
<div className="tw-w-80">
{isEmpty(userData) ? (
<span>No data available</span>
) : (
<Fragment>
<UserTeams />
<UserRoles />
</Fragment>
)}
</div>
)}
</Fragment>

View File

@ -51,4 +51,5 @@ export interface RichTextEditorProp extends HTMLAttributes<HTMLDivElement> {
useCommandShortcut?: boolean;
readonly?: boolean;
height?: string;
onTextChange?: (value: string) => void;
}

View File

@ -14,6 +14,7 @@
/* eslint-disable */
import { Editor, Viewer } from '@toast-ui/react-editor';
import classNames from 'classnames';
import React, {
createRef,
forwardRef,
@ -37,6 +38,9 @@ const RichTextEditor = forwardRef<editorRef, RichTextEditorProp>(
initialValue = '',
readonly,
height,
className,
style,
onTextChange,
}: RichTextEditorProp,
ref
) => {
@ -49,6 +53,7 @@ const RichTextEditor = forwardRef<editorRef, RichTextEditorProp>(
?.getInstance()
.getMarkdown() as string;
setEditorValue(value);
onTextChange && onTextChange(value);
};
useImperativeHandle(ref, () => ({
@ -62,7 +67,7 @@ const RichTextEditor = forwardRef<editorRef, RichTextEditorProp>(
}, [initialValue]);
return (
<div className="tw-my-4">
<div className={classNames(className, 'tw-my-4')} style={style}>
{readonly ? (
<div
className="tw-border tw-border-main tw-p-2 tw-rounded"

View File

@ -13,14 +13,18 @@
import { useEffect, useState } from 'react';
export const useAfterMount = (fnCallback: () => void) => {
export const useAfterMount = (
fnCallback: () => void,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
deps?: ReadonlyArray<any>
) => {
const [isMounting, setIsMounting] = useState(true);
useEffect(() => {
if (!isMounting) {
fnCallback();
}
}, [isMounting]);
}, [isMounting, ...(deps || [])]);
useEffect(() => {
setIsMounting(false);

View File

@ -11,12 +11,13 @@
* limitations under the License.
*/
import { Button, Card } from 'antd';
import { Button, Card, Input } from 'antd';
import { AxiosError, AxiosResponse } from 'axios';
import { capitalize, isNil } from 'lodash';
import { observer } from 'mobx-react';
import { EditorContentRef, EntityTags } from 'Models';
import React, {
ChangeEvent,
useCallback,
useEffect,
useMemo,
@ -70,6 +71,8 @@ const RequestDescription = () => {
const [entityData, setEntityData] = useState<EntityData>({} as EntityData);
const [options, setOptions] = useState<Option[]>([]);
const [assignees, setAssignees] = useState<Array<Option>>([]);
const [title, setTitle] = useState<string>('');
const [suggestion, setSuggestion] = useState<string>('');
const entityTier = useMemo(() => {
const tierFQN = getTierTags(entityData.tags || [])?.tagFQN;
@ -128,11 +131,20 @@ const RequestDescription = () => {
}
};
const onTitleChange = (e: ChangeEvent<HTMLInputElement>) => {
const { value: newValue } = e.target;
setTitle(newValue);
};
const onSuggestionChange = (value: string) => {
setSuggestion(value);
};
const onCreateTask = () => {
if (assignees.length) {
const data: CreateThread = {
from: currentUser?.name as string,
message,
message: title || message,
about: getEntityFeedLink(entityType, entityFQN, getTaskAbout()),
taskDetails: {
assignees: assignees.map((assignee) => ({
@ -176,6 +188,7 @@ const RequestDescription = () => {
setAssignees(defaultAssignee);
setOptions(defaultAssignee);
}
setTitle(message);
}, [entityData]);
return (
@ -191,9 +204,19 @@ const RequestDescription = () => {
className="tw-col-span-2"
key="request-description"
style={{ ...cardStyles }}
title={`Task: ${message}`}>
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 className="tw-text-grey-muted">Assignees:</span>{' '}
<span>Assignees:</span>{' '}
<Assignees
assignees={assignees}
options={options}
@ -203,16 +226,16 @@ const RequestDescription = () => {
</div>
<p data-testid="description-label">
<span>Description:</span>{' '}
<span className="tw-text-grey-muted">
description below will be suggested to the assignees
</span>
<span>Suggest description:</span>{' '}
</p>
<RichTextEditor
className="tw-my-0"
initialValue=""
placeHolder="Suggest description"
ref={markdownRef}
style={{ marginTop: '4px' }}
onTextChange={onSuggestionChange}
/>
<div className="tw-flex tw-justify-end" data-testid="cta-buttons">
@ -223,7 +246,7 @@ const RequestDescription = () => {
className="ant-btn-primary-custom"
type="primary"
onClick={onCreateTask}>
Submit
{suggestion ? 'Suggest' : 'Submit'}
</Button>
</div>
</Card>

View File

@ -16,7 +16,7 @@ import { Button, Card, Layout, Tabs } from 'antd';
import { AxiosError, AxiosResponse } from 'axios';
import classNames from 'classnames';
import { compare, Operation } from 'fast-json-patch';
import { isEmpty, isEqual, isUndefined, toLower } from 'lodash';
import { isEmpty, isEqual, isUndefined, toLower, uniqueId } from 'lodash';
import { observer } from 'mobx-react';
import { EditorContentRef, EntityTags } from 'Models';
import React, { Fragment, useEffect, useMemo, useRef, useState } from 'react';
@ -39,7 +39,7 @@ import Ellipses from '../../../components/common/Ellipses/Ellipses';
import ErrorPlaceHolder from '../../../components/common/error-with-placeholder/ErrorPlaceHolder';
import UserPopOverCard from '../../../components/common/PopOverCard/UserPopOverCard';
import ProfilePicture from '../../../components/common/ProfilePicture/ProfilePicture';
import RichTextEditorPreviewer from '../../../components/common/rich-text-editor/RichTextEditorPreviewer';
import RichTextEditor from '../../../components/common/rich-text-editor/RichTextEditor';
import TitleBreadcrumb from '../../../components/common/title-breadcrumb/title-breadcrumb.component';
import { FQN_SEPARATOR_CHAR } from '../../../constants/char.constants';
import { TaskOperation } from '../../../constants/feed.constants';
@ -47,6 +47,7 @@ import { EntityType } from '../../../enums/entity.enum';
import { CreateThread } from '../../../generated/api/feed/createThread';
import { Column } from '../../../generated/entity/data/table';
import {
TaskType,
Thread,
ThreadTaskStatus,
ThreadType,
@ -61,6 +62,7 @@ import {
getEntityType,
updateThreadData,
} from '../../../utils/FeedUtils';
import { getEncodedFqn } from '../../../utils/StringsUtils';
import SVGIcons from '../../../utils/SvgUtils';
import {
getEntityLink,
@ -72,11 +74,13 @@ import {
fetchOptions,
getBreadCrumbList,
getColumnObject,
getDescriptionDiff,
} from '../../../utils/TasksUtils';
import { getDayTimeByTimeStamp } from '../../../utils/TimeUtils';
import { showErrorToast, showSuccessToast } from '../../../utils/ToastUtils';
import Assignees from '../shared/Assignees';
import { DescriptionTabs } from '../shared/DescriptionTabs';
import { DiffView } from '../shared/DiffView';
import { background, cardStyles, contentStyles } from '../TaskPage.styles';
import { EntityData, Option } from '../TasksPage.interface';
@ -136,7 +140,7 @@ const TaskDetailPage = () => {
}, [taskDetail, entityData]);
const currentDescription = () => {
if (entityField) {
if (entityField && !isEmpty(columnObject)) {
return columnObject.description || '';
} else {
return entityData.description || '';
@ -239,12 +243,12 @@ const TaskDetailPage = () => {
};
const onTaskResolve = () => {
const description =
markdownRef.current?.getEditorContent() || taskDetail.task?.suggestion;
const description = markdownRef.current?.getEditorContent();
updateTask(TaskOperation.RESOLVE, taskDetail.task?.id, {
newValue: description,
})
const data = { newValue: description };
if (description) {
updateTask(TaskOperation.RESOLVE, taskDetail.task?.id, data)
.then(() => {
showSuccessToast('Task Resolved Successfully');
history.push(
@ -255,6 +259,9 @@ const TaskDetailPage = () => {
);
})
.catch((err: AxiosError) => showErrorToast(err));
} else {
showErrorToast('Cannot accept empty description');
}
};
const onTaskReject = () => {
@ -329,7 +336,7 @@ const TaskDetailPage = () => {
entityFQN &&
fetchEntityDetail(
entityType as EntityType,
entityFQN as string,
getEncodedFqn(entityFQN as string),
setEntityData
);
fetchTaskFeed(taskDetail.id);
@ -439,35 +446,36 @@ const TaskDetailPage = () => {
};
const getCurrentDescription = () => {
let markdown;
let newDescription;
let oldDescription;
if (taskDetail.task?.status === ThreadTaskStatus.Open) {
markdown = taskDetail.task.suggestion;
newDescription = taskDetail.task.suggestion;
oldDescription = !isEmpty(columnObject)
? columnObject.description
: entityData.description;
} else {
markdown = taskDetail.task?.newValue;
newDescription = taskDetail.task?.newValue;
oldDescription = taskDetail.task?.oldValue;
}
return markdown ? (
<RichTextEditorPreviewer
className="tw-p-2"
enableSeeMoreVariant={false}
markdown={markdown}
/>
) : (
<span className="tw-no-description tw-p-2">No description </span>
const diffs = getDescriptionDiff(
oldDescription || '',
newDescription || ''
);
return <DiffView className="tw-p-2" diffArr={diffs} />;
};
const hasEditAccess = () => {
const isOwner = entityData.owner?.id === currentUser?.id;
const isAssignee = taskDetail.task?.assignees?.some(
(assignee) => assignee.id === currentUser?.id
);
const isTaskClosed = taskDetail.task?.status === ThreadTaskStatus.Closed;
const isCreator = taskDetail.createdBy === currentUser?.name;
return (
(isAdminUser || isAuthDisabled || isAssignee || isOwner) && !isTaskClosed
);
const hasEditAccess = () => {
return isAdminUser || isAuthDisabled || isAssignee || isOwner;
};
return (
@ -529,9 +537,7 @@ const TaskDetailPage = () => {
<ColumnDetail column={columnObject} />
<div className="tw-flex tw-mb-4" data-testid="task-assignees">
<span className="tw-text-grey-muted tw-self-center tw-mr-1">
Assignees:
</span>
<span className="tw-text-grey-muted tw-mr-1">Assignees:</span>
{editAssignee ? (
<Fragment>
<Assignees
@ -563,14 +569,32 @@ const TaskDetailPage = () => {
</Fragment>
) : (
<Fragment>
<span className="tw-self-center tw-mr-1">
{taskDetail.task?.assignees
?.map((assignee) => getEntityName(assignee))
?.join(', ')}
<span
className={classNames('tw-self-center tw-mr-1 tw-flex', {
'tw-grid tw-grid-cols-4':
(taskDetail.task?.assignees || []).length > 2,
})}>
{taskDetail.task?.assignees?.map((assignee) => (
<UserPopOverCard
key={uniqueId()}
type={assignee.type}
userName={assignee.name || ''}>
<span className="tw-flex tw-m-1.5 tw-mt-0">
<ProfilePicture
id=""
name={getEntityName(assignee)}
width="20"
/>
<span className="tw-ml-1">
{getEntityName(assignee)}
</span>
{hasEditAccess() && (
</span>
</UserPopOverCard>
))}
</span>
{(hasEditAccess() || isCreator) && !isTaskClosed && (
<button
className="focus:tw-outline-none tw-self-baseline tw-p-2 tw-pl-0"
className="focus:tw-outline-none tw-self-baseline tw-p-2 tw-pt-0 tw-pl-0"
data-testid="edit-suggestion"
onClick={() => setEditAssignee(true)}>
<SVGIcons
@ -586,8 +610,29 @@ const TaskDetailPage = () => {
</div>
<div data-testid="task-description-tabs">
<span className="tw-text-grey-muted">Description:</span>{' '}
<p className="tw-text-grey-muted tw-mb-1">Description:</p>{' '}
{!isEmpty(taskDetail) && (
<Fragment>
{taskDetail.task?.type === TaskType.RequestDescription ? (
<Fragment>
{taskDetail.task.status === ThreadTaskStatus.Open ? (
<RichTextEditor
height="208px"
initialValue={taskDetail.task.suggestion || ''}
placeHolder="Add description"
ref={markdownRef}
/>
) : (
<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 || ''
)}
/>
)}
</Fragment>
) : (
<Fragment>
{showEdit ? (
<DescriptionTabs
@ -598,7 +643,7 @@ const TaskDetailPage = () => {
) : (
<div className="tw-flex tw-border tw-border-main tw-rounded tw-mb-4">
{getCurrentDescription()}
{hasEditAccess() && (
{hasEditAccess() && !isTaskClosed && (
<button
className="focus:tw-outline-none tw-self-baseline tw-p-2 tw-pl-0"
data-testid="edit-suggestion"
@ -615,23 +660,27 @@ const TaskDetailPage = () => {
)}
</Fragment>
)}
</Fragment>
)}
</div>
{hasEditAccess() && (
{hasEditAccess() && !isTaskClosed && (
<div
className="tw-flex tw-justify-end"
data-testid="task-cta-buttons">
{showEdit && (
<Button
className="ant-btn-link-custom"
type="link"
onClick={onTaskReject}>
{showEdit ? 'Cancel' : 'Reject'}
Cancel
</Button>
)}
<Button
className="ant-btn-primary-custom"
type="primary"
onClick={onTaskResolve}>
{showEdit ? 'Submit' : 'Accept'}
Accept
</Button>
</div>
)}

View File

@ -15,7 +15,7 @@ export const cardStyles = {
border: '1px rgb(221, 227, 234) solid',
borderRadius: '8px',
marginBottom: '20px',
boxShadow: '1px 1px 6px rgb(0 0 0 / 12%)',
boxShadow: '1px 1px 6px rgb(0 0 0 / 6%)',
marginTop: '8px',
};
@ -25,4 +25,4 @@ export const contentStyles = {
paddingTop: 0,
};
export const background = { background: '#ffffff' };
export const background = { background: '#F8F9FA' };

View File

@ -11,11 +11,12 @@
* limitations under the License.
*/
import { Button, Card } from 'antd';
import { Button, Card, Input } from 'antd';
import { AxiosError, AxiosResponse } from 'axios';
import { capitalize, isEmpty, isNil, isUndefined } from 'lodash';
import { EditorContentRef, EntityTags } from 'Models';
import React, {
ChangeEvent,
useCallback,
useEffect,
useMemo,
@ -70,6 +71,7 @@ const UpdateDescription = () => {
const [options, setOptions] = useState<Option[]>([]);
const [assignees, setAssignees] = useState<Array<Option>>([]);
const [currentDescription, setCurrentDescription] = useState<string>('');
const [title, setTitle] = useState<string>('');
const entityTier = useMemo(() => {
const tierFQN = getTierTags(entityData.tags || [])?.tagFQN;
@ -138,11 +140,16 @@ const UpdateDescription = () => {
}
};
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,
message: title || message,
about: getEntityFeedLink(entityType, entityFQN, getTaskAbout()),
taskDetails: {
assignees: assignees.map((assignee) => ({
@ -186,6 +193,7 @@ const UpdateDescription = () => {
setAssignees(defaultAssignee);
setOptions(defaultAssignee);
}
setTitle(message);
}, [entityData]);
useEffect(() => {
@ -205,9 +213,18 @@ const UpdateDescription = () => {
className="tw-col-span-2"
key="update-description"
style={{ ...cardStyles }}
title={`Task: ${message}`}>
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 className="tw-text-grey-muted">Assignees:</span>{' '}
<span>Assignees:</span>{' '}
<Assignees
assignees={assignees}
options={options}

View File

@ -12,12 +12,13 @@
*/
import { Tabs } from 'antd';
import { isEqual, uniqueId } from 'lodash';
import { isEqual } from 'lodash';
import { Diff, EditorContentRef } from 'Models';
import React, { useState } from 'react';
import RichTextEditor from '../../../components/common/rich-text-editor/RichTextEditor';
import RichTextEditorPreviewer from '../../../components/common/rich-text-editor/RichTextEditorPreviewer';
import { getDescriptionDiff } from '../../../utils/TasksUtils';
import { DiffView } from './DiffView';
interface Props {
description: string;
@ -42,44 +43,11 @@ export const DescriptionTabs = ({
if (newDescription) {
setDiffs(getDescriptionDiff(description, newDescription));
}
} else {
setDiffs([]);
}
};
const DiffView = ({ diffArr }: { diffArr: Diff[] }) => {
const elements = diffArr.map((diff) => {
if (diff.added) {
return (
<ins className="diff-added" key={uniqueId()}>
{diff.value}
</ins>
);
}
if (diff.removed) {
return (
<del
key={uniqueId()}
style={{ color: '#b30000', background: '#fadad7' }}>
{diff.value}
</del>
);
}
return <div key={uniqueId()}>No diff available</div>;
});
return (
<div className="tw-w-full tw-border tw-border-main tw-p-2 tw-rounded tw-my-3 tw-max-h-52 tw-overflow-y-auto">
<pre className="tw-whitespace-pre-wrap tw-mb-0">
{diffArr.length ? (
elements
) : (
<span className="tw-text-grey-muted">No diff available</span>
)}
</pre>
</div>
);
};
return (
<Tabs
activeKey={activeTab}
@ -101,13 +69,17 @@ export const DescriptionTabs = ({
</div>
</TabPane>
<TabPane key="2" tab="Diff">
<DiffView diffArr={diffs} />
<DiffView
className="tw-border tw-border-main tw-p-2 tw-rounded tw-my-3"
diffArr={diffs}
/>
</TabPane>
<TabPane key="3" tab="New">
<RichTextEditor
className="tw-my-0"
height="208px"
initialValue={suggestion}
placeHolder="Update description"
ref={markdownRef}
/>
</TabPane>

View File

@ -0,0 +1,62 @@
/*
* 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 { uniqueId } from 'lodash';
import { Diff } from 'Models';
import React from 'react';
export const DiffView = ({
diffArr,
className,
}: {
diffArr: Diff[];
className?: string;
}) => {
const elements = diffArr.map((diff) => {
if (diff.added) {
return (
<ins className="diff-added" key={uniqueId()}>
{diff.value}
</ins>
);
}
if (diff.removed) {
return (
<del
key={uniqueId()}
style={{ color: '#b30000', background: '#fadad7' }}>
{diff.value}
</del>
);
}
return <span key={uniqueId()}>{diff.value}</span>;
});
return (
<div
className={classNames(
'tw-w-full tw-max-h-52 tw-overflow-y-auto',
className
)}>
<pre className="tw-whitespace-pre-wrap tw-mb-0">
{diffArr.length ? (
elements
) : (
<span className="tw-text-grey-muted">No diff available</span>
)}
</pre>
</div>
);
};

View File

@ -22,7 +22,7 @@ const TaskPageLayout: FC<Props> = ({ children }) => {
const { Content, Sider } = Layout;
return (
<Layout style={background}>
<Layout style={{ ...background, height: '100vh' }}>
<Sider style={background} width={180} />
<Content style={contentStyles}>{children}</Content>
<Sider style={background} width={180} />

View File

@ -1231,7 +1231,7 @@ code {
.ant-layout-sider-task-detail {
background: #ffffff;
border: 1px solid #dde3ea;
box-shadow: -1px 3px 6px rgba(0, 0, 0, 0.16);
box-shadow: -1px 3px 6px rgba(0, 0, 0, 0.06);
padding: 16px;
padding-top: 0px;
overflow-y: auto;
@ -1296,3 +1296,10 @@ code {
div.ant-typography-ellipsis-custom {
margin-bottom: 0px;
}
.ant-input:hover,
.ant-input:focus {
border-color: #7147e8;
border-right-width: 0px;
box-shadow: none;
}

View File

@ -12,7 +12,7 @@
*/
import { AxiosError, AxiosResponse } from 'axios';
import { diffLines } from 'diff';
import { diffWordsWithSpace } from 'diff';
import { isEqual, isUndefined } from 'lodash';
import { Diff } from 'Models';
import { getDashboardByFqn } from '../axiosAPIs/dashboardAPI';
@ -92,7 +92,7 @@ export const getDescriptionDiff = (
oldValue: string,
newValue: string
): Diff[] => {
return diffLines(oldValue, newValue, { ignoreWhitespace: false });
return diffWordsWithSpace(oldValue, newValue);
};
export const fetchOptions = (