mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-10-08 07:16:29 +00:00
Add check to allow only the owner and admin to delete the post. (#3422)
* Add check to allow only the owner and admin to delete the post. * Change author prop to isAuthor prop * Add unit tests for feedcard component * Add group-hover for feedcard * Add delete posts support in homepage
This commit is contained in:
parent
a6e2ee78c4
commit
c860177a49
@ -44,8 +44,9 @@ export interface FeedBodyProp
|
|||||||
extends HTMLAttributes<HTMLDivElement>,
|
extends HTMLAttributes<HTMLDivElement>,
|
||||||
Pick<ActivityFeedCardProp, 'deletePostHandler'> {
|
Pick<ActivityFeedCardProp, 'deletePostHandler'> {
|
||||||
message: string;
|
message: string;
|
||||||
postId: string;
|
postId?: string;
|
||||||
threadId: string;
|
threadId?: string;
|
||||||
|
isAuthor: boolean;
|
||||||
onConfirmation: (data: ConfirmState) => void;
|
onConfirmation: (data: ConfirmState) => void;
|
||||||
}
|
}
|
||||||
export interface FeedFooterProp
|
export interface FeedFooterProp
|
||||||
|
@ -0,0 +1,78 @@
|
|||||||
|
/*
|
||||||
|
* 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 { findByText, queryByText, render } from '@testing-library/react';
|
||||||
|
import { Post } from 'Models';
|
||||||
|
import React from 'react';
|
||||||
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
|
import ActivityFeedCard from './ActivityFeedCard';
|
||||||
|
|
||||||
|
jest.mock('../../../AppState', () => ({
|
||||||
|
userDetails: {
|
||||||
|
name: '',
|
||||||
|
},
|
||||||
|
users: [{ name: '' }],
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('../../../hooks/authHooks', () => ({
|
||||||
|
useAuth: jest.fn().mockReturnValue({ isAdminUser: false }),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('../../../utils/FeedUtils', () => ({
|
||||||
|
getEntityField: jest.fn(),
|
||||||
|
getEntityFQN: jest.fn(),
|
||||||
|
getEntityType: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('../../Modals/ConfirmationModal/ConfirmationModal', () => {
|
||||||
|
return jest.fn().mockReturnValue(<p>ConfirmationModal</p>);
|
||||||
|
});
|
||||||
|
jest.mock('../FeedCardBody/FeedCardBody', () => {
|
||||||
|
return jest.fn().mockReturnValue(<p>FeedCardBody</p>);
|
||||||
|
});
|
||||||
|
jest.mock('../FeedCardFooter/FeedCardFooter', () => {
|
||||||
|
return jest.fn().mockReturnValue(<p>FeedCardFooter</p>);
|
||||||
|
});
|
||||||
|
jest.mock('../FeedCardHeader/FeedCardHeader', () => {
|
||||||
|
return jest.fn().mockReturnValue(<p>FeedCardHeader</p>);
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockFeedCardProps = {
|
||||||
|
feed: {} as Post,
|
||||||
|
replies: 0,
|
||||||
|
repliedUsers: [],
|
||||||
|
entityLink: '',
|
||||||
|
isEntityFeed: true,
|
||||||
|
threadId: '',
|
||||||
|
lastReplyTimeStamp: 1647322547179,
|
||||||
|
onThreadSelect: jest.fn(),
|
||||||
|
isFooterVisible: false,
|
||||||
|
deletePostHandler: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Test ActivityFeedCard Component', () => {
|
||||||
|
it('Check if ActivityFeedCard component has all child components', async () => {
|
||||||
|
const { container } = render(<ActivityFeedCard {...mockFeedCardProps} />, {
|
||||||
|
wrapper: MemoryRouter,
|
||||||
|
});
|
||||||
|
const feedCardHeader = await findByText(container, /FeedCardHeader/i);
|
||||||
|
const feedCardBody = await findByText(container, /FeedCardBody/i);
|
||||||
|
const feedCardFooter = await findByText(container, /FeedCardFooter/i);
|
||||||
|
const confirmationModal = queryByText(container, /ConfirmationModal/i);
|
||||||
|
|
||||||
|
expect(feedCardHeader).toBeInTheDocument();
|
||||||
|
expect(feedCardBody).toBeInTheDocument();
|
||||||
|
expect(feedCardFooter).toBeInTheDocument();
|
||||||
|
expect(confirmationModal).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
@ -11,268 +11,24 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { AxiosResponse } from 'axios';
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { isUndefined, toLower } from 'lodash';
|
import React, { FC, useState } from 'react';
|
||||||
import React, { FC, Fragment, useState } from 'react';
|
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
import AppState from '../../../AppState';
|
import AppState from '../../../AppState';
|
||||||
import { getUserByName } from '../../../axiosAPIs/userAPI';
|
import { useAuth } from '../../../hooks/authHooks';
|
||||||
import { EntityType, TabSpecificField } from '../../../enums/entity.enum';
|
|
||||||
import { User } from '../../../generated/entity/teams/user';
|
|
||||||
import { getPartialNameFromFQN } from '../../../utils/CommonUtils';
|
|
||||||
import {
|
import {
|
||||||
getEntityField,
|
getEntityField,
|
||||||
getEntityFQN,
|
getEntityFQN,
|
||||||
getEntityType,
|
getEntityType,
|
||||||
getFrontEndFormat,
|
|
||||||
getReplyText,
|
|
||||||
} from '../../../utils/FeedUtils';
|
} from '../../../utils/FeedUtils';
|
||||||
import SVGIcons, { Icons } from '../../../utils/SvgUtils';
|
|
||||||
import { getEntityLink } from '../../../utils/TableUtils';
|
|
||||||
import { getDayTimeByTimeStamp } from '../../../utils/TimeUtils';
|
|
||||||
import Avatar from '../../common/avatar/Avatar';
|
|
||||||
import PopOver from '../../common/popover/PopOver';
|
|
||||||
import RichTextEditorPreviewer from '../../common/rich-text-editor/RichTextEditorPreviewer';
|
|
||||||
import Loader from '../../Loader/Loader';
|
|
||||||
import ConfirmationModal from '../../Modals/ConfirmationModal/ConfirmationModal';
|
import ConfirmationModal from '../../Modals/ConfirmationModal/ConfirmationModal';
|
||||||
|
import FeedCardBody from '../FeedCardBody/FeedCardBody';
|
||||||
|
import FeedCardFooter from '../FeedCardFooter/FeedCardFooter';
|
||||||
|
import FeedCardHeader from '../FeedCardHeader/FeedCardHeader';
|
||||||
import {
|
import {
|
||||||
ActivityFeedCardProp,
|
ActivityFeedCardProp,
|
||||||
ConfirmState,
|
ConfirmState,
|
||||||
FeedBodyProp,
|
|
||||||
FeedFooterProp,
|
|
||||||
FeedHeaderProp,
|
|
||||||
} from './ActivityFeedCard.interface';
|
} from './ActivityFeedCard.interface';
|
||||||
|
|
||||||
const FeedHeader: FC<FeedHeaderProp> = ({
|
|
||||||
className,
|
|
||||||
createdBy,
|
|
||||||
timeStamp,
|
|
||||||
entityFQN,
|
|
||||||
entityType,
|
|
||||||
entityField,
|
|
||||||
isEntityFeed,
|
|
||||||
}) => {
|
|
||||||
const [userData, setUserData] = useState<User>({} as User);
|
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
|
||||||
const [isError, setIsError] = useState(false);
|
|
||||||
|
|
||||||
const onMousEnterHandler = () => {
|
|
||||||
getUserByName(createdBy, 'profile,roles,teams,follows,owns')
|
|
||||||
.then((res: AxiosResponse) => {
|
|
||||||
setUserData(res.data);
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
setIsError(true);
|
|
||||||
})
|
|
||||||
.finally(() => setIsLoading(false));
|
|
||||||
};
|
|
||||||
|
|
||||||
const getUserData = () => {
|
|
||||||
const displayName = userData.displayName ?? '';
|
|
||||||
const name = userData.name ?? '';
|
|
||||||
const teams = userData.teams;
|
|
||||||
const roles = userData.roles;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Fragment>
|
|
||||||
{isError ? (
|
|
||||||
<p>Error while getting user data.</p>
|
|
||||||
) : (
|
|
||||||
<div>
|
|
||||||
{isLoading ? (
|
|
||||||
<Loader size="small" />
|
|
||||||
) : (
|
|
||||||
<div>
|
|
||||||
<div className="tw-flex">
|
|
||||||
<div className="tw-mr-2">
|
|
||||||
<Avatar name={createdBy} type="square" width="30" />
|
|
||||||
</div>
|
|
||||||
<div className="tw-self-center">
|
|
||||||
<p>
|
|
||||||
<span className="tw-font-medium tw-mr-2">
|
|
||||||
{displayName}
|
|
||||||
</span>
|
|
||||||
<span className="tw-text-grey-muted">{name}</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="tw-text-left">
|
|
||||||
{teams?.length || roles?.length ? (
|
|
||||||
<hr className="tw-my-2 tw--mx-3" />
|
|
||||||
) : null}
|
|
||||||
{teams?.length ? (
|
|
||||||
<p className="tw-mt-2">
|
|
||||||
<SVGIcons
|
|
||||||
alt="icon"
|
|
||||||
className="tw-w-4"
|
|
||||||
icon={Icons.TEAMS_GREY}
|
|
||||||
/>
|
|
||||||
<span className="tw-mr-2 tw-ml-1 tw-align-middle tw-font-medium">
|
|
||||||
Teams
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
{teams.map((team, i) => (
|
|
||||||
<span
|
|
||||||
className="tw-bg-gray-200 tw-rounded tw-px-1 tw-text-grey-body tw-m-0.5 tw-text-xs"
|
|
||||||
key={i}>
|
|
||||||
{team?.displayName ?? team?.name}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
) : null}
|
|
||||||
{roles?.length ? (
|
|
||||||
<p className="tw-mt-2">
|
|
||||||
<SVGIcons
|
|
||||||
alt="icon"
|
|
||||||
className="tw-w-4"
|
|
||||||
icon={Icons.USERS}
|
|
||||||
/>
|
|
||||||
<span className="tw-mr-2 tw-ml-1 tw-align-middle tw-font-medium">
|
|
||||||
Roles
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
{roles.map((role, i) => (
|
|
||||||
<span
|
|
||||||
className="tw-bg-gray-200 tw-rounded tw-px-1 tw-text-grey-body tw-m-0.5 tw-text-xs"
|
|
||||||
key={i}>
|
|
||||||
{role?.displayName ?? role?.name}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={classNames('tw-flex tw-mb-1.5', className)}>
|
|
||||||
<PopOver
|
|
||||||
hideDelay={500}
|
|
||||||
html={getUserData()}
|
|
||||||
position="top"
|
|
||||||
theme="light"
|
|
||||||
trigger="mouseenter">
|
|
||||||
<span className="tw-cursor-pointer" onMouseEnter={onMousEnterHandler}>
|
|
||||||
<Avatar name={createdBy} type="square" width="30" />
|
|
||||||
</span>
|
|
||||||
</PopOver>
|
|
||||||
<h6 className="tw-flex tw-items-center tw-m-0 tw-heading tw-pl-2">
|
|
||||||
{createdBy}
|
|
||||||
{entityFQN && entityType ? (
|
|
||||||
<span className="tw-pl-1 tw-font-normal">
|
|
||||||
posted on{' '}
|
|
||||||
{isEntityFeed ? (
|
|
||||||
<span className="tw-heading">{entityField}</span>
|
|
||||||
) : (
|
|
||||||
<Fragment>
|
|
||||||
{entityType}{' '}
|
|
||||||
<Link
|
|
||||||
to={`${getEntityLink(
|
|
||||||
entityType as string,
|
|
||||||
entityFQN as string
|
|
||||||
)}${
|
|
||||||
entityType !== EntityType.WEBHOOK
|
|
||||||
? `/${TabSpecificField.ACTIVITY_FEED}`
|
|
||||||
: ''
|
|
||||||
}`}>
|
|
||||||
<button className="link-text" disabled={AppState.isTourOpen}>
|
|
||||||
{getPartialNameFromFQN(
|
|
||||||
entityFQN as string,
|
|
||||||
entityType === 'table' ? ['table'] : ['database']
|
|
||||||
) || entityFQN}
|
|
||||||
</button>
|
|
||||||
</Link>
|
|
||||||
</Fragment>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
<span className="tw-text-grey-muted tw-pl-2 tw-text-xs">
|
|
||||||
{getDayTimeByTimeStamp(timeStamp)}
|
|
||||||
</span>
|
|
||||||
</h6>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const FeedBody: FC<FeedBodyProp> = ({
|
|
||||||
message,
|
|
||||||
className,
|
|
||||||
threadId,
|
|
||||||
postId,
|
|
||||||
deletePostHandler,
|
|
||||||
onConfirmation,
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<Fragment>
|
|
||||||
<div className={className}>
|
|
||||||
<RichTextEditorPreviewer
|
|
||||||
className="activity-feed-card-text"
|
|
||||||
enableSeeMoreVariant={false}
|
|
||||||
markdown={getFrontEndFormat(message)}
|
|
||||||
/>
|
|
||||||
{threadId && postId && deletePostHandler ? (
|
|
||||||
<span
|
|
||||||
className="tw-opacity-0 hover:tw-opacity-100 tw-cursor-pointer"
|
|
||||||
onClick={() => onConfirmation({ state: true, postId, threadId })}>
|
|
||||||
<SVGIcons alt="delete" icon={Icons.DELETE} width="12px" />
|
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const FeedFooter: FC<FeedFooterProp> = ({
|
|
||||||
repliedUsers,
|
|
||||||
replies,
|
|
||||||
className,
|
|
||||||
threadId,
|
|
||||||
onThreadSelect,
|
|
||||||
lastReplyTimeStamp,
|
|
||||||
isFooterVisible,
|
|
||||||
}) => {
|
|
||||||
const repliesCount = isUndefined(replies) ? 0 : replies;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={className}>
|
|
||||||
{!isUndefined(repliedUsers) &&
|
|
||||||
!isUndefined(replies) &&
|
|
||||||
isFooterVisible ? (
|
|
||||||
<div className="tw-flex tw-group">
|
|
||||||
{repliedUsers?.map((u, i) => (
|
|
||||||
<Avatar
|
|
||||||
className="tw-mt-0.5 tw-mx-0.5"
|
|
||||||
key={i}
|
|
||||||
name={u}
|
|
||||||
type="square"
|
|
||||||
width="22"
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
<p
|
|
||||||
className="tw-ml-1 link-text tw-text-xs tw-mt-1.5 tw-underline"
|
|
||||||
onClick={() => onThreadSelect?.(threadId as string)}>
|
|
||||||
{getReplyText(repliesCount)}
|
|
||||||
</p>
|
|
||||||
{lastReplyTimeStamp && repliesCount > 0 ? (
|
|
||||||
<span className="tw-text-grey-muted tw-pl-2 tw-text-xs tw-font-medium tw-mt-1.5">
|
|
||||||
Last reply{' '}
|
|
||||||
{toLower(getDayTimeByTimeStamp(lastReplyTimeStamp as number))}
|
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const ActivityFeedCard: FC<ActivityFeedCardProp> = ({
|
const ActivityFeedCard: FC<ActivityFeedCardProp> = ({
|
||||||
feed,
|
feed,
|
||||||
className,
|
className,
|
||||||
@ -290,6 +46,9 @@ const ActivityFeedCard: FC<ActivityFeedCardProp> = ({
|
|||||||
const entityFQN = getEntityFQN(entityLink as string);
|
const entityFQN = getEntityFQN(entityLink as string);
|
||||||
const entityField = getEntityField(entityLink as string);
|
const entityField = getEntityField(entityLink as string);
|
||||||
|
|
||||||
|
const { isAdminUser } = useAuth();
|
||||||
|
const currentUser = AppState.userDetails?.name ?? AppState.users[0]?.name;
|
||||||
|
|
||||||
const [confirmationState, setConfirmationState] = useState<ConfirmState>({
|
const [confirmationState, setConfirmationState] = useState<ConfirmState>({
|
||||||
state: false,
|
state: false,
|
||||||
threadId: undefined,
|
threadId: undefined,
|
||||||
@ -317,7 +76,7 @@ const ActivityFeedCard: FC<ActivityFeedCardProp> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classNames(className)}>
|
<div className={classNames(className)}>
|
||||||
<FeedHeader
|
<FeedCardHeader
|
||||||
createdBy={feed.from}
|
createdBy={feed.from}
|
||||||
entityFQN={entityFQN as string}
|
entityFQN={entityFQN as string}
|
||||||
entityField={entityField as string}
|
entityField={entityField as string}
|
||||||
@ -325,15 +84,16 @@ const ActivityFeedCard: FC<ActivityFeedCardProp> = ({
|
|||||||
isEntityFeed={isEntityFeed}
|
isEntityFeed={isEntityFeed}
|
||||||
timeStamp={feed.postTs}
|
timeStamp={feed.postTs}
|
||||||
/>
|
/>
|
||||||
<FeedBody
|
<FeedCardBody
|
||||||
className="tw-mx-7 tw-ml-9 tw-bg-white tw-p-3 tw-border tw-border-main tw-rounded-md tw-break-all tw-flex tw-justify-between "
|
className="tw-mx-7 tw-ml-9 tw-bg-white tw-p-3 tw-border tw-border-main tw-rounded-md tw-break-all tw-flex tw-justify-between "
|
||||||
deletePostHandler={deletePostHandler}
|
deletePostHandler={deletePostHandler}
|
||||||
|
isAuthor={Boolean(feed.from === currentUser || isAdminUser)}
|
||||||
message={feed.message}
|
message={feed.message}
|
||||||
postId={feed.id}
|
postId={feed.id}
|
||||||
threadId={threadId as string}
|
threadId={threadId as string}
|
||||||
onConfirmation={onConfirmation}
|
onConfirmation={onConfirmation}
|
||||||
/>
|
/>
|
||||||
<FeedFooter
|
<FeedCardFooter
|
||||||
className="tw-ml-9 tw-mt-3"
|
className="tw-ml-9 tw-mt-3"
|
||||||
isFooterVisible={isFooterVisible}
|
isFooterVisible={isFooterVisible}
|
||||||
lastReplyTimeStamp={lastReplyTimeStamp}
|
lastReplyTimeStamp={lastReplyTimeStamp}
|
||||||
|
@ -17,11 +17,10 @@ import { EntityThread } from 'Models';
|
|||||||
import React, { FC, Fragment, useEffect, useState } from 'react';
|
import React, { FC, Fragment, useEffect, useState } from 'react';
|
||||||
import { withLoader } from '../../../hoc/withLoader';
|
import { withLoader } from '../../../hoc/withLoader';
|
||||||
import { getFeedListWithRelativeDays } from '../../../utils/FeedUtils';
|
import { getFeedListWithRelativeDays } from '../../../utils/FeedUtils';
|
||||||
import ActivityFeedCard, {
|
import ActivityFeedCard from '../ActivityFeedCard/ActivityFeedCard';
|
||||||
FeedFooter,
|
|
||||||
} from '../ActivityFeedCard/ActivityFeedCard';
|
|
||||||
import ActivityFeedEditor from '../ActivityFeedEditor/ActivityFeedEditor';
|
import ActivityFeedEditor from '../ActivityFeedEditor/ActivityFeedEditor';
|
||||||
import ActivityFeedPanel from '../ActivityFeedPanel/ActivityFeedPanel';
|
import ActivityFeedPanel from '../ActivityFeedPanel/ActivityFeedPanel';
|
||||||
|
import FeedCardFooter from '../FeedCardFooter/FeedCardFooter';
|
||||||
import NoFeedPlaceholder from '../NoFeedPlaceholder/NoFeedPlaceholder';
|
import NoFeedPlaceholder from '../NoFeedPlaceholder/NoFeedPlaceholder';
|
||||||
import {
|
import {
|
||||||
ActivityFeedListProp,
|
ActivityFeedListProp,
|
||||||
@ -87,7 +86,7 @@ const FeedListBody: FC<FeedListBodyProp> = ({
|
|||||||
{postLength > 1 ? (
|
{postLength > 1 ? (
|
||||||
<div className="tw-mb-6">
|
<div className="tw-mb-6">
|
||||||
<div className="tw-ml-9 tw-flex tw-mb-6">
|
<div className="tw-ml-9 tw-flex tw-mb-6">
|
||||||
<FeedFooter
|
<FeedCardFooter
|
||||||
isFooterVisible
|
isFooterVisible
|
||||||
className="tw--mt-4"
|
className="tw--mt-4"
|
||||||
lastReplyTimeStamp={lastPost?.postTs}
|
lastReplyTimeStamp={lastPost?.postTs}
|
||||||
@ -159,6 +158,7 @@ const ActivityFeedList: FC<ActivityFeedListProp> = ({
|
|||||||
|
|
||||||
const onThreadIdSelect = (id: string) => {
|
const onThreadIdSelect = (id: string) => {
|
||||||
setSelctedThreadId(id);
|
setSelctedThreadId(id);
|
||||||
|
setSelectedThread(undefined);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onThreadIdDeselect = () => {
|
const onThreadIdDeselect = () => {
|
||||||
|
@ -23,15 +23,14 @@ import {
|
|||||||
getFeedListWithRelativeDays,
|
getFeedListWithRelativeDays,
|
||||||
getReplyText,
|
getReplyText,
|
||||||
} from '../../../utils/FeedUtils';
|
} from '../../../utils/FeedUtils';
|
||||||
import ActivityFeedCard, {
|
import ActivityFeedCard from '../ActivityFeedCard/ActivityFeedCard';
|
||||||
FeedFooter,
|
|
||||||
} from '../ActivityFeedCard/ActivityFeedCard';
|
|
||||||
import ActivityFeedEditor from '../ActivityFeedEditor/ActivityFeedEditor';
|
import ActivityFeedEditor from '../ActivityFeedEditor/ActivityFeedEditor';
|
||||||
import { FeedListSeparator } from '../ActivityFeedList/ActivityFeedList';
|
import { FeedListSeparator } from '../ActivityFeedList/ActivityFeedList';
|
||||||
import {
|
import {
|
||||||
FeedPanelHeader,
|
FeedPanelHeader,
|
||||||
FeedPanelOverlay,
|
FeedPanelOverlay,
|
||||||
} from '../ActivityFeedPanel/ActivityFeedPanel';
|
} from '../ActivityFeedPanel/ActivityFeedPanel';
|
||||||
|
import FeedCardFooter from '../FeedCardFooter/FeedCardFooter';
|
||||||
import {
|
import {
|
||||||
ActivityThreadListProp,
|
ActivityThreadListProp,
|
||||||
ActivityThreadPanelProp,
|
ActivityThreadPanelProp,
|
||||||
@ -88,7 +87,7 @@ const ActivityThreadList: FC<ActivityThreadListProp> = ({
|
|||||||
{postLength > 1 ? (
|
{postLength > 1 ? (
|
||||||
<div className="tw-mb-6">
|
<div className="tw-mb-6">
|
||||||
<div className="tw-ml-9 tw-flex tw-mb-6">
|
<div className="tw-ml-9 tw-flex tw-mb-6">
|
||||||
<FeedFooter
|
<FeedCardFooter
|
||||||
isFooterVisible
|
isFooterVisible
|
||||||
className="tw--mt-4"
|
className="tw--mt-4"
|
||||||
lastReplyTimeStamp={lastPost?.postTs}
|
lastReplyTimeStamp={lastPost?.postTs}
|
||||||
|
@ -0,0 +1,118 @@
|
|||||||
|
/*
|
||||||
|
* 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 {
|
||||||
|
findByTestId,
|
||||||
|
findByText,
|
||||||
|
queryByTestId,
|
||||||
|
render,
|
||||||
|
} from '@testing-library/react';
|
||||||
|
import React from 'react';
|
||||||
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
|
import FeedCardBody from './FeedCardBody';
|
||||||
|
|
||||||
|
jest.mock('../../../utils/FeedUtils', () => ({
|
||||||
|
getFrontEndFormat: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('../../common/rich-text-editor/RichTextEditorPreviewer', () => {
|
||||||
|
return jest.fn().mockReturnValue(<p>RichText Preview</p>);
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockFeedCardBodyProps = {
|
||||||
|
isAuthor: true,
|
||||||
|
message: 'xyz',
|
||||||
|
threadId: 'id1',
|
||||||
|
postId: 'id2',
|
||||||
|
deletePostHandler: jest.fn(),
|
||||||
|
onConfirmation: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Test FeedCardBody component', () => {
|
||||||
|
it('Check if FeedCardBody has all the child elements', async () => {
|
||||||
|
const { container } = render(<FeedCardBody {...mockFeedCardBodyProps} />, {
|
||||||
|
wrapper: MemoryRouter,
|
||||||
|
});
|
||||||
|
|
||||||
|
const messagePreview = await findByText(container, /RichText Preview/i);
|
||||||
|
const deleteButton = await findByTestId(container, 'delete-button');
|
||||||
|
|
||||||
|
expect(messagePreview).toBeInTheDocument();
|
||||||
|
expect(deleteButton).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Check if FeedCardBody has isAuthor as false', async () => {
|
||||||
|
const { container } = render(
|
||||||
|
<FeedCardBody {...mockFeedCardBodyProps} isAuthor={false} />,
|
||||||
|
{
|
||||||
|
wrapper: MemoryRouter,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const deleteButton = queryByTestId(container, 'delete-button');
|
||||||
|
|
||||||
|
expect(deleteButton).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Check if FeedCardBody has deletePostHandler as undefined', async () => {
|
||||||
|
const { container } = render(
|
||||||
|
<FeedCardBody {...mockFeedCardBodyProps} deletePostHandler={undefined} />,
|
||||||
|
{
|
||||||
|
wrapper: MemoryRouter,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const deleteButton = queryByTestId(container, 'delete-button');
|
||||||
|
|
||||||
|
expect(deleteButton).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Check if FeedCardBody has postId as undefined', async () => {
|
||||||
|
const { container } = render(
|
||||||
|
<FeedCardBody {...mockFeedCardBodyProps} postId={undefined} />,
|
||||||
|
{
|
||||||
|
wrapper: MemoryRouter,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const deleteButton = queryByTestId(container, 'delete-button');
|
||||||
|
|
||||||
|
expect(deleteButton).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Check if FeedCardBody has threadId as undefined', async () => {
|
||||||
|
const { container } = render(
|
||||||
|
<FeedCardBody {...mockFeedCardBodyProps} threadId={undefined} />,
|
||||||
|
{
|
||||||
|
wrapper: MemoryRouter,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const deleteButton = queryByTestId(container, 'delete-button');
|
||||||
|
|
||||||
|
expect(deleteButton).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Check if FeedCardBody has postId, threadId and deletePostHandler as undefined and isAuthor as false', async () => {
|
||||||
|
const { container } = render(
|
||||||
|
<FeedCardBody {...mockFeedCardBodyProps} threadId={undefined} />,
|
||||||
|
{
|
||||||
|
wrapper: MemoryRouter,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const deleteButton = queryByTestId(container, 'delete-button');
|
||||||
|
|
||||||
|
expect(deleteButton).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,51 @@
|
|||||||
|
/*
|
||||||
|
* 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 React, { FC, Fragment } from 'react';
|
||||||
|
import { getFrontEndFormat } from '../../../utils/FeedUtils';
|
||||||
|
import SVGIcons, { Icons } from '../../../utils/SvgUtils';
|
||||||
|
import RichTextEditorPreviewer from '../../common/rich-text-editor/RichTextEditorPreviewer';
|
||||||
|
import { FeedBodyProp } from '../ActivityFeedCard/ActivityFeedCard.interface';
|
||||||
|
|
||||||
|
const FeedCardBody: FC<FeedBodyProp> = ({
|
||||||
|
isAuthor,
|
||||||
|
message,
|
||||||
|
className,
|
||||||
|
threadId,
|
||||||
|
postId,
|
||||||
|
deletePostHandler,
|
||||||
|
onConfirmation,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<div className={classNames('tw-group', className)}>
|
||||||
|
<RichTextEditorPreviewer
|
||||||
|
className="activity-feed-card-text"
|
||||||
|
enableSeeMoreVariant={false}
|
||||||
|
markdown={getFrontEndFormat(message)}
|
||||||
|
/>
|
||||||
|
{threadId && postId && deletePostHandler && isAuthor ? (
|
||||||
|
<span
|
||||||
|
className="tw-opacity-0 group-hover:tw-opacity-100 tw-cursor-pointer"
|
||||||
|
data-testid="delete-button"
|
||||||
|
onClick={() => onConfirmation({ state: true, postId, threadId })}>
|
||||||
|
<SVGIcons alt="delete" icon={Icons.DELETE} width="12px" />
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FeedCardBody;
|
@ -0,0 +1,135 @@
|
|||||||
|
/*
|
||||||
|
* 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 {
|
||||||
|
findAllByTestId,
|
||||||
|
findByTestId,
|
||||||
|
queryAllByTestId,
|
||||||
|
queryByTestId,
|
||||||
|
render,
|
||||||
|
} from '@testing-library/react';
|
||||||
|
import React from 'react';
|
||||||
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
|
import FeedCardFooter from './FeedCardFooter';
|
||||||
|
|
||||||
|
jest.mock('../../../utils/FeedUtils', () => ({
|
||||||
|
getReplyText: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('../../../utils/TimeUtils', () => ({
|
||||||
|
getDayTimeByTimeStamp: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('../../common/avatar/Avatar', () => {
|
||||||
|
return jest.fn().mockReturnValue(<p data-testid="replied-user">Avatar</p>);
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockFeedCardFooterPorps = {
|
||||||
|
repliedUsers: ['xyz', 'pqr'],
|
||||||
|
replies: 2,
|
||||||
|
threadId: 'id1',
|
||||||
|
onThreadSelect: jest.fn(),
|
||||||
|
lastReplyTimeStamp: 1647322547179,
|
||||||
|
isFooterVisible: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Test FeedCardFooter component', () => {
|
||||||
|
it('Check if FeedCardFooter has all child elements', async () => {
|
||||||
|
const { container } = render(
|
||||||
|
<FeedCardFooter {...mockFeedCardFooterPorps} />,
|
||||||
|
{
|
||||||
|
wrapper: MemoryRouter,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const repliedUsers = await findAllByTestId(container, 'replied-user');
|
||||||
|
const replyCount = await findByTestId(container, 'reply-count');
|
||||||
|
const lastReply = await findByTestId(container, 'last-reply');
|
||||||
|
|
||||||
|
expect(repliedUsers).toHaveLength(2);
|
||||||
|
expect(replyCount).toBeInTheDocument();
|
||||||
|
expect(lastReply).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Check if FeedCardFooter has isFooterVisible as false', async () => {
|
||||||
|
const { container } = render(
|
||||||
|
<FeedCardFooter {...mockFeedCardFooterPorps} isFooterVisible={false} />,
|
||||||
|
{
|
||||||
|
wrapper: MemoryRouter,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const repliedUsers = queryAllByTestId(container, 'replied-user');
|
||||||
|
const replyCount = queryByTestId(container, 'reply-count');
|
||||||
|
const lastReply = queryByTestId(container, 'last-reply');
|
||||||
|
|
||||||
|
expect(repliedUsers).toHaveLength(0);
|
||||||
|
expect(replyCount).not.toBeInTheDocument();
|
||||||
|
expect(lastReply).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Check if FeedCardFooter has repliedUsers as undefined', async () => {
|
||||||
|
const { container } = render(
|
||||||
|
<FeedCardFooter {...mockFeedCardFooterPorps} repliedUsers={undefined} />,
|
||||||
|
{
|
||||||
|
wrapper: MemoryRouter,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const repliedUsers = queryAllByTestId(container, 'replied-user');
|
||||||
|
const replyCount = queryByTestId(container, 'reply-count');
|
||||||
|
const lastReply = queryByTestId(container, 'last-reply');
|
||||||
|
|
||||||
|
expect(repliedUsers).toHaveLength(0);
|
||||||
|
expect(replyCount).not.toBeInTheDocument();
|
||||||
|
expect(lastReply).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Check if FeedCardFooter has replies as undefined', async () => {
|
||||||
|
const { container } = render(
|
||||||
|
<FeedCardFooter {...mockFeedCardFooterPorps} replies={undefined} />,
|
||||||
|
{
|
||||||
|
wrapper: MemoryRouter,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const repliedUsers = queryAllByTestId(container, 'replied-user');
|
||||||
|
const replyCount = queryByTestId(container, 'reply-count');
|
||||||
|
const lastReply = queryByTestId(container, 'last-reply');
|
||||||
|
|
||||||
|
expect(repliedUsers).toHaveLength(0);
|
||||||
|
expect(replyCount).not.toBeInTheDocument();
|
||||||
|
expect(lastReply).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Check if FeedCardFooter has lastReplyTimeStamp as undefined and replies as 0', async () => {
|
||||||
|
const { container } = render(
|
||||||
|
<FeedCardFooter
|
||||||
|
{...mockFeedCardFooterPorps}
|
||||||
|
lastReplyTimeStamp={undefined}
|
||||||
|
replies={0}
|
||||||
|
/>,
|
||||||
|
{
|
||||||
|
wrapper: MemoryRouter,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const repliedUsers = queryAllByTestId(container, 'replied-user');
|
||||||
|
const replyCount = queryByTestId(container, 'reply-count');
|
||||||
|
const lastReply = queryByTestId(container, 'last-reply');
|
||||||
|
|
||||||
|
expect(repliedUsers).toHaveLength(2);
|
||||||
|
expect(replyCount).toBeInTheDocument();
|
||||||
|
expect(lastReply).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,68 @@
|
|||||||
|
/*
|
||||||
|
* 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 { isUndefined, toLower } from 'lodash';
|
||||||
|
import React, { FC } from 'react';
|
||||||
|
import { getReplyText } from '../../../utils/FeedUtils';
|
||||||
|
import { getDayTimeByTimeStamp } from '../../../utils/TimeUtils';
|
||||||
|
import Avatar from '../../common/avatar/Avatar';
|
||||||
|
import { FeedFooterProp } from '../ActivityFeedCard/ActivityFeedCard.interface';
|
||||||
|
|
||||||
|
const FeedCardFooter: FC<FeedFooterProp> = ({
|
||||||
|
repliedUsers,
|
||||||
|
replies,
|
||||||
|
className,
|
||||||
|
threadId,
|
||||||
|
onThreadSelect,
|
||||||
|
lastReplyTimeStamp,
|
||||||
|
isFooterVisible,
|
||||||
|
}) => {
|
||||||
|
const repliesCount = isUndefined(replies) ? 0 : replies;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={className}>
|
||||||
|
{!isUndefined(repliedUsers) &&
|
||||||
|
!isUndefined(replies) &&
|
||||||
|
isFooterVisible ? (
|
||||||
|
<div className="tw-flex tw-group">
|
||||||
|
{repliedUsers?.map((u, i) => (
|
||||||
|
<Avatar
|
||||||
|
className="tw-mt-0.5 tw-mx-0.5"
|
||||||
|
data-testid="replied-user"
|
||||||
|
key={i}
|
||||||
|
name={u}
|
||||||
|
type="square"
|
||||||
|
width="22"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<p
|
||||||
|
className="tw-ml-1 link-text tw-text-xs tw-mt-1.5 tw-underline"
|
||||||
|
data-testid="reply-count"
|
||||||
|
onClick={() => onThreadSelect?.(threadId as string)}>
|
||||||
|
{getReplyText(repliesCount)}
|
||||||
|
</p>
|
||||||
|
{lastReplyTimeStamp && repliesCount > 0 ? (
|
||||||
|
<span
|
||||||
|
className="tw-text-grey-muted tw-pl-2 tw-text-xs tw-font-medium tw-mt-1.5"
|
||||||
|
data-testid="last-reply">
|
||||||
|
Last reply{' '}
|
||||||
|
{toLower(getDayTimeByTimeStamp(lastReplyTimeStamp as number))}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FeedCardFooter;
|
@ -0,0 +1,104 @@
|
|||||||
|
/*
|
||||||
|
* 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 {
|
||||||
|
findByTestId,
|
||||||
|
findByText,
|
||||||
|
queryByTestId,
|
||||||
|
render,
|
||||||
|
} from '@testing-library/react';
|
||||||
|
import React from 'react';
|
||||||
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
|
import FeedCardHeader from './FeedCardHeader';
|
||||||
|
|
||||||
|
jest.mock('../../../axiosAPIs/userAPI', () => ({
|
||||||
|
getUserByName: jest.fn().mockReturnValue({}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('../../../utils/CommonUtils', () => ({
|
||||||
|
getPartialNameFromFQN: jest.fn().mockReturnValue('feedcard'),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('../../../utils/TableUtils', () => ({
|
||||||
|
getEntityLink: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('../../../utils/TimeUtils', () => ({
|
||||||
|
getDayTimeByTimeStamp: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('../../common/avatar/Avatar', () => {
|
||||||
|
return jest.fn().mockReturnValue(<p>Avatar</p>);
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockFeedHeaderProps = {
|
||||||
|
createdBy: 'xyz',
|
||||||
|
entityFQN: 'x.y.z',
|
||||||
|
entityField: 'z',
|
||||||
|
entityType: 'y',
|
||||||
|
isEntityFeed: true,
|
||||||
|
timeStamp: 1647322547179,
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Test Feedheader Component', () => {
|
||||||
|
it('Checks if the Feedheader component has isEntityFeed as true', async () => {
|
||||||
|
const { container } = render(<FeedCardHeader {...mockFeedHeaderProps} />, {
|
||||||
|
wrapper: MemoryRouter,
|
||||||
|
});
|
||||||
|
const createdBy = await findByText(container, /xyz/i);
|
||||||
|
|
||||||
|
const headerElement = await findByTestId(container, 'headerText');
|
||||||
|
const entityFieldElement = await findByTestId(
|
||||||
|
container,
|
||||||
|
'headerText-entityField'
|
||||||
|
);
|
||||||
|
const entityTypeElement = queryByTestId(container, 'entityType');
|
||||||
|
const entityLinkElement = queryByTestId(container, 'entitylink');
|
||||||
|
const timeStampElement = await findByTestId(container, 'timestamp');
|
||||||
|
|
||||||
|
expect(createdBy).toBeInTheDocument();
|
||||||
|
|
||||||
|
expect(headerElement).toBeInTheDocument();
|
||||||
|
expect(entityFieldElement).toBeInTheDocument();
|
||||||
|
expect(entityTypeElement).not.toBeInTheDocument();
|
||||||
|
expect(entityLinkElement).not.toBeInTheDocument();
|
||||||
|
expect(timeStampElement).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Checks if the Feedheader component has isEntityFeed as false', async () => {
|
||||||
|
const { container } = render(
|
||||||
|
<FeedCardHeader {...mockFeedHeaderProps} isEntityFeed={false} />,
|
||||||
|
{
|
||||||
|
wrapper: MemoryRouter,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const createdBy = await findByText(container, /xyz/i);
|
||||||
|
|
||||||
|
const headerElement = await findByTestId(container, 'headerText');
|
||||||
|
const entityFieldElement = queryByTestId(
|
||||||
|
container,
|
||||||
|
'headerText-entityField'
|
||||||
|
);
|
||||||
|
const entityTypeElement = await findByTestId(container, 'entityType');
|
||||||
|
const entityLinkElement = await findByTestId(container, 'entitylink');
|
||||||
|
const timeStampElement = await findByTestId(container, 'timestamp');
|
||||||
|
|
||||||
|
expect(createdBy).toBeInTheDocument();
|
||||||
|
|
||||||
|
expect(headerElement).toBeInTheDocument();
|
||||||
|
expect(entityFieldElement).not.toBeInTheDocument();
|
||||||
|
expect(entityTypeElement).toBeInTheDocument();
|
||||||
|
expect(entityLinkElement).toBeInTheDocument();
|
||||||
|
expect(timeStampElement).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,197 @@
|
|||||||
|
/*
|
||||||
|
* 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 { AxiosResponse } from 'axios';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import React, { FC, Fragment, useState } from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import AppState from '../../../AppState';
|
||||||
|
import { getUserByName } from '../../../axiosAPIs/userAPI';
|
||||||
|
import { EntityType, TabSpecificField } from '../../../enums/entity.enum';
|
||||||
|
import { User } from '../../../generated/entity/teams/user';
|
||||||
|
import { getPartialNameFromFQN } from '../../../utils/CommonUtils';
|
||||||
|
import SVGIcons, { Icons } from '../../../utils/SvgUtils';
|
||||||
|
import { getEntityLink } from '../../../utils/TableUtils';
|
||||||
|
import { getDayTimeByTimeStamp } from '../../../utils/TimeUtils';
|
||||||
|
import Avatar from '../../common/avatar/Avatar';
|
||||||
|
import PopOver from '../../common/popover/PopOver';
|
||||||
|
import Loader from '../../Loader/Loader';
|
||||||
|
import { FeedHeaderProp } from '../ActivityFeedCard/ActivityFeedCard.interface';
|
||||||
|
|
||||||
|
const FeedCardHeader: FC<FeedHeaderProp> = ({
|
||||||
|
className,
|
||||||
|
createdBy,
|
||||||
|
timeStamp,
|
||||||
|
entityFQN,
|
||||||
|
entityType,
|
||||||
|
entityField,
|
||||||
|
isEntityFeed,
|
||||||
|
}) => {
|
||||||
|
const [userData, setUserData] = useState<User>({} as User);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [isError, setIsError] = useState(false);
|
||||||
|
|
||||||
|
const onMousEnterHandler = () => {
|
||||||
|
getUserByName(createdBy, 'profile,roles,teams,follows,owns')
|
||||||
|
.then((res: AxiosResponse) => {
|
||||||
|
setUserData(res.data);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
setIsError(true);
|
||||||
|
})
|
||||||
|
.finally(() => setIsLoading(false));
|
||||||
|
};
|
||||||
|
|
||||||
|
const getUserData = () => {
|
||||||
|
const displayName = userData.displayName ?? '';
|
||||||
|
const name = userData.name ?? '';
|
||||||
|
const teams = userData.teams;
|
||||||
|
const roles = userData.roles;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
{isError ? (
|
||||||
|
<p>Error while getting user data.</p>
|
||||||
|
) : (
|
||||||
|
<div>
|
||||||
|
{isLoading ? (
|
||||||
|
<Loader size="small" />
|
||||||
|
) : (
|
||||||
|
<div>
|
||||||
|
<div className="tw-flex">
|
||||||
|
<div className="tw-mr-2">
|
||||||
|
<Avatar name={createdBy} type="square" width="30" />
|
||||||
|
</div>
|
||||||
|
<div className="tw-self-center">
|
||||||
|
<p>
|
||||||
|
<span className="tw-font-medium tw-mr-2">
|
||||||
|
{displayName}
|
||||||
|
</span>
|
||||||
|
<span className="tw-text-grey-muted">{name}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="tw-text-left">
|
||||||
|
{teams?.length || roles?.length ? (
|
||||||
|
<hr className="tw-my-2 tw--mx-3" />
|
||||||
|
) : null}
|
||||||
|
{teams?.length ? (
|
||||||
|
<p className="tw-mt-2">
|
||||||
|
<SVGIcons
|
||||||
|
alt="icon"
|
||||||
|
className="tw-w-4"
|
||||||
|
icon={Icons.TEAMS_GREY}
|
||||||
|
/>
|
||||||
|
<span className="tw-mr-2 tw-ml-1 tw-align-middle tw-font-medium">
|
||||||
|
Teams
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
{teams.map((team, i) => (
|
||||||
|
<span
|
||||||
|
className="tw-bg-gray-200 tw-rounded tw-px-1 tw-text-grey-body tw-m-0.5 tw-text-xs"
|
||||||
|
key={i}>
|
||||||
|
{team?.displayName ?? team?.name}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
{roles?.length ? (
|
||||||
|
<p className="tw-mt-2">
|
||||||
|
<SVGIcons
|
||||||
|
alt="icon"
|
||||||
|
className="tw-w-4"
|
||||||
|
icon={Icons.USERS}
|
||||||
|
/>
|
||||||
|
<span className="tw-mr-2 tw-ml-1 tw-align-middle tw-font-medium">
|
||||||
|
Roles
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
{roles.map((role, i) => (
|
||||||
|
<span
|
||||||
|
className="tw-bg-gray-200 tw-rounded tw-px-1 tw-text-grey-body tw-m-0.5 tw-text-xs"
|
||||||
|
key={i}>
|
||||||
|
{role?.displayName ?? role?.name}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classNames('tw-flex tw-mb-1.5', className)}>
|
||||||
|
<PopOver
|
||||||
|
hideDelay={500}
|
||||||
|
html={getUserData()}
|
||||||
|
position="top"
|
||||||
|
theme="light"
|
||||||
|
trigger="mouseenter">
|
||||||
|
<span
|
||||||
|
className="tw-cursor-pointer"
|
||||||
|
data-testid="authorAvatar"
|
||||||
|
onMouseEnter={onMousEnterHandler}>
|
||||||
|
<Avatar name={createdBy} type="square" width="30" />
|
||||||
|
</span>
|
||||||
|
</PopOver>
|
||||||
|
<h6 className="tw-flex tw-items-center tw-m-0 tw-heading tw-pl-2">
|
||||||
|
{createdBy}
|
||||||
|
{entityFQN && entityType ? (
|
||||||
|
<span className="tw-pl-1 tw-font-normal" data-testid="headerText">
|
||||||
|
posted on{' '}
|
||||||
|
{isEntityFeed ? (
|
||||||
|
<span className="tw-heading" data-testid="headerText-entityField">
|
||||||
|
{entityField}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<Fragment>
|
||||||
|
<span data-testid="entityType">{entityType} </span>
|
||||||
|
<Link
|
||||||
|
data-testid="entitylink"
|
||||||
|
to={`${getEntityLink(
|
||||||
|
entityType as string,
|
||||||
|
entityFQN as string
|
||||||
|
)}${
|
||||||
|
entityType !== EntityType.WEBHOOK
|
||||||
|
? `/${TabSpecificField.ACTIVITY_FEED}`
|
||||||
|
: ''
|
||||||
|
}`}>
|
||||||
|
<button className="link-text" disabled={AppState.isTourOpen}>
|
||||||
|
{getPartialNameFromFQN(
|
||||||
|
entityFQN as string,
|
||||||
|
entityType === 'table' ? ['table'] : ['database']
|
||||||
|
) || entityFQN}
|
||||||
|
</button>
|
||||||
|
</Link>
|
||||||
|
</Fragment>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
<span
|
||||||
|
className="tw-text-grey-muted tw-pl-2 tw-text-xs"
|
||||||
|
data-testid="timestamp">
|
||||||
|
{getDayTimeByTimeStamp(timeStamp)}
|
||||||
|
</span>
|
||||||
|
</h6>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FeedCardHeader;
|
@ -50,6 +50,7 @@ const MyData: React.FC<MyDataProps> = ({
|
|||||||
feedFilterHandler,
|
feedFilterHandler,
|
||||||
isFeedLoading,
|
isFeedLoading,
|
||||||
postFeedHandler,
|
postFeedHandler,
|
||||||
|
deletePostHandler,
|
||||||
}: MyDataProps): React.ReactElement => {
|
}: MyDataProps): React.ReactElement => {
|
||||||
const [fieldListVisible, setFieldListVisible] = useState<boolean>(false);
|
const [fieldListVisible, setFieldListVisible] = useState<boolean>(false);
|
||||||
const isMounted = useRef(false);
|
const isMounted = useRef(false);
|
||||||
@ -177,6 +178,7 @@ const MyData: React.FC<MyDataProps> = ({
|
|||||||
<ActivityFeedList
|
<ActivityFeedList
|
||||||
withSidePanel
|
withSidePanel
|
||||||
className=""
|
className=""
|
||||||
|
deletePostHandler={deletePostHandler}
|
||||||
feedList={feedData}
|
feedList={feedData}
|
||||||
isLoading={isFeedLoading}
|
isLoading={isFeedLoading}
|
||||||
postFeedHandler={postFeedHandler}
|
postFeedHandler={postFeedHandler}
|
||||||
|
@ -36,4 +36,5 @@ export interface MyDataProps {
|
|||||||
entityCounts: EntityCounts;
|
entityCounts: EntityCounts;
|
||||||
isFeedLoading?: boolean;
|
isFeedLoading?: boolean;
|
||||||
postFeedHandler: (value: string, id: string) => void;
|
postFeedHandler: (value: string, id: string) => void;
|
||||||
|
deletePostHandler?: (threadId: string, postId: string) => void;
|
||||||
}
|
}
|
||||||
|
@ -24,7 +24,11 @@ import React, { useEffect, useState } from 'react';
|
|||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
import AppState from '../../AppState';
|
import AppState from '../../AppState';
|
||||||
import { getAirflowPipelines } from '../../axiosAPIs/airflowPipelineAPI';
|
import { getAirflowPipelines } from '../../axiosAPIs/airflowPipelineAPI';
|
||||||
import { getFeedsWithFilter, postFeedById } from '../../axiosAPIs/feedsAPI';
|
import {
|
||||||
|
deletePostById,
|
||||||
|
getFeedsWithFilter,
|
||||||
|
postFeedById,
|
||||||
|
} from '../../axiosAPIs/feedsAPI';
|
||||||
import { searchData } from '../../axiosAPIs/miscAPI';
|
import { searchData } from '../../axiosAPIs/miscAPI';
|
||||||
import PageContainerV1 from '../../components/containers/PageContainerV1';
|
import PageContainerV1 from '../../components/containers/PageContainerV1';
|
||||||
import Loader from '../../components/Loader/Loader';
|
import Loader from '../../components/Loader/Loader';
|
||||||
@ -176,6 +180,29 @@ const MyDataPage = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const deletePostHandler = (threadId: string, postId: string) => {
|
||||||
|
deletePostById(threadId, postId)
|
||||||
|
.then((res: AxiosResponse) => {
|
||||||
|
if (res.data) {
|
||||||
|
const { id } = res.data;
|
||||||
|
setEntityThread((pre) => {
|
||||||
|
return pre.map((thread) => {
|
||||||
|
const posts = thread.posts.filter((post) => post.id !== id);
|
||||||
|
|
||||||
|
return { ...thread, posts: posts };
|
||||||
|
});
|
||||||
|
});
|
||||||
|
showToast({
|
||||||
|
variant: 'success',
|
||||||
|
body: 'Post got deleted successfully',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
showToast({ variant: 'error', body: 'Error while deleting post' });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchData(true);
|
fetchData(true);
|
||||||
}, []);
|
}, []);
|
||||||
@ -202,6 +229,7 @@ const MyDataPage = () => {
|
|||||||
!isLoading ? (
|
!isLoading ? (
|
||||||
<MyData
|
<MyData
|
||||||
countServices={countServices}
|
countServices={countServices}
|
||||||
|
deletePostHandler={deletePostHandler}
|
||||||
entityCounts={entityCounts}
|
entityCounts={entityCounts}
|
||||||
error={error}
|
error={error}
|
||||||
feedData={entityThread || []}
|
feedData={entityThread || []}
|
||||||
|
@ -165,7 +165,9 @@ const DatabaseDetails: FunctionComponent = () => {
|
|||||||
key: 'Owner',
|
key: 'Owner',
|
||||||
value:
|
value:
|
||||||
database?.owner?.type === 'team'
|
database?.owner?.type === 'team'
|
||||||
? getTeamDetailsPath(database?.owner?.type || '')
|
? getTeamDetailsPath(
|
||||||
|
database?.owner?.displayName || database?.owner?.name || ''
|
||||||
|
)
|
||||||
: database?.owner?.displayName || database?.owner?.name || '',
|
: database?.owner?.displayName || database?.owner?.name || '',
|
||||||
placeholderText:
|
placeholderText:
|
||||||
database?.owner?.displayName || database?.owner?.name || '',
|
database?.owner?.displayName || database?.owner?.name || '',
|
||||||
|
Loading…
x
Reference in New Issue
Block a user