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

View File

@ -12,9 +12,12 @@
*/ */
import { Card } from 'antd'; import { Card } from 'antd';
import { uniqueId } from 'lodash';
import React, { FC, Fragment } from 'react'; import React, { FC, Fragment } from 'react';
import { Post, ThreadType } from '../../../generated/entity/feed/thread'; import { Post, ThreadType } from '../../../generated/entity/feed/thread';
import { getEntityName } from '../../../utils/CommonUtils'; import { getEntityName } from '../../../utils/CommonUtils';
import UserPopOverCard from '../../common/PopOverCard/UserPopOverCard';
import ProfilePicture from '../../common/ProfilePicture/ProfilePicture';
import { leftPanelAntCardStyle } from '../../containers/PageLayout'; import { leftPanelAntCardStyle } from '../../containers/PageLayout';
import ActivityFeedCard from '../ActivityFeedCard/ActivityFeedCard'; import ActivityFeedCard from '../ActivityFeedCard/ActivityFeedCard';
import FeedCardFooter from '../ActivityFeedCard/FeedCardFooter/FeedCardFooter'; import FeedCardFooter from '../ActivityFeedCard/FeedCardFooter/FeedCardFooter';
@ -150,12 +153,26 @@ const FeedListBody: FC<FeedListBodyProp> = ({
) : null} ) : null}
</div> </div>
{feed.task && ( {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-text-grey-muted">Assignees: </span>
<span className="tw-ml-0.5 tw-align-middle"> <span className="tw-ml-0.5 tw-align-middle tw-grid tw-grid-cols-4">
{feed.task.assignees {feed.task.assignees.map((assignee) => (
.map((assignee) => getEntityName(assignee)) <UserPopOverCard
.join(', ')} 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> </span>
</div> </div>
)} )}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -13,14 +13,18 @@
import { useEffect, useState } from 'react'; 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); const [isMounting, setIsMounting] = useState(true);
useEffect(() => { useEffect(() => {
if (!isMounting) { if (!isMounting) {
fnCallback(); fnCallback();
} }
}, [isMounting]); }, [isMounting, ...(deps || [])]);
useEffect(() => { useEffect(() => {
setIsMounting(false); setIsMounting(false);

View File

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

View File

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

View File

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

View File

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

View File

@ -12,12 +12,13 @@
*/ */
import { Tabs } from 'antd'; import { Tabs } from 'antd';
import { isEqual, uniqueId } from 'lodash'; import { isEqual } from 'lodash';
import { Diff, EditorContentRef } from 'Models'; import { Diff, EditorContentRef } from 'Models';
import React, { useState } from 'react'; import React, { useState } from 'react';
import RichTextEditor from '../../../components/common/rich-text-editor/RichTextEditor'; import RichTextEditor from '../../../components/common/rich-text-editor/RichTextEditor';
import RichTextEditorPreviewer from '../../../components/common/rich-text-editor/RichTextEditorPreviewer'; import RichTextEditorPreviewer from '../../../components/common/rich-text-editor/RichTextEditorPreviewer';
import { getDescriptionDiff } from '../../../utils/TasksUtils'; import { getDescriptionDiff } from '../../../utils/TasksUtils';
import { DiffView } from './DiffView';
interface Props { interface Props {
description: string; description: string;
@ -42,44 +43,11 @@ export const DescriptionTabs = ({
if (newDescription) { if (newDescription) {
setDiffs(getDescriptionDiff(description, 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 ( return (
<Tabs <Tabs
activeKey={activeTab} activeKey={activeTab}
@ -101,13 +69,17 @@ export const DescriptionTabs = ({
</div> </div>
</TabPane> </TabPane>
<TabPane key="2" tab="Diff"> <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>
<TabPane key="3" tab="New"> <TabPane key="3" tab="New">
<RichTextEditor <RichTextEditor
className="tw-my-0" className="tw-my-0"
height="208px" height="208px"
initialValue={suggestion} initialValue={suggestion}
placeHolder="Update description"
ref={markdownRef} ref={markdownRef}
/> />
</TabPane> </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; const { Content, Sider } = Layout;
return ( return (
<Layout style={background}> <Layout style={{ ...background, height: '100vh' }}>
<Sider style={background} width={180} /> <Sider style={background} width={180} />
<Content style={contentStyles}>{children}</Content> <Content style={contentStyles}>{children}</Content>
<Sider style={background} width={180} /> <Sider style={background} width={180} />

View File

@ -1231,7 +1231,7 @@ code {
.ant-layout-sider-task-detail { .ant-layout-sider-task-detail {
background: #ffffff; background: #ffffff;
border: 1px solid #dde3ea; 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: 16px;
padding-top: 0px; padding-top: 0px;
overflow-y: auto; overflow-y: auto;
@ -1296,3 +1296,10 @@ code {
div.ant-typography-ellipsis-custom { div.ant-typography-ellipsis-custom {
margin-bottom: 0px; 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 { AxiosError, AxiosResponse } from 'axios';
import { diffLines } from 'diff'; import { diffWordsWithSpace } from 'diff';
import { isEqual, isUndefined } from 'lodash'; import { isEqual, isUndefined } from 'lodash';
import { Diff } from 'Models'; import { Diff } from 'Models';
import { getDashboardByFqn } from '../axiosAPIs/dashboardAPI'; import { getDashboardByFqn } from '../axiosAPIs/dashboardAPI';
@ -92,7 +92,7 @@ export const getDescriptionDiff = (
oldValue: string, oldValue: string,
newValue: string newValue: string
): Diff[] => { ): Diff[] => {
return diffLines(oldValue, newValue, { ignoreWhitespace: false }); return diffWordsWithSpace(oldValue, newValue);
}; };
export const fetchOptions = ( export const fetchOptions = (