mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-10-12 17:26:43 +00:00
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:
parent
7ad97d8fed
commit
07d882ee50
@ -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">
|
||||||
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
@ -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
|
||||||
|
@ -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 () => {
|
||||||
|
@ -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>
|
||||||
|
@ -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
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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>
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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"
|
||||||
|
@ -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);
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
@ -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' };
|
||||||
|
@ -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}
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -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} />
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
@ -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 = (
|
||||||
|
Loading…
x
Reference in New Issue
Block a user