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:
Sachin Chaurasiya 2022-03-15 18:05:13 +05:30 committed by GitHub
parent a6e2ee78c4
commit c860177a49
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 808 additions and 264 deletions

View File

@ -44,8 +44,9 @@ export interface FeedBodyProp
extends HTMLAttributes<HTMLDivElement>,
Pick<ActivityFeedCardProp, 'deletePostHandler'> {
message: string;
postId: string;
threadId: string;
postId?: string;
threadId?: string;
isAuthor: boolean;
onConfirmation: (data: ConfirmState) => void;
}
export interface FeedFooterProp

View File

@ -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();
});
});

View File

@ -11,268 +11,24 @@
* limitations under the License.
*/
import { AxiosResponse } from 'axios';
import classNames from 'classnames';
import { isUndefined, toLower } from 'lodash';
import React, { FC, Fragment, useState } from 'react';
import { Link } from 'react-router-dom';
import React, { FC, useState } from 'react';
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 { useAuth } from '../../../hooks/authHooks';
import {
getEntityField,
getEntityFQN,
getEntityType,
getFrontEndFormat,
getReplyText,
} 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 FeedCardBody from '../FeedCardBody/FeedCardBody';
import FeedCardFooter from '../FeedCardFooter/FeedCardFooter';
import FeedCardHeader from '../FeedCardHeader/FeedCardHeader';
import {
ActivityFeedCardProp,
ConfirmState,
FeedBodyProp,
FeedFooterProp,
FeedHeaderProp,
} 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> = ({
feed,
className,
@ -290,6 +46,9 @@ const ActivityFeedCard: FC<ActivityFeedCardProp> = ({
const entityFQN = getEntityFQN(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>({
state: false,
threadId: undefined,
@ -317,7 +76,7 @@ const ActivityFeedCard: FC<ActivityFeedCardProp> = ({
return (
<div className={classNames(className)}>
<FeedHeader
<FeedCardHeader
createdBy={feed.from}
entityFQN={entityFQN as string}
entityField={entityField as string}
@ -325,15 +84,16 @@ const ActivityFeedCard: FC<ActivityFeedCardProp> = ({
isEntityFeed={isEntityFeed}
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 "
deletePostHandler={deletePostHandler}
isAuthor={Boolean(feed.from === currentUser || isAdminUser)}
message={feed.message}
postId={feed.id}
threadId={threadId as string}
onConfirmation={onConfirmation}
/>
<FeedFooter
<FeedCardFooter
className="tw-ml-9 tw-mt-3"
isFooterVisible={isFooterVisible}
lastReplyTimeStamp={lastReplyTimeStamp}

View File

@ -17,11 +17,10 @@ import { EntityThread } from 'Models';
import React, { FC, Fragment, useEffect, useState } from 'react';
import { withLoader } from '../../../hoc/withLoader';
import { getFeedListWithRelativeDays } from '../../../utils/FeedUtils';
import ActivityFeedCard, {
FeedFooter,
} from '../ActivityFeedCard/ActivityFeedCard';
import ActivityFeedCard from '../ActivityFeedCard/ActivityFeedCard';
import ActivityFeedEditor from '../ActivityFeedEditor/ActivityFeedEditor';
import ActivityFeedPanel from '../ActivityFeedPanel/ActivityFeedPanel';
import FeedCardFooter from '../FeedCardFooter/FeedCardFooter';
import NoFeedPlaceholder from '../NoFeedPlaceholder/NoFeedPlaceholder';
import {
ActivityFeedListProp,
@ -87,7 +86,7 @@ const FeedListBody: FC<FeedListBodyProp> = ({
{postLength > 1 ? (
<div className="tw-mb-6">
<div className="tw-ml-9 tw-flex tw-mb-6">
<FeedFooter
<FeedCardFooter
isFooterVisible
className="tw--mt-4"
lastReplyTimeStamp={lastPost?.postTs}
@ -159,6 +158,7 @@ const ActivityFeedList: FC<ActivityFeedListProp> = ({
const onThreadIdSelect = (id: string) => {
setSelctedThreadId(id);
setSelectedThread(undefined);
};
const onThreadIdDeselect = () => {

View File

@ -23,15 +23,14 @@ import {
getFeedListWithRelativeDays,
getReplyText,
} from '../../../utils/FeedUtils';
import ActivityFeedCard, {
FeedFooter,
} from '../ActivityFeedCard/ActivityFeedCard';
import ActivityFeedCard from '../ActivityFeedCard/ActivityFeedCard';
import ActivityFeedEditor from '../ActivityFeedEditor/ActivityFeedEditor';
import { FeedListSeparator } from '../ActivityFeedList/ActivityFeedList';
import {
FeedPanelHeader,
FeedPanelOverlay,
} from '../ActivityFeedPanel/ActivityFeedPanel';
import FeedCardFooter from '../FeedCardFooter/FeedCardFooter';
import {
ActivityThreadListProp,
ActivityThreadPanelProp,
@ -88,7 +87,7 @@ const ActivityThreadList: FC<ActivityThreadListProp> = ({
{postLength > 1 ? (
<div className="tw-mb-6">
<div className="tw-ml-9 tw-flex tw-mb-6">
<FeedFooter
<FeedCardFooter
isFooterVisible
className="tw--mt-4"
lastReplyTimeStamp={lastPost?.postTs}

View File

@ -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();
});
});

View File

@ -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;

View File

@ -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();
});
});

View File

@ -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;

View File

@ -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();
});
});

View File

@ -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;

View File

@ -50,6 +50,7 @@ const MyData: React.FC<MyDataProps> = ({
feedFilterHandler,
isFeedLoading,
postFeedHandler,
deletePostHandler,
}: MyDataProps): React.ReactElement => {
const [fieldListVisible, setFieldListVisible] = useState<boolean>(false);
const isMounted = useRef(false);
@ -177,6 +178,7 @@ const MyData: React.FC<MyDataProps> = ({
<ActivityFeedList
withSidePanel
className=""
deletePostHandler={deletePostHandler}
feedList={feedData}
isLoading={isFeedLoading}
postFeedHandler={postFeedHandler}

View File

@ -36,4 +36,5 @@ export interface MyDataProps {
entityCounts: EntityCounts;
isFeedLoading?: boolean;
postFeedHandler: (value: string, id: string) => void;
deletePostHandler?: (threadId: string, postId: string) => void;
}

View File

@ -24,7 +24,11 @@ import React, { useEffect, useState } from 'react';
import { useLocation } from 'react-router-dom';
import AppState from '../../AppState';
import { getAirflowPipelines } from '../../axiosAPIs/airflowPipelineAPI';
import { getFeedsWithFilter, postFeedById } from '../../axiosAPIs/feedsAPI';
import {
deletePostById,
getFeedsWithFilter,
postFeedById,
} from '../../axiosAPIs/feedsAPI';
import { searchData } from '../../axiosAPIs/miscAPI';
import PageContainerV1 from '../../components/containers/PageContainerV1';
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(() => {
fetchData(true);
}, []);
@ -202,6 +229,7 @@ const MyDataPage = () => {
!isLoading ? (
<MyData
countServices={countServices}
deletePostHandler={deletePostHandler}
entityCounts={entityCounts}
error={error}
feedData={entityThread || []}

View File

@ -165,7 +165,9 @@ const DatabaseDetails: FunctionComponent = () => {
key: 'Owner',
value:
database?.owner?.type === 'team'
? getTeamDetailsPath(database?.owner?.type || '')
? getTeamDetailsPath(
database?.owner?.displayName || database?.owner?.name || ''
)
: database?.owner?.displayName || database?.owner?.name || '',
placeholderText:
database?.owner?.displayName || database?.owner?.name || '',