mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-12-25 22:49:12 +00:00
revamp announcement card (#16016)
* revamp announcement card * cypress fix and minor improvement * fix sonar issue * refactor ProfilePicture view and support cypress and fix sonar lint issue * added and fix some unit test * fix and added unit test * changes made as per comments * fix sonar issue * skip cypress failure due to flakiness
This commit is contained in:
parent
5954ecf060
commit
89b083b6f2
@ -17,7 +17,8 @@ import { EntityType, ENTITY_PATH } from '../../constants/Entity.interface';
|
||||
import {
|
||||
createAnnouncement as createAnnouncementUtil,
|
||||
createInactiveAnnouncement as createInactiveAnnouncementUtil,
|
||||
deleteAnnoucement,
|
||||
deleteAnnouncement,
|
||||
replyAnnouncementUtil,
|
||||
} from '../Utils/Annoucement';
|
||||
import {
|
||||
createCustomPropertyForEntity,
|
||||
@ -449,26 +450,30 @@ class EntityClass {
|
||||
|
||||
createAnnouncement() {
|
||||
createAnnouncementUtil({
|
||||
title: 'Cypress annocement',
|
||||
description: 'Cypress annocement description',
|
||||
title: 'Cypress announcement',
|
||||
description: 'Cypress announcement description',
|
||||
});
|
||||
}
|
||||
|
||||
replyAnnouncement() {
|
||||
replyAnnouncementUtil();
|
||||
}
|
||||
|
||||
removeAnnouncement() {
|
||||
deleteAnnoucement();
|
||||
deleteAnnouncement();
|
||||
}
|
||||
|
||||
// Inactive Announcement
|
||||
|
||||
createInactiveAnnouncement() {
|
||||
createInactiveAnnouncementUtil({
|
||||
title: 'Inactive Cypress annocement',
|
||||
description: 'Inactive Cypress annocement description',
|
||||
title: 'Inactive Cypress announcement',
|
||||
description: 'Inactive Cypress announcement description',
|
||||
});
|
||||
}
|
||||
|
||||
removeInactiveAnnouncement() {
|
||||
deleteAnnoucement();
|
||||
deleteAnnouncement();
|
||||
}
|
||||
|
||||
followUnfollowEntity() {
|
||||
|
||||
@ -21,7 +21,7 @@ import {
|
||||
verifyResponseStatusCode,
|
||||
} from '../common';
|
||||
|
||||
const annoucementForm = ({ title, description, startDate, endDate }) => {
|
||||
const announcementForm = ({ title, description, startDate, endDate }) => {
|
||||
cy.get('#title').type(title);
|
||||
|
||||
cy.get('#startTime').click().type(`${startDate}{enter}`);
|
||||
@ -60,7 +60,7 @@ export const createAnnouncement = (announcement) => {
|
||||
cy.get('[data-testid="add-announcement"]').click();
|
||||
cy.get('.ant-modal-header').should('contain', 'Make an announcement');
|
||||
|
||||
annoucementForm({ ...announcement, startDate, endDate });
|
||||
announcementForm({ ...announcement, startDate, endDate });
|
||||
|
||||
// wait time for success toast message
|
||||
verifyResponseStatusCode('@announcementFeed', 200);
|
||||
@ -76,7 +76,7 @@ export const createAnnouncement = (announcement) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const deleteAnnoucement = () => {
|
||||
export const deleteAnnouncement = () => {
|
||||
interceptURL(
|
||||
'GET',
|
||||
'/api/v1/feed?entityLink=*type=Announcement',
|
||||
@ -103,6 +103,62 @@ export const deleteAnnoucement = () => {
|
||||
});
|
||||
};
|
||||
|
||||
export const replyAnnouncementUtil = () => {
|
||||
interceptURL(
|
||||
'GET',
|
||||
'/api/v1/feed?entityLink=*type=Announcement',
|
||||
'announcementFeed'
|
||||
);
|
||||
interceptURL('GET', '/api/v1/feed/*', 'allAnnouncementFeed');
|
||||
interceptURL('POST', '/api/v1/feed/*/posts', 'addAnnouncementReply');
|
||||
|
||||
cy.get('[data-testid="announcement-card"]').click();
|
||||
|
||||
cy.get(
|
||||
'[data-testid="announcement-card"] [data-testid="main-message"]'
|
||||
).trigger('mouseover');
|
||||
|
||||
cy.get('[data-testid="add-reply"]').should('be.visible').click();
|
||||
|
||||
cy.get('[data-testid="send-button"]').should('be.disabled');
|
||||
|
||||
verifyResponseStatusCode('@allAnnouncementFeed', 200);
|
||||
|
||||
cy.get('[data-testid="editor-wrapper"] .ql-editor').type('Reply message');
|
||||
|
||||
cy.get('[data-testid="send-button"]').should('not.disabled').click();
|
||||
|
||||
verifyResponseStatusCode('@addAnnouncementReply', 201);
|
||||
verifyResponseStatusCode('@announcementFeed', 200);
|
||||
verifyResponseStatusCode('@allAnnouncementFeed', 200);
|
||||
|
||||
cy.get('[data-testid="replies"] [data-testid="viewer-container"]').should(
|
||||
'contain',
|
||||
'Reply message'
|
||||
);
|
||||
cy.get('[data-testid="show-reply-thread"]').should('contain', '1 replies');
|
||||
|
||||
// Edit the reply message
|
||||
cy.get('[data-testid="replies"] > [data-testid="main-message"]').trigger(
|
||||
'mouseover'
|
||||
);
|
||||
|
||||
cy.get('[data-testid="edit-message"]').should('be.visible').click();
|
||||
|
||||
cy.get('.feed-message [data-testid="editor-wrapper"] .ql-editor')
|
||||
.clear()
|
||||
.type('Reply message edited');
|
||||
|
||||
cy.get('[data-testid="save-button"]').click();
|
||||
|
||||
cy.get('[data-testid="replies"] [data-testid="viewer-container"]').should(
|
||||
'contain',
|
||||
'Reply message edited'
|
||||
);
|
||||
|
||||
cy.reload();
|
||||
};
|
||||
|
||||
export const createInactiveAnnouncement = (announcement) => {
|
||||
// Create InActive Announcement
|
||||
interceptURL(
|
||||
@ -126,7 +182,7 @@ export const createInactiveAnnouncement = (announcement) => {
|
||||
'yyyy-MM-dd'
|
||||
);
|
||||
|
||||
annoucementForm({
|
||||
announcementForm({
|
||||
...announcement,
|
||||
startDate: InActiveStartDate,
|
||||
endDate: InActiveEndDate,
|
||||
|
||||
@ -101,12 +101,16 @@ describe('Database hierarchy details page', { tags: 'DataAssets' }, () => {
|
||||
entity.renameEntity();
|
||||
});
|
||||
|
||||
it(`Annoucement create & delete`, () => {
|
||||
it(`Announcement create & delete`, () => {
|
||||
entity.createAnnouncement();
|
||||
/**
|
||||
* Todo: Fix the flakiness issue with the Activity feed changes and enable this test
|
||||
*/
|
||||
// entity.replyAnnouncement();
|
||||
entity.removeAnnouncement();
|
||||
});
|
||||
|
||||
it(`Inactive annoucement create & delete`, () => {
|
||||
it(`Inactive announcement create & delete`, () => {
|
||||
entity.createInactiveAnnouncement();
|
||||
entity.removeInactiveAnnouncement();
|
||||
});
|
||||
|
||||
@ -105,12 +105,16 @@ describe('Entity detail page', { tags: 'DataAssets' }, () => {
|
||||
entity.removeGlossary();
|
||||
});
|
||||
|
||||
it(`Annoucement create & delete`, () => {
|
||||
it(`Announcement create & delete`, () => {
|
||||
entity.createAnnouncement();
|
||||
/**
|
||||
* Todo: Fix the flakiness issue with the Activity feed changes and enable this test
|
||||
*/
|
||||
// entity.replyAnnouncement();
|
||||
entity.removeAnnouncement();
|
||||
});
|
||||
|
||||
it(`Inactive annoucement create & delete`, () => {
|
||||
it(`Inactive Announcement create & delete`, () => {
|
||||
entity.createInactiveAnnouncement();
|
||||
entity.removeInactiveAnnouncement();
|
||||
});
|
||||
|
||||
@ -106,12 +106,13 @@ describe('Services detail page', { tags: 'Integration' }, () => {
|
||||
entity.renameEntity();
|
||||
});
|
||||
|
||||
it(`Annoucement create & delete`, () => {
|
||||
it(`Announcement create & delete`, () => {
|
||||
entity.createAnnouncement();
|
||||
entity.replyAnnouncement();
|
||||
entity.removeAnnouncement();
|
||||
});
|
||||
|
||||
it(`Inactive annoucement create & delete`, () => {
|
||||
it(`Inactive Announcement create & delete`, () => {
|
||||
entity.createInactiveAnnouncement();
|
||||
entity.removeInactiveAnnouncement();
|
||||
});
|
||||
|
||||
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 442" fill="none"><path fill="currentColor" d="M481.508 175.336 68.414 3.926C51.011-3.296 31.35-.119 17.105 12.213 2.86 24.547-3.098 43.551 1.558 61.808L38.328 206h180.025c8.284 0 15.001 6.716 15.001 15.001 0 8.284-6.716 15.001-15.001 15.001H38.327L1.558 380.193c-4.656 18.258 1.301 37.262 15.547 49.595 14.274 12.357 33.937 15.495 51.31 8.287L481.51 266.666C500.317 258.862 512 241.364 512 221.001s-11.683-37.862-30.492-45.665Z"/></svg>
|
||||
|
After Width: | Height: | Size: 493 B |
@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 442" fill="none"><path fill="#0968da" d="M481.508 175.336 68.414 3.926C51.011-3.296 31.35-.119 17.105 12.213 2.86 24.547-3.098 43.551 1.558 61.808L38.328 206h180.025c8.284 0 15.001 6.716 15.001 15.001 0 8.284-6.716 15.001-15.001 15.001H38.327L1.558 380.193c-4.656 18.258 1.301 37.262 15.547 49.595 14.274 12.357 33.937 15.495 51.31 8.287L481.51 266.666C500.317 258.862 512 241.364 512 221.001s-11.683-37.862-30.492-45.665Z"/></svg>
|
||||
|
Before Width: | Height: | Size: 488 B |
@ -43,6 +43,7 @@ const FeedCardFooter: FC<FeedFooterProp> = ({
|
||||
onClick={() => onThreadSelect?.(threadId as string)}>
|
||||
{repliedUsers?.map((u, i) => (
|
||||
<ProfilePicture
|
||||
avatarType="outlined"
|
||||
className="m-r-xss"
|
||||
data-testid="replied-user"
|
||||
key={i}
|
||||
|
||||
@ -11,10 +11,12 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import Icon from '@ant-design/icons/lib/components/Icon';
|
||||
import { Button } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
import React, { FC } from 'react';
|
||||
import { ReactComponent as IconPaperPlanePrimary } from '../../../assets/svg/paper-plane-primary.svg';
|
||||
import { ReactComponent as IconPaperPlanePrimary } from '../../../assets/svg/paper-plane-fill.svg';
|
||||
import './send-button.less';
|
||||
|
||||
interface SendButtonProp {
|
||||
editorValue: string;
|
||||
@ -28,12 +30,10 @@ export const SendButton: FC<SendButtonProp> = ({
|
||||
onSaveHandler,
|
||||
}) => (
|
||||
<Button
|
||||
className={classNames('absolute', className)}
|
||||
className={classNames('send-button', className)}
|
||||
data-testid="send-button"
|
||||
disabled={editorValue.length === 0}
|
||||
icon={<IconPaperPlanePrimary height={18} width={18} />}
|
||||
size="small"
|
||||
style={{ bottom: '2px', right: '5px' }}
|
||||
icon={<Icon component={IconPaperPlanePrimary} />}
|
||||
type="text"
|
||||
onClick={onSaveHandler}
|
||||
/>
|
||||
|
||||
@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright 2024 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 (reference) url('../../../styles/variables.less');
|
||||
|
||||
// Define a mixin for repeated styles
|
||||
.buttonStyle() {
|
||||
background: @primary-color;
|
||||
color: @white;
|
||||
}
|
||||
|
||||
.ant-btn.send-button {
|
||||
position: absolute;
|
||||
bottom: 8px;
|
||||
right: 8px;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
padding: 0;
|
||||
border-radius: 50%;
|
||||
|
||||
// Apply the mixin to the base and hover, disabled states
|
||||
.buttonStyle();
|
||||
|
||||
&:hover {
|
||||
.buttonStyle();
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
.buttonStyle();
|
||||
opacity: 0.8;
|
||||
|
||||
&:hover {
|
||||
.buttonStyle();
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -44,7 +44,6 @@ export interface ActivityThreadPanelBodyProp
|
||||
| 'createThread'
|
||||
| 'deletePostHandler'
|
||||
> {
|
||||
editAnnouncementPermission?: boolean;
|
||||
threadType: ThreadType;
|
||||
showHeader?: boolean;
|
||||
}
|
||||
|
||||
@ -42,11 +42,9 @@ import FeedPanelHeader from '../ActivityFeedPanel/FeedPanelHeader';
|
||||
import ActivityThread from './ActivityThread';
|
||||
import ActivityThreadList from './ActivityThreadList';
|
||||
import { ActivityThreadPanelBodyProp } from './ActivityThreadPanel.interface';
|
||||
import AnnouncementThreads from './AnnouncementThreads';
|
||||
|
||||
const ActivityThreadPanelBody: FC<ActivityThreadPanelBodyProp> = ({
|
||||
threadLink,
|
||||
editAnnouncementPermission,
|
||||
onCancel,
|
||||
postFeedHandler,
|
||||
createThread,
|
||||
@ -84,8 +82,6 @@ const ActivityThreadPanelBody: FC<ActivityThreadPanelBodyProp> = ({
|
||||
|
||||
const isTaskClosed = isEqual(taskStatus, ThreadTaskStatus.Closed);
|
||||
|
||||
const isAnnouncementType = threadType === ThreadType.Announcement;
|
||||
|
||||
const getThreads = (after?: string) => {
|
||||
const status = isTaskType ? taskStatus : undefined;
|
||||
setIsThreadLoading(true);
|
||||
@ -295,49 +291,29 @@ const ActivityThreadPanelBody: FC<ActivityThreadPanelBodyProp> = ({
|
||||
/>
|
||||
</Space>
|
||||
)}
|
||||
{(isAnnouncementType || isTaskType) && !isThreadLoading && (
|
||||
{isTaskType && !isThreadLoading && (
|
||||
<ErrorPlaceHolder
|
||||
className="mt-24"
|
||||
type={ERROR_PLACEHOLDER_TYPE.CUSTOM}>
|
||||
{isTaskType ? (
|
||||
<Typography.Paragraph>
|
||||
{isTaskClosed
|
||||
? t('message.no-closed-task')
|
||||
: t('message.no-open-task')}
|
||||
</Typography.Paragraph>
|
||||
) : (
|
||||
<Typography.Paragraph data-testid="announcement-error">
|
||||
{t('message.no-announcement-message')}
|
||||
</Typography.Paragraph>
|
||||
)}
|
||||
<Typography.Paragraph>
|
||||
{isTaskClosed
|
||||
? t('message.no-closed-task')
|
||||
: t('message.no-open-task')}
|
||||
</Typography.Paragraph>
|
||||
</ErrorPlaceHolder>
|
||||
)}
|
||||
</>
|
||||
) : null}
|
||||
{isAnnouncementType ? (
|
||||
<AnnouncementThreads
|
||||
className={classNames(className)}
|
||||
editAnnouncementPermission={editAnnouncementPermission}
|
||||
postFeed={postFeed}
|
||||
selectedThreadId={selectedThreadId}
|
||||
threads={threads}
|
||||
updateThreadHandler={onUpdateThread}
|
||||
onConfirmation={onConfirmation}
|
||||
onThreadIdSelect={onThreadIdSelect}
|
||||
onThreadSelect={onThreadSelect}
|
||||
/>
|
||||
) : (
|
||||
<ActivityThreadList
|
||||
className={classNames(className)}
|
||||
postFeed={postFeed}
|
||||
selectedThreadId={selectedThreadId}
|
||||
threads={threads}
|
||||
updateThreadHandler={onUpdateThread}
|
||||
onConfirmation={onConfirmation}
|
||||
onThreadIdSelect={onThreadIdSelect}
|
||||
onThreadSelect={onThreadSelect}
|
||||
/>
|
||||
)}
|
||||
<ActivityThreadList
|
||||
className={classNames(className)}
|
||||
postFeed={postFeed}
|
||||
selectedThreadId={selectedThreadId}
|
||||
threads={threads}
|
||||
updateThreadHandler={onUpdateThread}
|
||||
onConfirmation={onConfirmation}
|
||||
onThreadIdSelect={onThreadIdSelect}
|
||||
onThreadSelect={onThreadSelect}
|
||||
/>
|
||||
<div
|
||||
data-testid="observer-element"
|
||||
id="observer-element"
|
||||
|
||||
@ -1,175 +0,0 @@
|
||||
/*
|
||||
* Copyright 2022 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 { Card, Divider, Typography } from 'antd';
|
||||
import React, { FC, Fragment } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Post,
|
||||
Thread,
|
||||
ThreadType,
|
||||
} from '../../../generated/entity/feed/thread';
|
||||
import { isActiveAnnouncement } from '../../../utils/AnnouncementsUtils';
|
||||
import { getFeedListWithRelativeDays } from '../../../utils/FeedUtils';
|
||||
import ActivityFeedCard from '../ActivityFeedCard/ActivityFeedCard';
|
||||
import FeedCardFooter from '../ActivityFeedCard/FeedCardFooter/FeedCardFooter';
|
||||
import ActivityFeedEditor from '../ActivityFeedEditor/ActivityFeedEditor';
|
||||
import AnnouncementBadge from '../Shared/AnnouncementBadge';
|
||||
import { ActivityThreadListProp } from './ActivityThreadPanel.interface';
|
||||
import './announcement.less';
|
||||
|
||||
const AnnouncementThreads: FC<ActivityThreadListProp> = ({
|
||||
threads,
|
||||
className,
|
||||
selectedThreadId,
|
||||
onThreadIdSelect,
|
||||
onThreadSelect,
|
||||
onConfirmation,
|
||||
postFeed,
|
||||
updateThreadHandler,
|
||||
editAnnouncementPermission,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { updatedFeedList: updatedThreads } =
|
||||
getFeedListWithRelativeDays(threads);
|
||||
|
||||
const toggleReplyEditor = (id: string) => {
|
||||
onThreadIdSelect(selectedThreadId === id ? '' : id);
|
||||
};
|
||||
|
||||
const activeAnnouncements = updatedThreads.filter(
|
||||
(thread) =>
|
||||
thread.announcement &&
|
||||
isActiveAnnouncement(
|
||||
thread.announcement?.startTime,
|
||||
thread.announcement?.endTime
|
||||
)
|
||||
);
|
||||
|
||||
const inActiveAnnouncements = updatedThreads.filter(
|
||||
(thread) =>
|
||||
!(
|
||||
thread.announcement &&
|
||||
isActiveAnnouncement(
|
||||
thread.announcement?.startTime,
|
||||
thread.announcement?.endTime
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
const getAnnouncements = (announcements: Thread[]) => {
|
||||
return announcements.map((thread, index) => {
|
||||
const mainFeed = {
|
||||
message: thread.message,
|
||||
postTs: thread.threadTs,
|
||||
from: thread.createdBy,
|
||||
id: thread.id,
|
||||
reactions: thread.reactions,
|
||||
} as Post;
|
||||
|
||||
const postLength = thread?.posts?.length || 0;
|
||||
const replies = thread.postsCount ? thread.postsCount - 1 : 0;
|
||||
const repliedUsers = [
|
||||
...new Set((thread?.posts || []).map((f) => f.from)),
|
||||
];
|
||||
const repliedUniqueUsersList = repliedUsers.slice(
|
||||
0,
|
||||
postLength >= 3 ? 2 : 1
|
||||
);
|
||||
const lastPost = thread?.posts?.[postLength - 1];
|
||||
|
||||
return (
|
||||
<Fragment key={index}>
|
||||
<Card
|
||||
className="ant-card-feed announcement-thread-card"
|
||||
data-testid="announcement-card"
|
||||
key={`${index} - card`}>
|
||||
<AnnouncementBadge />
|
||||
<div data-testid="main-message">
|
||||
<ActivityFeedCard
|
||||
isEntityFeed
|
||||
isThread
|
||||
announcementDetails={thread.announcement}
|
||||
editAnnouncementPermission={editAnnouncementPermission}
|
||||
entityLink={thread.about}
|
||||
feed={mainFeed}
|
||||
feedType={thread.type || ThreadType.Conversation}
|
||||
task={thread}
|
||||
threadId={thread.id}
|
||||
updateThreadHandler={updateThreadHandler}
|
||||
onConfirmation={onConfirmation}
|
||||
onReply={() => onThreadSelect(thread.id)}
|
||||
/>
|
||||
</div>
|
||||
{postLength > 0 ? (
|
||||
<div data-testid="replies-container">
|
||||
{postLength > 1 ? (
|
||||
<div>
|
||||
{Boolean(lastPost) && <div />}
|
||||
<div className="d-flex ">
|
||||
<FeedCardFooter
|
||||
isFooterVisible
|
||||
lastReplyTimeStamp={lastPost?.postTs}
|
||||
repliedUsers={repliedUniqueUsersList}
|
||||
replies={replies}
|
||||
threadId={thread.id}
|
||||
onThreadSelect={() => onThreadSelect(thread.id)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
<div data-testid="latest-reply">
|
||||
<ActivityFeedCard
|
||||
isEntityFeed
|
||||
feed={lastPost as Post}
|
||||
feedType={thread.type || ThreadType.Conversation}
|
||||
task={thread}
|
||||
threadId={thread.id}
|
||||
updateThreadHandler={updateThreadHandler}
|
||||
onConfirmation={onConfirmation}
|
||||
onReply={() => toggleReplyEditor(thread.id)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{selectedThreadId === thread.id ? (
|
||||
<div data-testid="quick-reply-editor">
|
||||
<ActivityFeedEditor onSave={postFeed} />
|
||||
</div>
|
||||
) : null}
|
||||
</Card>
|
||||
</Fragment>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
{getAnnouncements(activeAnnouncements)}
|
||||
{Boolean(inActiveAnnouncements.length) && (
|
||||
<>
|
||||
<Typography.Text
|
||||
className="d-block m-t-lg"
|
||||
data-testid="inActive-announcements">
|
||||
{t('label.inactive-announcement-plural')}
|
||||
</Typography.Text>
|
||||
<Divider className="m-t-xs m-b-xlg" />
|
||||
</>
|
||||
)}
|
||||
|
||||
{getAnnouncements(inActiveAnnouncements)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AnnouncementThreads;
|
||||
@ -1,22 +0,0 @@
|
||||
/*
|
||||
* Copyright 2023 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 url('../../../styles/variables.less');
|
||||
|
||||
.announcement-thread-card {
|
||||
margin-top: 20px;
|
||||
padding-top: 8px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid @announcement-border;
|
||||
background: @announcement-background;
|
||||
}
|
||||
@ -11,6 +11,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import Icon from '@ant-design/icons/lib/components/Icon';
|
||||
import { Typography } from 'antd';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@ -22,11 +23,11 @@ const AnnouncementBadge = () => {
|
||||
|
||||
return (
|
||||
<div className="announcement-badge-container">
|
||||
<AnnouncementIcon className="announcement-badge" />
|
||||
<Icon className="announcement-badge" component={AnnouncementIcon} />
|
||||
|
||||
<Typography.Paragraph className="text-xs m-l-xss m-b-0 text-primary">
|
||||
<Typography.Text className="announcement-text">
|
||||
{t('label.announcement')}
|
||||
</Typography.Paragraph>
|
||||
</Typography.Text>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -20,16 +20,23 @@
|
||||
background: @announcement-background-dark;
|
||||
border-radius: 4px;
|
||||
border: 1px solid @announcement-border;
|
||||
padding: 0 8px;
|
||||
padding: 3px 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.announcement-badge {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
color: @primary-color;
|
||||
.announcement-badge {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
color: @announcement-border;
|
||||
}
|
||||
|
||||
.announcement-text {
|
||||
margin-left: 4px;
|
||||
color: @announcement-border;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
.task-badge {
|
||||
|
||||
@ -0,0 +1,91 @@
|
||||
/*
|
||||
* Copyright 2024 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 { Operation } from 'fast-json-patch';
|
||||
import { HTMLAttributes } from 'react';
|
||||
import {
|
||||
AnnouncementDetails,
|
||||
CreateThread,
|
||||
ThreadType,
|
||||
} from '../../generated/api/feed/createThread';
|
||||
import { Post, Thread } from '../../generated/entity/feed/thread';
|
||||
import { ThreadUpdatedFunc } from '../../interface/feed.interface';
|
||||
import { ConfirmState } from '../ActivityFeed/ActivityFeedCard/ActivityFeedCard.interface';
|
||||
|
||||
export type ThreadUpdatedFunction = (
|
||||
threadId: string,
|
||||
postId: string,
|
||||
isThread: boolean,
|
||||
data: Operation[]
|
||||
) => Promise<void>;
|
||||
|
||||
export interface AnnouncementThreadProp extends HTMLAttributes<HTMLDivElement> {
|
||||
threadLink: string;
|
||||
threadType?: ThreadType;
|
||||
open?: boolean;
|
||||
postFeedHandler: (value: string, id: string) => Promise<void>;
|
||||
createThread: (data: CreateThread) => Promise<void>;
|
||||
updateThreadHandler: ThreadUpdatedFunction;
|
||||
onCancel?: () => void;
|
||||
deletePostHandler?: (
|
||||
threadId: string,
|
||||
postId: string,
|
||||
isThread: boolean
|
||||
) => Promise<void>;
|
||||
}
|
||||
|
||||
export interface AnnouncementThreadBodyProp
|
||||
extends HTMLAttributes<HTMLDivElement>,
|
||||
Pick<
|
||||
AnnouncementThreadProp,
|
||||
| 'threadLink'
|
||||
| 'updateThreadHandler'
|
||||
| 'postFeedHandler'
|
||||
| 'deletePostHandler'
|
||||
> {
|
||||
refetchThread: boolean;
|
||||
editPermission: boolean;
|
||||
}
|
||||
|
||||
export interface AnnouncementThreadListProp
|
||||
extends HTMLAttributes<HTMLDivElement>,
|
||||
Pick<AnnouncementThreadProp, 'updateThreadHandler'> {
|
||||
editPermission: boolean;
|
||||
threads: Thread[];
|
||||
postFeed: (value: string, id: string) => Promise<void>;
|
||||
onConfirmation: (data: ConfirmState) => void;
|
||||
}
|
||||
|
||||
export interface AnnouncementFeedCardProp {
|
||||
feed: Post;
|
||||
task: Thread;
|
||||
editPermission: boolean;
|
||||
onConfirmation: (data: ConfirmState) => void;
|
||||
updateThreadHandler: ThreadUpdatedFunction;
|
||||
postFeed: (value: string, id: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export interface AnnouncementFeedCardBodyProp
|
||||
extends HTMLAttributes<HTMLDivElement> {
|
||||
feed: Post;
|
||||
editPermission: boolean;
|
||||
entityLink?: string;
|
||||
isThread?: boolean;
|
||||
task: Thread;
|
||||
announcementDetails?: AnnouncementDetails;
|
||||
showRepliesButton?: boolean;
|
||||
isReplyThreadOpen?: boolean;
|
||||
onReply?: () => void;
|
||||
onConfirmation: (data: ConfirmState) => void;
|
||||
showReplyThread?: () => void;
|
||||
updateThreadHandler: ThreadUpdatedFunc;
|
||||
}
|
||||
@ -0,0 +1,168 @@
|
||||
/*
|
||||
* Copyright 2024 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 { Card, Col, Row } from 'antd';
|
||||
import { AxiosError } from 'axios';
|
||||
import { Operation } from 'fast-json-patch';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Post } from '../../generated/entity/feed/thread';
|
||||
import { getFeedById } from '../../rest/feedsAPI';
|
||||
import { showErrorToast } from '../../utils/ToastUtils';
|
||||
import ActivityFeedEditor from '../ActivityFeed/ActivityFeedEditor/ActivityFeedEditor';
|
||||
import AnnouncementBadge from '../ActivityFeed/Shared/AnnouncementBadge';
|
||||
import ProfilePicture from '../common/ProfilePicture/ProfilePicture';
|
||||
import { AnnouncementFeedCardProp } from './Announcement.interface';
|
||||
import './announcement.less';
|
||||
import AnnouncementFeedCardBody from './AnnouncementFeedCardBody.component';
|
||||
|
||||
const AnnouncementFeedCard = ({
|
||||
feed,
|
||||
task,
|
||||
editPermission,
|
||||
postFeed,
|
||||
onConfirmation,
|
||||
updateThreadHandler,
|
||||
}: AnnouncementFeedCardProp) => {
|
||||
const { t } = useTranslation();
|
||||
const [isReplyThreadVisible, setIsReplyThreadVisible] =
|
||||
useState<boolean>(false);
|
||||
const [postFeedData, setPostFeedData] = useState<Post[]>([]);
|
||||
|
||||
const fetchAnnouncementThreadData = async () => {
|
||||
try {
|
||||
const res = await getFeedById(task.id);
|
||||
setPostFeedData(res.data.posts ?? []);
|
||||
} catch (err) {
|
||||
showErrorToast(
|
||||
err as AxiosError,
|
||||
t('message.entity-fetch-error', {
|
||||
entity: t('label.message-lowercase-plural'),
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdateThreadHandler = async (
|
||||
threadId: string,
|
||||
postId: string,
|
||||
isThread: boolean,
|
||||
data: Operation[]
|
||||
) => {
|
||||
await updateThreadHandler(threadId, postId, isThread, data);
|
||||
|
||||
if (isReplyThreadVisible) {
|
||||
fetchAnnouncementThreadData();
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveReply = async (value: string) => {
|
||||
await postFeed(value, task.id);
|
||||
|
||||
if (isReplyThreadVisible) {
|
||||
fetchAnnouncementThreadData();
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenReplyThread = () => {
|
||||
fetchAnnouncementThreadData();
|
||||
setIsReplyThreadVisible((prev) => !prev);
|
||||
};
|
||||
|
||||
const postFeedReplies = useMemo(
|
||||
() =>
|
||||
postFeedData.map((reply) => (
|
||||
<AnnouncementFeedCardBody
|
||||
editPermission={editPermission}
|
||||
feed={reply}
|
||||
key={reply.id}
|
||||
showRepliesButton={false}
|
||||
task={task}
|
||||
updateThreadHandler={handleUpdateThreadHandler}
|
||||
onConfirmation={onConfirmation}
|
||||
/>
|
||||
)),
|
||||
[
|
||||
task,
|
||||
postFeedData,
|
||||
editPermission,
|
||||
onConfirmation,
|
||||
handleUpdateThreadHandler,
|
||||
]
|
||||
);
|
||||
|
||||
// fetch announcement thread after delete action
|
||||
useEffect(() => {
|
||||
if (postFeedData.length !== task.postsCount) {
|
||||
if (isReplyThreadVisible) {
|
||||
fetchAnnouncementThreadData();
|
||||
}
|
||||
}
|
||||
}, [task.postsCount]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card
|
||||
className="ant-card-feed announcement-thread-card"
|
||||
data-testid="announcement-card">
|
||||
<AnnouncementBadge />
|
||||
<AnnouncementFeedCardBody
|
||||
isThread
|
||||
announcementDetails={task.announcement}
|
||||
editPermission={editPermission}
|
||||
entityLink={task.about}
|
||||
feed={feed}
|
||||
isReplyThreadOpen={isReplyThreadVisible}
|
||||
showReplyThread={handleOpenReplyThread}
|
||||
task={task}
|
||||
updateThreadHandler={handleUpdateThreadHandler}
|
||||
onConfirmation={onConfirmation}
|
||||
onReply={handleOpenReplyThread}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
{isReplyThreadVisible && (
|
||||
<Row className="m-t-lg" gutter={[0, 10]}>
|
||||
<Col span={24}>
|
||||
<Row gutter={[10, 0]} wrap={false}>
|
||||
<Col className="d-flex justify-center" flex="70px">
|
||||
<div className="feed-line" />
|
||||
</Col>
|
||||
<Col flex="auto">
|
||||
<div className="w-full m-l-xs" data-testid="replies">
|
||||
{postFeedReplies}
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Row gutter={[10, 0]} wrap={false}>
|
||||
<Col className="d-flex justify-center" flex="70px">
|
||||
<ProfilePicture
|
||||
avatarType="outlined"
|
||||
className="m-l-xs"
|
||||
name={feed.from}
|
||||
width="24"
|
||||
/>
|
||||
</Col>
|
||||
<Col flex="auto">
|
||||
<ActivityFeedEditor onSave={handleSaveReply} />
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AnnouncementFeedCard;
|
||||
@ -0,0 +1,165 @@
|
||||
/*
|
||||
* Copyright 2024 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 { act, fireEvent, render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import {
|
||||
MOCK_ANNOUNCEMENT_DATA,
|
||||
MOCK_ANNOUNCEMENT_FEED_DATA,
|
||||
} from '../../mocks/Announcement.mock';
|
||||
import { getFeedById } from '../../rest/feedsAPI';
|
||||
import AnnouncementFeedCard from './AnnouncementFeedCard.component';
|
||||
|
||||
jest.mock('../../rest/feedsAPI', () => ({
|
||||
getFeedById: jest.fn().mockImplementation(() => Promise.resolve()),
|
||||
}));
|
||||
|
||||
jest.mock('./AnnouncementFeedCardBody.component', () =>
|
||||
jest
|
||||
.fn()
|
||||
.mockImplementation(
|
||||
({ showReplyThread, updateThreadHandler, onConfirmation, onReply }) => (
|
||||
<>
|
||||
<p>AnnouncementFeedCardBody</p>
|
||||
<button onClick={showReplyThread}>ShowReplyThreadButton</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
updateThreadHandler('threadId', 'postId', true, 'data')
|
||||
}>
|
||||
UpdateThreadHandlerButton
|
||||
</button>
|
||||
<button onClick={onConfirmation}>ConfirmationButton</button>
|
||||
<button onClick={onReply}>ReplyButton</button>
|
||||
</>
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
jest.mock('../ActivityFeed/ActivityFeedEditor/ActivityFeedEditor', () => {
|
||||
return jest.fn().mockImplementation(({ onSave }) => (
|
||||
<>
|
||||
<p>ActivityFeedEditor</p>
|
||||
<button onClick={() => onSave('changesValue')}>onSaveReply</button>
|
||||
</>
|
||||
));
|
||||
});
|
||||
|
||||
jest.mock('../ActivityFeed/Shared/AnnouncementBadge', () => {
|
||||
return jest.fn().mockReturnValue(<p>AnnouncementBadge</p>);
|
||||
});
|
||||
|
||||
jest.mock('../common/ProfilePicture/ProfilePicture', () => {
|
||||
return jest.fn().mockReturnValue(<p>ProfilePicture</p>);
|
||||
});
|
||||
|
||||
jest.mock('../../utils/ToastUtils', () => ({
|
||||
showErrorToast: jest.fn(),
|
||||
}));
|
||||
|
||||
const mockProps = {
|
||||
feed: {
|
||||
message: 'Cypress announcement',
|
||||
postTs: 1714026576902,
|
||||
from: 'admin',
|
||||
id: '36ea94c9-7f12-489c-94df-56cbefe14b2f',
|
||||
reactions: [],
|
||||
},
|
||||
task: MOCK_ANNOUNCEMENT_DATA.data[0],
|
||||
editPermission: true,
|
||||
postFeed: jest.fn(),
|
||||
onConfirmation: jest.fn(),
|
||||
updateThreadHandler: jest.fn(),
|
||||
};
|
||||
|
||||
describe('Test AnnouncementFeedCard Component', () => {
|
||||
it('should render AnnouncementFeedCard component', () => {
|
||||
render(<AnnouncementFeedCard {...mockProps} />);
|
||||
|
||||
expect(screen.getByText('AnnouncementBadge')).toBeInTheDocument();
|
||||
expect(screen.getByText('AnnouncementFeedCardBody')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should trigger onConfirmation', () => {
|
||||
render(<AnnouncementFeedCard {...mockProps} />);
|
||||
|
||||
fireEvent.click(screen.getByText('ConfirmationButton'));
|
||||
|
||||
expect(mockProps.onConfirmation).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should trigger updateThreadHandler without fetchAnnouncementThreadData when replyThread is closed', () => {
|
||||
render(<AnnouncementFeedCard {...mockProps} />);
|
||||
|
||||
fireEvent.click(screen.getByText('UpdateThreadHandlerButton'));
|
||||
|
||||
expect(mockProps.updateThreadHandler).toHaveBeenCalledWith(
|
||||
'threadId',
|
||||
'postId',
|
||||
true,
|
||||
'data'
|
||||
);
|
||||
expect(getFeedById).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should trigger updateThreadHandler with fetchAnnouncementThreadData when replyThread is open', () => {
|
||||
render(<AnnouncementFeedCard {...mockProps} />);
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(screen.getByText('ShowReplyThreadButton'));
|
||||
});
|
||||
|
||||
expect(getFeedById).toHaveBeenCalledWith(MOCK_ANNOUNCEMENT_DATA.data[0].id);
|
||||
|
||||
fireEvent.click(screen.getByText('UpdateThreadHandlerButton'));
|
||||
|
||||
expect(mockProps.updateThreadHandler).toHaveBeenCalledWith(
|
||||
'threadId',
|
||||
'postId',
|
||||
true,
|
||||
'data'
|
||||
);
|
||||
expect(getFeedById).toHaveBeenCalledWith(MOCK_ANNOUNCEMENT_DATA.data[0].id);
|
||||
});
|
||||
|
||||
it('should trigger onReply with fetchAnnouncementThreadData', async () => {
|
||||
(getFeedById as jest.Mock).mockImplementationOnce(() =>
|
||||
Promise.resolve({ data: MOCK_ANNOUNCEMENT_FEED_DATA })
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
render(<AnnouncementFeedCard {...mockProps} />);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByText('ReplyButton'));
|
||||
});
|
||||
|
||||
expect(getFeedById).toHaveBeenCalledWith(MOCK_ANNOUNCEMENT_DATA.data[0].id);
|
||||
|
||||
expect(screen.getByTestId('replies')).toBeInTheDocument();
|
||||
|
||||
expect(screen.getAllByText('AnnouncementFeedCardBody')).toHaveLength(5);
|
||||
|
||||
expect(screen.getByText('ProfilePicture')).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText('ActivityFeedEditor')).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByText('onSaveReply'));
|
||||
|
||||
expect(mockProps.postFeed).toHaveBeenCalledWith(
|
||||
'changesValue',
|
||||
'36ea94c9-7f12-489c-94df-56cbefe14b2f'
|
||||
);
|
||||
|
||||
expect(getFeedById).toHaveBeenCalledWith(MOCK_ANNOUNCEMENT_DATA.data[0].id);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,282 @@
|
||||
/*
|
||||
* Copyright 2024 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 Icon from '@ant-design/icons/lib/components/Icon';
|
||||
import { Avatar, Button, Col, Popover, Row } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
import { compare, Operation } from 'fast-json-patch';
|
||||
import { isEmpty, isUndefined } from 'lodash';
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ReactComponent as ArrowBottom } from '../../assets/svg/ic-arrow-down.svg';
|
||||
import { ReactionOperation } from '../../enums/reactions.enum';
|
||||
import {
|
||||
AnnouncementDetails,
|
||||
ThreadType,
|
||||
} from '../../generated/api/feed/createThread';
|
||||
import { Post } from '../../generated/entity/feed/thread';
|
||||
import { Reaction, ReactionType } from '../../generated/type/reaction';
|
||||
import { useApplicationStore } from '../../hooks/useApplicationStore';
|
||||
import {
|
||||
getEntityField,
|
||||
getEntityFQN,
|
||||
getEntityType,
|
||||
} from '../../utils/FeedUtils';
|
||||
import FeedCardBody from '../ActivityFeed/ActivityFeedCard/FeedCardBody/FeedCardBody';
|
||||
import FeedCardHeader from '../ActivityFeed/ActivityFeedCard/FeedCardHeader/FeedCardHeader';
|
||||
import PopoverContent from '../ActivityFeed/ActivityFeedCard/PopoverContent';
|
||||
import UserPopOverCard from '../common/PopOverCard/UserPopOverCard';
|
||||
import ProfilePicture from '../common/ProfilePicture/ProfilePicture';
|
||||
import EditAnnouncementModal from '../Modals/AnnouncementModal/EditAnnouncementModal';
|
||||
import { AnnouncementFeedCardBodyProp } from './Announcement.interface';
|
||||
import './announcement.less';
|
||||
|
||||
const AnnouncementFeedCardBody = ({
|
||||
feed,
|
||||
entityLink,
|
||||
isThread,
|
||||
editPermission,
|
||||
showRepliesButton = true,
|
||||
showReplyThread,
|
||||
onReply,
|
||||
announcementDetails,
|
||||
onConfirmation,
|
||||
updateThreadHandler,
|
||||
task,
|
||||
isReplyThreadOpen,
|
||||
}: AnnouncementFeedCardBodyProp) => {
|
||||
const { t } = useTranslation();
|
||||
const entityType = getEntityType(entityLink ?? '');
|
||||
const entityFQN = getEntityFQN(entityLink ?? '');
|
||||
const entityField = getEntityField(entityLink ?? '');
|
||||
const { currentUser } = useApplicationStore();
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [feedDetail, setFeedDetail] = useState<Post>(feed);
|
||||
|
||||
const [visible, setVisible] = useState<boolean>(false);
|
||||
const [isEditAnnouncement, setIsEditAnnouncement] = useState<boolean>(false);
|
||||
const [isEditPost, setIsEditPost] = useState<boolean>(false);
|
||||
|
||||
const isAuthor = feedDetail.from === currentUser?.name;
|
||||
|
||||
const { id: threadId, type: feedType, posts } = task;
|
||||
|
||||
const repliesPostAvatarGroup = useMemo(() => {
|
||||
return (
|
||||
<Avatar.Group>
|
||||
{(posts ?? []).map((u) => (
|
||||
<ProfilePicture
|
||||
avatarType="outlined"
|
||||
key={u.id}
|
||||
name={u.from}
|
||||
width="18"
|
||||
/>
|
||||
))}
|
||||
</Avatar.Group>
|
||||
);
|
||||
}, [posts]);
|
||||
|
||||
const onFeedUpdate = (data: Operation[]) => {
|
||||
updateThreadHandler(
|
||||
threadId ?? feedDetail.id,
|
||||
feedDetail.id,
|
||||
Boolean(isThread),
|
||||
data
|
||||
);
|
||||
};
|
||||
|
||||
const onReactionSelect = (
|
||||
reactionType: ReactionType,
|
||||
reactionOperation: ReactionOperation
|
||||
) => {
|
||||
let updatedReactions = feedDetail.reactions || [];
|
||||
if (reactionOperation === ReactionOperation.ADD) {
|
||||
const reactionObject = {
|
||||
reactionType,
|
||||
user: {
|
||||
id: currentUser?.id as string,
|
||||
},
|
||||
};
|
||||
|
||||
updatedReactions = [...updatedReactions, reactionObject as Reaction];
|
||||
} else {
|
||||
updatedReactions = updatedReactions.filter(
|
||||
(reaction) =>
|
||||
!(
|
||||
reaction.reactionType === reactionType &&
|
||||
reaction.user.id === currentUser?.id
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const patch = compare(
|
||||
{ ...feedDetail, reactions: [...(feedDetail.reactions || [])] },
|
||||
{
|
||||
...feedDetail,
|
||||
reactions: updatedReactions,
|
||||
}
|
||||
);
|
||||
|
||||
if (!isEmpty(patch)) {
|
||||
onFeedUpdate(patch);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAnnouncementUpdate = (
|
||||
title: string,
|
||||
announcement: AnnouncementDetails
|
||||
) => {
|
||||
const existingAnnouncement = {
|
||||
...feedDetail,
|
||||
announcement: announcementDetails,
|
||||
};
|
||||
|
||||
const updatedAnnouncement = {
|
||||
...feedDetail,
|
||||
message: title,
|
||||
announcement,
|
||||
};
|
||||
|
||||
const patch = compare(existingAnnouncement, updatedAnnouncement);
|
||||
|
||||
if (!isEmpty(patch)) {
|
||||
onFeedUpdate(patch);
|
||||
}
|
||||
setIsEditAnnouncement(false);
|
||||
};
|
||||
|
||||
const handlePostUpdate = (message: string) => {
|
||||
const updatedPost = { ...feedDetail, message };
|
||||
|
||||
const patch = compare(feedDetail, updatedPost);
|
||||
|
||||
if (!isEmpty(patch)) {
|
||||
onFeedUpdate(patch);
|
||||
}
|
||||
setIsEditPost(false);
|
||||
};
|
||||
|
||||
const handleThreadEdit = () => {
|
||||
if (announcementDetails) {
|
||||
setIsEditAnnouncement(true);
|
||||
} else {
|
||||
setIsEditPost(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleVisibleChange = (newVisible: boolean) => setVisible(newVisible);
|
||||
|
||||
const onHide = () => setVisible(false);
|
||||
|
||||
useEffect(() => {
|
||||
setFeedDetail(feed);
|
||||
}, [feed]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'bg-grey-1-hover m--x-sm w-full p-x-sm m--t-xss py-2 m-b-xss rounded-4',
|
||||
{
|
||||
'bg-grey-1-hover': visible,
|
||||
}
|
||||
)}
|
||||
data-testid="main-message"
|
||||
ref={containerRef}>
|
||||
<Popover
|
||||
align={{ targetOffset: [0, -16] }}
|
||||
content={
|
||||
<PopoverContent
|
||||
editAnnouncementPermission={editPermission}
|
||||
isAnnouncement={!isUndefined(announcementDetails)}
|
||||
isAuthor={isAuthor}
|
||||
isThread={isThread}
|
||||
postId={feedDetail.id}
|
||||
reactions={feedDetail.reactions || []}
|
||||
threadId={threadId}
|
||||
onConfirmation={onConfirmation}
|
||||
onEdit={handleThreadEdit}
|
||||
onPopoverHide={onHide}
|
||||
onReactionSelect={onReactionSelect}
|
||||
onReply={onReply}
|
||||
/>
|
||||
}
|
||||
destroyTooltipOnHide={{ keepParent: false }}
|
||||
getPopupContainer={() => containerRef.current || document.body}
|
||||
key="reaction-options-popover"
|
||||
open={visible && !isEditPost}
|
||||
overlayClassName="ant-popover-feed"
|
||||
placement="topRight"
|
||||
trigger="hover"
|
||||
onOpenChange={handleVisibleChange}>
|
||||
<Row gutter={[10, 0]} wrap={false}>
|
||||
<Col className="avatar-column d-flex flex-column items-center justify-between">
|
||||
<UserPopOverCard userName={feedDetail.from} />
|
||||
|
||||
{showRepliesButton && repliesPostAvatarGroup}
|
||||
</Col>
|
||||
<Col flex="auto">
|
||||
<div>
|
||||
<FeedCardHeader
|
||||
isEntityFeed
|
||||
createdBy={feedDetail.from}
|
||||
entityFQN={entityFQN}
|
||||
entityField={entityField ?? ''}
|
||||
entityType={entityType}
|
||||
feedType={feedType ?? ThreadType.Conversation}
|
||||
task={task}
|
||||
timeStamp={feedDetail.postTs}
|
||||
/>
|
||||
<FeedCardBody
|
||||
announcementDetails={announcementDetails}
|
||||
isEditPost={isEditPost}
|
||||
isThread={isThread}
|
||||
message={feedDetail.message}
|
||||
reactions={feedDetail.reactions || []}
|
||||
onCancelPostUpdate={() => setIsEditPost(false)}
|
||||
onPostUpdate={handlePostUpdate}
|
||||
onReactionSelect={onReactionSelect}
|
||||
/>
|
||||
{!isEmpty(task.posts) && showRepliesButton ? (
|
||||
<Button
|
||||
className="p-0 h-auto line-height-16 text-announcement m-r-xs m-t-xs d-flex items-center"
|
||||
data-testid="show-reply-thread"
|
||||
type="text"
|
||||
onClick={showReplyThread}>
|
||||
{`${task.postsCount} ${t('label.reply-lowercase-plural')}`}
|
||||
|
||||
<Icon
|
||||
className={classNames('arrow-icon', {
|
||||
'rotate-180': isReplyThreadOpen,
|
||||
})}
|
||||
component={ArrowBottom}
|
||||
/>
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</Popover>
|
||||
|
||||
{isEditAnnouncement && announcementDetails && (
|
||||
<EditAnnouncementModal
|
||||
announcement={announcementDetails}
|
||||
announcementTitle={feedDetail.message}
|
||||
open={isEditAnnouncement}
|
||||
onCancel={() => setIsEditAnnouncement(false)}
|
||||
onConfirm={handleAnnouncementUpdate}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AnnouncementFeedCardBody;
|
||||
@ -0,0 +1,180 @@
|
||||
/*
|
||||
* Copyright 2024 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 { fireEvent, render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { ReactionOperation } from '../../enums/reactions.enum';
|
||||
import { Thread } from '../../generated/entity/feed/thread';
|
||||
import { ReactionType } from '../../generated/type/reaction';
|
||||
import { MOCK_ANNOUNCEMENT_DATA } from '../../mocks/Announcement.mock';
|
||||
import { mockUserData } from '../../mocks/MyDataPage.mock';
|
||||
import AnnouncementFeedCardBody from './AnnouncementFeedCardBody.component';
|
||||
|
||||
jest.mock('../../utils/FeedUtils', () => ({
|
||||
getEntityField: jest.fn(),
|
||||
getEntityFQN: jest.fn(),
|
||||
getEntityType: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../../hooks/useApplicationStore', () => ({
|
||||
useApplicationStore: jest.fn(() => ({
|
||||
currentUser: mockUserData,
|
||||
})),
|
||||
}));
|
||||
|
||||
jest.mock('../ActivityFeed/ActivityFeedCard/FeedCardBody/FeedCardBody', () =>
|
||||
jest.fn().mockImplementation(({ onPostUpdate, onReactionSelect }) => (
|
||||
<>
|
||||
<p>FeedCardBody</p>
|
||||
<button onClick={() => onPostUpdate('message')}>PostUpdateButton</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
onReactionSelect(ReactionType.Confused, ReactionOperation.ADD)
|
||||
}>
|
||||
ReactionSelectButton
|
||||
</button>
|
||||
</>
|
||||
))
|
||||
);
|
||||
|
||||
jest.mock(
|
||||
'../ActivityFeed/ActivityFeedCard/FeedCardHeader/FeedCardHeader',
|
||||
() => {
|
||||
return jest.fn().mockReturnValue(<p>FeedCardHeader</p>);
|
||||
}
|
||||
);
|
||||
|
||||
jest.mock('../common/PopOverCard/UserPopOverCard', () => {
|
||||
return jest.fn().mockImplementation(() => <p>UserPopOverCard</p>);
|
||||
});
|
||||
jest.mock('../common/ProfilePicture/ProfilePicture', () => {
|
||||
return jest.fn().mockImplementation(() => <p>ProfilePicture</p>);
|
||||
});
|
||||
|
||||
jest.mock('../Modals/AnnouncementModal/EditAnnouncementModal', () => {
|
||||
return jest.fn().mockImplementation(() => <p>EditAnnouncementModal</p>);
|
||||
});
|
||||
|
||||
jest.mock('../ActivityFeed/ActivityFeedCard/PopoverContent', () => {
|
||||
return jest.fn().mockImplementation(() => <p>PopoverContent</p>);
|
||||
});
|
||||
|
||||
const mockFeedCardProps = {
|
||||
feed: {
|
||||
from: 'admin',
|
||||
id: '36ea94c9-7f12-489c-94df-56cbefe14b2f',
|
||||
message: 'Cypress announcement',
|
||||
postTs: 1714026576902,
|
||||
reactions: [],
|
||||
},
|
||||
task: MOCK_ANNOUNCEMENT_DATA.data[0],
|
||||
entityLink:
|
||||
'<#E::database::cy-database-service-373851.cypress-database-1714026557974>',
|
||||
isThread: true,
|
||||
editPermission: true,
|
||||
isReplyThreadOpen: false,
|
||||
updateThreadHandler: jest.fn(),
|
||||
onReply: jest.fn(),
|
||||
onConfirmation: jest.fn(),
|
||||
showReplyThread: jest.fn(),
|
||||
};
|
||||
|
||||
describe('Test AnnouncementFeedCardBody Component', () => {
|
||||
it('Check if AnnouncementFeedCardBody component has all child components', async () => {
|
||||
render(<AnnouncementFeedCardBody {...mockFeedCardProps} />);
|
||||
const feedCardHeader = screen.getByText('FeedCardHeader');
|
||||
const feedCardBody = screen.getByText('FeedCardBody');
|
||||
const profilePictures = screen.getAllByText('ProfilePicture');
|
||||
const userPopOverCard = screen.getByText('UserPopOverCard');
|
||||
|
||||
expect(feedCardHeader).toBeInTheDocument();
|
||||
expect(feedCardBody).toBeInTheDocument();
|
||||
expect(userPopOverCard).toBeInTheDocument();
|
||||
expect(profilePictures).toHaveLength(4);
|
||||
});
|
||||
|
||||
it('should trigger onPostUpdate from FeedCardBody', async () => {
|
||||
render(<AnnouncementFeedCardBody {...mockFeedCardProps} />);
|
||||
|
||||
const postUpdateButton = screen.getByText('PostUpdateButton');
|
||||
|
||||
fireEvent.click(postUpdateButton);
|
||||
|
||||
expect(mockFeedCardProps.updateThreadHandler).toHaveBeenCalledWith(
|
||||
MOCK_ANNOUNCEMENT_DATA.data[0].id,
|
||||
MOCK_ANNOUNCEMENT_DATA.data[0].id,
|
||||
true,
|
||||
[{ op: 'replace', path: '/message', value: 'message' }]
|
||||
);
|
||||
});
|
||||
|
||||
it('should trigger ReactionSelectButton from FeedCardBody', async () => {
|
||||
render(<AnnouncementFeedCardBody {...mockFeedCardProps} />);
|
||||
|
||||
const reactionSelectButton = screen.getByText('ReactionSelectButton');
|
||||
|
||||
fireEvent.click(reactionSelectButton);
|
||||
|
||||
expect(mockFeedCardProps.updateThreadHandler).toHaveBeenCalledWith(
|
||||
MOCK_ANNOUNCEMENT_DATA.data[0].id,
|
||||
MOCK_ANNOUNCEMENT_DATA.data[0].id,
|
||||
true,
|
||||
[
|
||||
{
|
||||
op: 'add',
|
||||
path: '/reactions/0',
|
||||
value: {
|
||||
reactionType: 'confused',
|
||||
user: {
|
||||
id: '123',
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
it('should trigger postReplies button', async () => {
|
||||
render(<AnnouncementFeedCardBody {...mockFeedCardProps} />);
|
||||
|
||||
const showReplyThread = screen.getByTestId('show-reply-thread');
|
||||
|
||||
fireEvent.click(showReplyThread);
|
||||
|
||||
expect(mockFeedCardProps.showReplyThread).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not render PostReplies Profile Picture if showRepliesButton is false', async () => {
|
||||
render(
|
||||
<AnnouncementFeedCardBody
|
||||
{...mockFeedCardProps}
|
||||
showRepliesButton={false}
|
||||
/>
|
||||
);
|
||||
|
||||
const profilePictures = screen.queryByText('ProfilePicture');
|
||||
const showReplyThread = screen.queryByTestId('show-reply-thread');
|
||||
|
||||
expect(profilePictures).not.toBeInTheDocument();
|
||||
expect(showReplyThread).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not render PostReplies button if repliesPost is empty', async () => {
|
||||
render(
|
||||
<AnnouncementFeedCardBody {...mockFeedCardProps} task={{} as Thread} />
|
||||
);
|
||||
|
||||
const showReplyThread = screen.queryByTestId('show-reply-thread');
|
||||
|
||||
expect(showReplyThread).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,152 @@
|
||||
/*
|
||||
* Copyright 2024 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 { Typography } from 'antd';
|
||||
import { AxiosError } from 'axios';
|
||||
import { Operation } from 'fast-json-patch';
|
||||
import { isEmpty } from 'lodash';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { confirmStateInitialValue } from '../../constants/Feeds.constants';
|
||||
import { ERROR_PLACEHOLDER_TYPE } from '../../enums/common.enum';
|
||||
import { FeedFilter } from '../../enums/mydata.enum';
|
||||
import { Thread, ThreadType } from '../../generated/entity/feed/thread';
|
||||
import { getAllFeeds } from '../../rest/feedsAPI';
|
||||
import { showErrorToast } from '../../utils/ToastUtils';
|
||||
import { ConfirmState } from '../ActivityFeed/ActivityFeedCard/ActivityFeedCard.interface';
|
||||
import ErrorPlaceHolder from '../common/ErrorWithPlaceholder/ErrorPlaceHolder';
|
||||
import ConfirmationModal from '../Modals/ConfirmationModal/ConfirmationModal';
|
||||
import { AnnouncementThreadBodyProp } from './Announcement.interface';
|
||||
import AnnouncementThreads from './AnnouncementThreads';
|
||||
|
||||
const AnnouncementThreadBody = ({
|
||||
threadLink,
|
||||
refetchThread,
|
||||
editPermission,
|
||||
postFeedHandler,
|
||||
deletePostHandler,
|
||||
updateThreadHandler,
|
||||
}: AnnouncementThreadBodyProp) => {
|
||||
const { t } = useTranslation();
|
||||
const [threads, setThreads] = useState<Thread[]>([]);
|
||||
const [confirmationState, setConfirmationState] = useState<ConfirmState>(
|
||||
confirmStateInitialValue
|
||||
);
|
||||
const [isThreadLoading, setIsThreadLoading] = useState(true);
|
||||
|
||||
const getThreads = async (after?: string) => {
|
||||
setIsThreadLoading(true);
|
||||
|
||||
try {
|
||||
const res = await getAllFeeds(
|
||||
threadLink,
|
||||
after,
|
||||
ThreadType.Announcement,
|
||||
FeedFilter.ALL
|
||||
);
|
||||
|
||||
setThreads(res.data);
|
||||
} catch (error) {
|
||||
showErrorToast(
|
||||
error as AxiosError,
|
||||
t('server.entity-fetch-error', {
|
||||
entity: t('label.thread-plural-lowercase'),
|
||||
})
|
||||
);
|
||||
} finally {
|
||||
setIsThreadLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const loadNewThreads = () => {
|
||||
setTimeout(() => {
|
||||
getThreads();
|
||||
}, 500);
|
||||
};
|
||||
|
||||
const onDiscard = () => {
|
||||
setConfirmationState(confirmStateInitialValue);
|
||||
};
|
||||
|
||||
const onPostDelete = async (): Promise<void> => {
|
||||
if (confirmationState.postId && confirmationState.threadId) {
|
||||
await deletePostHandler?.(
|
||||
confirmationState.threadId,
|
||||
confirmationState.postId,
|
||||
confirmationState.isThread
|
||||
);
|
||||
}
|
||||
onDiscard();
|
||||
loadNewThreads();
|
||||
};
|
||||
|
||||
const onConfirmation = (data: ConfirmState) => {
|
||||
setConfirmationState(data);
|
||||
};
|
||||
|
||||
const postFeed = async (value: string, id: string): Promise<void> => {
|
||||
await postFeedHandler?.(value, id);
|
||||
loadNewThreads();
|
||||
};
|
||||
|
||||
const onUpdateThread = async (
|
||||
threadId: string,
|
||||
postId: string,
|
||||
isThread: boolean,
|
||||
data: Operation[]
|
||||
): Promise<void> => {
|
||||
await updateThreadHandler(threadId, postId, isThread, data);
|
||||
loadNewThreads();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getThreads();
|
||||
}, [threadLink, refetchThread]);
|
||||
|
||||
if (isEmpty(threads) && !isThreadLoading) {
|
||||
return (
|
||||
<ErrorPlaceHolder
|
||||
className="h-auto mt-24"
|
||||
type={ERROR_PLACEHOLDER_TYPE.CUSTOM}>
|
||||
<Typography.Paragraph data-testid="announcement-error">
|
||||
{t('message.no-announcement-message')}
|
||||
</Typography.Paragraph>
|
||||
</ErrorPlaceHolder>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="announcement-thread-body"
|
||||
data-testid="announcement-thread-body">
|
||||
<AnnouncementThreads
|
||||
editPermission={editPermission}
|
||||
postFeed={postFeed}
|
||||
threads={threads}
|
||||
updateThreadHandler={onUpdateThread}
|
||||
onConfirmation={onConfirmation}
|
||||
/>
|
||||
|
||||
<ConfirmationModal
|
||||
bodyText={t('message.confirm-delete-message')}
|
||||
cancelText={t('label.cancel')}
|
||||
confirmText={t('label.delete')}
|
||||
header={t('message.delete-message-question-mark')}
|
||||
visible={confirmationState.state}
|
||||
onCancel={onDiscard}
|
||||
onConfirm={onPostDelete}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AnnouncementThreadBody;
|
||||
@ -0,0 +1,276 @@
|
||||
/*
|
||||
* Copyright 2024 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 { fireEvent, render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { act } from 'react-test-renderer';
|
||||
import { MOCK_ANNOUNCEMENT_DATA } from '../../mocks/Announcement.mock';
|
||||
import { getAllFeeds } from '../../rest/feedsAPI';
|
||||
import AnnouncementThreadBody from './AnnouncementThreadBody.component';
|
||||
|
||||
jest.mock('../../rest/feedsAPI', () => ({
|
||||
getAllFeeds: jest.fn().mockImplementation(() => Promise.resolve()),
|
||||
}));
|
||||
|
||||
jest.mock('./AnnouncementThreads', () =>
|
||||
jest
|
||||
.fn()
|
||||
.mockImplementation(({ postFeed, updateThreadHandler, onConfirmation }) => (
|
||||
<>
|
||||
<p>AnnouncementThreads</p>
|
||||
<button onClick={() => postFeed('valueId', 'id')}>
|
||||
PostFeedButton
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
onConfirmation({
|
||||
state: true,
|
||||
threadId: 'threadId',
|
||||
postId: 'postId',
|
||||
isThread: false,
|
||||
})
|
||||
}>
|
||||
ConfirmationButton
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
updateThreadHandler('threadId', 'postId', true, {
|
||||
op: 'replace',
|
||||
path: '/announcement/description',
|
||||
value: 'Cypress announcement description.',
|
||||
})
|
||||
}>
|
||||
UpdateThreadButton
|
||||
</button>
|
||||
</>
|
||||
))
|
||||
);
|
||||
|
||||
jest.mock('../Modals/ConfirmationModal/ConfirmationModal', () =>
|
||||
jest.fn().mockImplementation(({ visible, onConfirm, onCancel }) => (
|
||||
<>
|
||||
{visible ? 'Confirmation Modal is open' : 'Confirmation Modal is close'}
|
||||
<button onClick={onConfirm}>Confirm Confirmation Modal</button>
|
||||
<button onClick={onCancel}>Cancel Confirmation Modal</button>
|
||||
</>
|
||||
))
|
||||
);
|
||||
|
||||
jest.mock('../common/ErrorWithPlaceholder/ErrorPlaceHolder', () => {
|
||||
return jest.fn().mockReturnValue(<p>ErrorPlaceHolder</p>);
|
||||
});
|
||||
|
||||
jest.mock('../../utils/ToastUtils', () => ({
|
||||
showErrorToast: jest.fn(),
|
||||
}));
|
||||
|
||||
const mockProps = {
|
||||
threadLink: 'threadLink',
|
||||
refetchThread: false,
|
||||
editPermission: true,
|
||||
postFeedHandler: jest.fn(),
|
||||
deletePostHandler: jest.fn(),
|
||||
updateThreadHandler: jest.fn(),
|
||||
};
|
||||
|
||||
describe('Test AnnouncementThreadBody Component', () => {
|
||||
it('should call getAllFeeds when component is mount', async () => {
|
||||
render(<AnnouncementThreadBody {...mockProps} />);
|
||||
|
||||
expect(getAllFeeds).toHaveBeenCalledWith(
|
||||
'threadLink',
|
||||
undefined,
|
||||
'Announcement',
|
||||
'ALL'
|
||||
);
|
||||
});
|
||||
|
||||
it('should render empty placeholder when data is not there', async () => {
|
||||
await act(async () => {
|
||||
render(<AnnouncementThreadBody {...mockProps} />);
|
||||
});
|
||||
|
||||
const emptyPlaceholder = screen.getByText('ErrorPlaceHolder');
|
||||
|
||||
expect(emptyPlaceholder).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Check if all child elements rendered', async () => {
|
||||
(getAllFeeds as jest.Mock).mockImplementationOnce(() =>
|
||||
Promise.resolve(MOCK_ANNOUNCEMENT_DATA)
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
render(<AnnouncementThreadBody {...mockProps} />);
|
||||
});
|
||||
|
||||
const component = screen.getByTestId('announcement-thread-body');
|
||||
const announcementThreads = screen.getByText('AnnouncementThreads');
|
||||
const confirmationModal = screen.getByText('Confirmation Modal is close');
|
||||
|
||||
expect(component).toBeInTheDocument();
|
||||
expect(confirmationModal).toBeInTheDocument();
|
||||
expect(announcementThreads).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Confirmation Modal
|
||||
|
||||
it('should open delete confirmation modal', async () => {
|
||||
(getAllFeeds as jest.Mock).mockImplementationOnce(() =>
|
||||
Promise.resolve(MOCK_ANNOUNCEMENT_DATA)
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
render(<AnnouncementThreadBody {...mockProps} />);
|
||||
});
|
||||
|
||||
const confirmationCloseModal = screen.getByText(
|
||||
'Confirmation Modal is close'
|
||||
);
|
||||
|
||||
expect(confirmationCloseModal).toBeInTheDocument();
|
||||
|
||||
const confirmationButton = screen.getByText('ConfirmationButton');
|
||||
act(() => {
|
||||
fireEvent.click(confirmationButton);
|
||||
});
|
||||
const confirmationOpenModal = screen.getByText(
|
||||
'Confirmation Modal is open'
|
||||
);
|
||||
|
||||
expect(confirmationOpenModal).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should trigger onConfirm in confirmation modal', async () => {
|
||||
(getAllFeeds as jest.Mock).mockImplementationOnce(() =>
|
||||
Promise.resolve(MOCK_ANNOUNCEMENT_DATA)
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
render(<AnnouncementThreadBody {...mockProps} />);
|
||||
});
|
||||
|
||||
const confirmationButton = screen.getByText('ConfirmationButton');
|
||||
act(() => {
|
||||
fireEvent.click(confirmationButton);
|
||||
});
|
||||
|
||||
expect(screen.getByText('Confirmation Modal is open')).toBeInTheDocument();
|
||||
|
||||
const confirmConfirmationButton = screen.getByText(
|
||||
'Confirm Confirmation Modal'
|
||||
);
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(confirmConfirmationButton);
|
||||
});
|
||||
|
||||
expect(mockProps.deletePostHandler).toHaveBeenCalledWith(
|
||||
'threadId',
|
||||
'postId',
|
||||
false
|
||||
);
|
||||
|
||||
expect(getAllFeeds).toHaveBeenCalledWith(
|
||||
'threadLink',
|
||||
undefined,
|
||||
'Announcement',
|
||||
'ALL'
|
||||
);
|
||||
});
|
||||
|
||||
it('should trigger onCancel in confirmation modal', async () => {
|
||||
(getAllFeeds as jest.Mock).mockImplementationOnce(() =>
|
||||
Promise.resolve(MOCK_ANNOUNCEMENT_DATA)
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
render(<AnnouncementThreadBody {...mockProps} />);
|
||||
});
|
||||
|
||||
const confirmationButton = screen.getByText('ConfirmationButton');
|
||||
act(() => {
|
||||
fireEvent.click(confirmationButton);
|
||||
});
|
||||
|
||||
expect(screen.getByText('Confirmation Modal is open')).toBeInTheDocument();
|
||||
|
||||
const cancelConfirmationButton = screen.getByText(
|
||||
'Cancel Confirmation Modal'
|
||||
);
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(cancelConfirmationButton);
|
||||
});
|
||||
|
||||
expect(screen.getByText('Confirmation Modal is close')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// AnnouncementThreads Component
|
||||
|
||||
it('should trigger postFeedHandler', async () => {
|
||||
(getAllFeeds as jest.Mock).mockImplementationOnce(() =>
|
||||
Promise.resolve(MOCK_ANNOUNCEMENT_DATA)
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
render(<AnnouncementThreadBody {...mockProps} />);
|
||||
});
|
||||
|
||||
const postFeedButton = screen.getByText('PostFeedButton');
|
||||
act(() => {
|
||||
fireEvent.click(postFeedButton);
|
||||
});
|
||||
|
||||
expect(mockProps.postFeedHandler).toHaveBeenCalledWith('valueId', 'id');
|
||||
|
||||
expect(getAllFeeds).toHaveBeenCalledWith(
|
||||
'threadLink',
|
||||
undefined,
|
||||
'Announcement',
|
||||
'ALL'
|
||||
);
|
||||
});
|
||||
|
||||
it('should trigger updateThreadHandler', async () => {
|
||||
(getAllFeeds as jest.Mock).mockImplementationOnce(() =>
|
||||
Promise.resolve(MOCK_ANNOUNCEMENT_DATA)
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
render(<AnnouncementThreadBody {...mockProps} />);
|
||||
});
|
||||
|
||||
const postFeedButton = screen.getByText('UpdateThreadButton');
|
||||
act(() => {
|
||||
fireEvent.click(postFeedButton);
|
||||
});
|
||||
|
||||
expect(mockProps.updateThreadHandler).toHaveBeenCalledWith(
|
||||
'threadId',
|
||||
'postId',
|
||||
true,
|
||||
{
|
||||
op: 'replace',
|
||||
path: '/announcement/description',
|
||||
value: 'Cypress announcement description.',
|
||||
}
|
||||
);
|
||||
|
||||
expect(getAllFeeds).toHaveBeenCalledWith(
|
||||
'threadLink',
|
||||
undefined,
|
||||
'Announcement',
|
||||
'ALL'
|
||||
);
|
||||
});
|
||||
});
|
||||
@ -14,10 +14,10 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { mockThreadData } from './ActivityThread.mock';
|
||||
import { mockThreadData } from '../ActivityFeed/ActivityThreadPanel/ActivityThread.mock';
|
||||
import AnnouncementThreads from './AnnouncementThreads';
|
||||
|
||||
jest.mock('../../../utils/FeedUtils', () => ({
|
||||
jest.mock('../../utils/FeedUtils', () => ({
|
||||
getFeedListWithRelativeDays: jest.fn().mockReturnValue({
|
||||
updatedFeedList: mockThreadData,
|
||||
relativeDays: ['Today', 'Yesterday'],
|
||||
@ -27,6 +27,7 @@ jest.mock('../../../utils/FeedUtils', () => ({
|
||||
const mockAnnouncementThreadsProp = {
|
||||
threads: mockThreadData,
|
||||
selectedThreadId: '',
|
||||
editPermission: true,
|
||||
postFeed: jest.fn(),
|
||||
onThreadIdSelect: jest.fn(),
|
||||
onThreadSelect: jest.fn(),
|
||||
@ -34,16 +35,8 @@ const mockAnnouncementThreadsProp = {
|
||||
updateThreadHandler: jest.fn(),
|
||||
};
|
||||
|
||||
jest.mock('../ActivityFeedCard/ActivityFeedCard', () => {
|
||||
return jest.fn().mockReturnValue(<p>ActivityFeedCard</p>);
|
||||
});
|
||||
|
||||
jest.mock('../ActivityFeedEditor/ActivityFeedEditor', () => {
|
||||
return jest.fn().mockReturnValue(<p>ActivityFeedEditor</p>);
|
||||
});
|
||||
|
||||
jest.mock('../ActivityFeedCard/FeedCardFooter/FeedCardFooter', () => {
|
||||
return jest.fn().mockReturnValue(<p>FeedCardFooter</p>);
|
||||
jest.mock('./AnnouncementFeedCard.component', () => {
|
||||
return jest.fn().mockReturnValue(<p>AnnouncementFeedCard</p>);
|
||||
});
|
||||
|
||||
describe('Test AnnouncementThreads Component', () => {
|
||||
@ -52,7 +45,7 @@ describe('Test AnnouncementThreads Component', () => {
|
||||
wrapper: MemoryRouter,
|
||||
});
|
||||
|
||||
const threads = await screen.findAllByTestId('announcement-card');
|
||||
const threads = await screen.findAllByText('AnnouncementFeedCard');
|
||||
|
||||
expect(threads).toHaveLength(2);
|
||||
});
|
||||
@ -0,0 +1,112 @@
|
||||
/*
|
||||
* Copyright 2022 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 { Divider, Typography } from 'antd';
|
||||
import React, { FC, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Post, Thread } from '../../generated/entity/feed/thread';
|
||||
import { isActiveAnnouncement } from '../../utils/AnnouncementsUtils';
|
||||
import { getFeedListWithRelativeDays } from '../../utils/FeedUtils';
|
||||
import { AnnouncementThreadListProp } from './Announcement.interface';
|
||||
import './announcement.less';
|
||||
import AnnouncementFeedCard from './AnnouncementFeedCard.component';
|
||||
|
||||
const AnnouncementThreads: FC<AnnouncementThreadListProp> = ({
|
||||
threads,
|
||||
editPermission,
|
||||
postFeed,
|
||||
onConfirmation,
|
||||
updateThreadHandler,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { updatedFeedList: updatedThreads } =
|
||||
getFeedListWithRelativeDays(threads);
|
||||
|
||||
const { activeAnnouncements, inActiveAnnouncements } = useMemo(() => {
|
||||
return updatedThreads.reduce(
|
||||
(
|
||||
acc: {
|
||||
activeAnnouncements: Thread[];
|
||||
inActiveAnnouncements: Thread[];
|
||||
},
|
||||
cv: Thread
|
||||
) => {
|
||||
if (
|
||||
cv.announcement &&
|
||||
isActiveAnnouncement(
|
||||
cv.announcement?.startTime,
|
||||
cv.announcement?.endTime
|
||||
)
|
||||
) {
|
||||
acc.activeAnnouncements.push(cv);
|
||||
} else {
|
||||
acc.inActiveAnnouncements.push(cv);
|
||||
}
|
||||
|
||||
return acc;
|
||||
},
|
||||
{
|
||||
activeAnnouncements: [],
|
||||
inActiveAnnouncements: [],
|
||||
}
|
||||
);
|
||||
}, [updatedThreads]);
|
||||
|
||||
const getAnnouncements = useCallback(
|
||||
(announcements: Thread[]) => {
|
||||
return announcements.map((thread) => {
|
||||
const mainFeed = {
|
||||
message: thread.message,
|
||||
postTs: thread.threadTs,
|
||||
from: thread.createdBy,
|
||||
id: thread.id,
|
||||
reactions: thread.reactions,
|
||||
} as Post;
|
||||
|
||||
return (
|
||||
<AnnouncementFeedCard
|
||||
editPermission={editPermission}
|
||||
feed={mainFeed}
|
||||
key={thread.id}
|
||||
postFeed={postFeed}
|
||||
task={thread}
|
||||
updateThreadHandler={updateThreadHandler}
|
||||
onConfirmation={onConfirmation}
|
||||
/>
|
||||
);
|
||||
});
|
||||
},
|
||||
[editPermission, postFeed, updateThreadHandler, onConfirmation]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{getAnnouncements(activeAnnouncements)}
|
||||
{Boolean(inActiveAnnouncements.length) && (
|
||||
<div className="d-flex flex-column items-end m-y-xlg">
|
||||
<Typography.Text
|
||||
className="text-announcement"
|
||||
data-testid="inActive-announcements">
|
||||
<strong>{inActiveAnnouncements.length}</strong>{' '}
|
||||
{t('label.inactive-announcement-plural')}
|
||||
</Typography.Text>
|
||||
<Divider className="m-t-xs m-b-0" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{getAnnouncements(inActiveAnnouncements)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AnnouncementThreads;
|
||||
@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Copyright 2024 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 (reference) url('../../styles/variables.less');
|
||||
|
||||
.announcement-thread-body {
|
||||
margin-top: 16px;
|
||||
|
||||
.text-announcement {
|
||||
color: @announcement-border;
|
||||
}
|
||||
|
||||
.ant-card.announcement-thread-card {
|
||||
margin-top: 20px;
|
||||
padding-top: 8px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid @announcement-border;
|
||||
background: @announcement-background;
|
||||
|
||||
.avatar-column {
|
||||
position: relative;
|
||||
|
||||
&::after {
|
||||
position: absolute;
|
||||
content: '';
|
||||
top: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 1px;
|
||||
background: @announcement-border-light;
|
||||
height: calc(100% - 10px);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.assignee-item {
|
||||
margin: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.ant-avatar-group {
|
||||
z-index: 10;
|
||||
}
|
||||
}
|
||||
|
||||
.arrow-icon {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.rotate-180 {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.feed-line {
|
||||
margin-left: 8px;
|
||||
background: @announcement-border-light;
|
||||
width: 1px;
|
||||
height: calc(100% - 10px);
|
||||
}
|
||||
@ -147,7 +147,6 @@ const TableQueryRightPanel = ({
|
||||
<ProfilePicture
|
||||
displayName={getEntityName(user)}
|
||||
name={user.name || ''}
|
||||
textClass="text-xs"
|
||||
width="20"
|
||||
/>
|
||||
<Link to={getUserPath(user.name ?? '')}>
|
||||
|
||||
@ -63,7 +63,6 @@ function DomainExperts({
|
||||
<ProfilePicture
|
||||
displayName={getEntityName(expert)}
|
||||
name={expert.name ?? ''}
|
||||
textClass="text-xs"
|
||||
width="20"
|
||||
/>
|
||||
<Link to={getUserPath(expert.name ?? '')}>
|
||||
|
||||
@ -73,7 +73,6 @@ function GlossaryReviewers({
|
||||
displayName={getEntityName(reviewer)}
|
||||
isTeam={reviewer.type === UserTeam.Team}
|
||||
name={reviewer.name ?? ''}
|
||||
textClass="text-xs"
|
||||
width="20"
|
||||
/>
|
||||
<Link to={getUserPath(reviewer.name ?? '')}>
|
||||
|
||||
@ -42,12 +42,14 @@ jest.mock('react-router-dom', () => ({
|
||||
useLocation: jest.fn().mockReturnValue({ pathname: 'pathname' }),
|
||||
}));
|
||||
const onCancel = jest.fn();
|
||||
const onSave = jest.fn();
|
||||
|
||||
const mockProps = {
|
||||
open: true,
|
||||
entityType: '',
|
||||
entityFQN: '',
|
||||
onCancel,
|
||||
onSave,
|
||||
};
|
||||
|
||||
describe('Test Add Announcement modal', () => {
|
||||
|
||||
@ -35,6 +35,7 @@ interface Props {
|
||||
entityType: string;
|
||||
entityFQN: string;
|
||||
onCancel: () => void;
|
||||
onSave: () => void;
|
||||
}
|
||||
|
||||
export interface CreateAnnouncement {
|
||||
@ -47,6 +48,7 @@ export interface CreateAnnouncement {
|
||||
const AddAnnouncementModal: FC<Props> = ({
|
||||
open,
|
||||
onCancel,
|
||||
onSave,
|
||||
entityType,
|
||||
entityFQN,
|
||||
}) => {
|
||||
@ -85,7 +87,7 @@ const AddAnnouncementModal: FC<Props> = ({
|
||||
if (data) {
|
||||
showSuccessToast(t('message.announcement-created-successfully'));
|
||||
}
|
||||
onCancel();
|
||||
onSave();
|
||||
} catch (error) {
|
||||
showErrorToast(error as AxiosError);
|
||||
} finally {
|
||||
|
||||
@ -40,7 +40,7 @@ import {
|
||||
getImageWithResolutionAndFallback,
|
||||
ImageQuality,
|
||||
} from '../../../../utils/ProfilerUtils';
|
||||
import Avatar from '../../../common/AvatarComponent/Avatar';
|
||||
import ProfilePicture from '../../../common/ProfilePicture/ProfilePicture';
|
||||
import './user-profile-icon.less';
|
||||
|
||||
type ListMenuItemProps = {
|
||||
@ -337,7 +337,7 @@ export const UserProfileIcon = () => {
|
||||
onError={handleOnImageError}
|
||||
/>
|
||||
) : (
|
||||
<Avatar name={userName} type="circle" width="36" />
|
||||
<ProfilePicture name={userName} width="36" />
|
||||
)}
|
||||
<div className="d-flex flex-col">
|
||||
<Tooltip title={getEntityName(currentUser)}>
|
||||
|
||||
@ -40,8 +40,8 @@ jest.mock('../../../../utils/ProfilerUtils', () => ({
|
||||
ImageQuality: jest.fn().mockReturnValue('6x'),
|
||||
}));
|
||||
|
||||
jest.mock('../../../common/AvatarComponent/Avatar', () =>
|
||||
jest.fn().mockReturnValue(<div>Avatar</div>)
|
||||
jest.mock('../../../common/ProfilePicture/ProfilePicture', () =>
|
||||
jest.fn().mockReturnValue(<div>ProfilePicture</div>)
|
||||
);
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
@ -84,7 +84,7 @@ describe('UserProfileIcon', () => {
|
||||
const { queryByTestId, getByText } = render(<UserProfileIcon />);
|
||||
|
||||
expect(queryByTestId('app-bar-user-profile-pic')).not.toBeInTheDocument();
|
||||
expect(getByText('Avatar')).toBeInTheDocument();
|
||||
expect(getByText('ProfilePicture')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should display the user team', () => {
|
||||
|
||||
@ -53,7 +53,6 @@ const UserProfileImage = ({ userData }: UserProfileImageProps) => {
|
||||
displayName={userData?.displayName ?? userData.name}
|
||||
height="54"
|
||||
name={userData?.name ?? ''}
|
||||
textClass="text-xl"
|
||||
width="54"
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -13,7 +13,7 @@
|
||||
|
||||
@import url('../../../../styles/variables.less');
|
||||
|
||||
.announcement-card {
|
||||
.ant-card.announcement-card {
|
||||
width: 340px;
|
||||
background: @announcement-background;
|
||||
border: 1px solid @announcement-border;
|
||||
|
||||
@ -28,12 +28,9 @@ jest.mock('../../../../utils/ToastUtils', () => ({
|
||||
showErrorToast: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock(
|
||||
'../../../ActivityFeed/ActivityThreadPanel/ActivityThreadPanelBody',
|
||||
() => {
|
||||
return jest.fn().mockReturnValue(<div>ActivityThreadPanelBody</div>);
|
||||
}
|
||||
);
|
||||
jest.mock('../../../Announcement/AnnouncementThreadBody.component', () => {
|
||||
return jest.fn().mockReturnValue(<div>AnnouncementThreadBody</div>);
|
||||
});
|
||||
|
||||
jest.mock('../../../Modals/AnnouncementModal/AddAnnouncementModal', () => {
|
||||
return jest.fn().mockReturnValue(<div>AddAnnouncementModal</div>);
|
||||
@ -51,21 +48,31 @@ describe('Test Announcement drawer component', () => {
|
||||
it('Should render the component', async () => {
|
||||
render(<AnnouncementDrawer {...mockProps} />);
|
||||
|
||||
const drawer = await screen.findByTestId('announcement-drawer');
|
||||
const announcementHeader = screen.getByText('label.announcement-plural');
|
||||
const addAnnouncementButton = screen.getByTestId('add-announcement');
|
||||
|
||||
const addButton = await screen.findByTestId('add-announcement');
|
||||
const addButton = screen.getByTestId('add-announcement');
|
||||
|
||||
const announcements = await screen.findByText('ActivityThreadPanelBody');
|
||||
const announcements = screen.getByText('AnnouncementThreadBody');
|
||||
|
||||
expect(drawer).toBeInTheDocument();
|
||||
expect(announcementHeader).toBeInTheDocument();
|
||||
expect(addAnnouncementButton).toBeInTheDocument();
|
||||
expect(addButton).toBeInTheDocument();
|
||||
expect(announcements).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should be disabled if not having permission to create', async () => {
|
||||
render(<AnnouncementDrawer {...mockProps} createPermission={false} />);
|
||||
|
||||
const addButton = screen.getByTestId('add-announcement');
|
||||
|
||||
expect(addButton).toBeDisabled();
|
||||
});
|
||||
|
||||
it('Should open modal on click of add button', async () => {
|
||||
render(<AnnouncementDrawer {...mockProps} />);
|
||||
|
||||
const addButton = await screen.findByTestId('add-announcement');
|
||||
const addButton = screen.getByTestId('add-announcement');
|
||||
|
||||
fireEvent.click(addButton);
|
||||
|
||||
|
||||
@ -15,29 +15,24 @@ import { CloseOutlined } from '@ant-design/icons';
|
||||
import { Button, Drawer, Space, Tooltip, Typography } from 'antd';
|
||||
import { AxiosError } from 'axios';
|
||||
import { Operation } from 'fast-json-patch';
|
||||
import { uniqueId } from 'lodash';
|
||||
import React, { FC, useState } from 'react';
|
||||
import React, { FC, useCallback, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
CreateThread,
|
||||
ThreadType,
|
||||
} from '../../../../generated/api/feed/createThread';
|
||||
import { Post } from '../../../../generated/entity/feed/thread';
|
||||
import { postFeedById, postThread } from '../../../../rest/feedsAPI';
|
||||
import { postFeedById } from '../../../../rest/feedsAPI';
|
||||
import { getEntityFeedLink } from '../../../../utils/EntityUtils';
|
||||
import { deletePost, updateThreadData } from '../../../../utils/FeedUtils';
|
||||
import { showErrorToast } from '../../../../utils/ToastUtils';
|
||||
import ActivityThreadPanelBody from '../../../ActivityFeed/ActivityThreadPanel/ActivityThreadPanelBody';
|
||||
|
||||
import { useApplicationStore } from '../../../../hooks/useApplicationStore';
|
||||
import AnnouncementThreadBody from '../../../Announcement/AnnouncementThreadBody.component';
|
||||
import AddAnnouncementModal from '../../../Modals/AnnouncementModal/AddAnnouncementModal';
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
entityType: string;
|
||||
entityFQN: string;
|
||||
createPermission: boolean;
|
||||
onClose: () => void;
|
||||
createPermission?: boolean;
|
||||
}
|
||||
|
||||
const AnnouncementDrawer: FC<Props> = ({
|
||||
@ -45,11 +40,13 @@ const AnnouncementDrawer: FC<Props> = ({
|
||||
onClose,
|
||||
entityFQN,
|
||||
entityType,
|
||||
createPermission,
|
||||
createPermission = false,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { currentUser } = useApplicationStore();
|
||||
const [isAnnouncement, setIsAnnouncement] = useState<boolean>(false);
|
||||
const [isAddAnnouncementOpen, setIsAddAnnouncementOpen] =
|
||||
useState<boolean>(false);
|
||||
const [refetchThread, setRefetchThread] = useState<boolean>(false);
|
||||
|
||||
const title = (
|
||||
<Space
|
||||
@ -64,92 +61,94 @@ const AnnouncementDrawer: FC<Props> = ({
|
||||
</Space>
|
||||
);
|
||||
|
||||
const createThread = async (data: CreateThread) => {
|
||||
const deletePostHandler = async (
|
||||
threadId: string,
|
||||
postId: string,
|
||||
isThread: boolean
|
||||
): Promise<void> => {
|
||||
await deletePost(threadId, postId, isThread);
|
||||
};
|
||||
|
||||
const postFeedHandler = async (value: string, id: string): Promise<void> => {
|
||||
const data = {
|
||||
message: value,
|
||||
from: currentUser?.name,
|
||||
} as Post;
|
||||
|
||||
try {
|
||||
await postThread(data);
|
||||
await postFeedById(id, data);
|
||||
} catch (err) {
|
||||
showErrorToast(err as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
const deletePostHandler = (
|
||||
threadId: string,
|
||||
postId: string,
|
||||
isThread: boolean
|
||||
) => {
|
||||
deletePost(threadId, postId, isThread);
|
||||
};
|
||||
|
||||
const postFeedHandler = (value: string, id: string) => {
|
||||
const data = {
|
||||
message: value,
|
||||
from: currentUser?.name,
|
||||
} as Post;
|
||||
postFeedById(id, data).catch((err: AxiosError) => {
|
||||
showErrorToast(err);
|
||||
});
|
||||
};
|
||||
|
||||
const updateThreadHandler = (
|
||||
const updateThreadHandler = async (
|
||||
threadId: string,
|
||||
postId: string,
|
||||
isThread: boolean,
|
||||
data: Operation[]
|
||||
) => {
|
||||
): Promise<void> => {
|
||||
const callback = () => {
|
||||
return;
|
||||
};
|
||||
|
||||
updateThreadData(threadId, postId, isThread, data, callback);
|
||||
await updateThreadData(threadId, postId, isThread, data, callback);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div data-testid="announcement-drawer">
|
||||
<Drawer
|
||||
closable={false}
|
||||
open={open}
|
||||
placement="right"
|
||||
title={title}
|
||||
width={576}
|
||||
onClose={onClose}>
|
||||
<div className="d-flex justify-end">
|
||||
<Tooltip
|
||||
title={!createPermission && t('message.no-permission-to-view')}>
|
||||
<Button
|
||||
data-testid="add-announcement"
|
||||
disabled={!createPermission}
|
||||
type="primary"
|
||||
onClick={() => setIsAnnouncement(true)}>
|
||||
{t('label.add-entity', { entity: t('label.announcement') })}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
const handleCloseAnnouncementModal = useCallback(
|
||||
() => setIsAddAnnouncementOpen(false),
|
||||
[]
|
||||
);
|
||||
const handleOpenAnnouncementModal = useCallback(
|
||||
() => setIsAddAnnouncementOpen(true),
|
||||
[]
|
||||
);
|
||||
|
||||
<ActivityThreadPanelBody
|
||||
className="p-0"
|
||||
createThread={createThread}
|
||||
deletePostHandler={deletePostHandler}
|
||||
editAnnouncementPermission={createPermission}
|
||||
key={uniqueId()}
|
||||
postFeedHandler={postFeedHandler}
|
||||
showHeader={false}
|
||||
threadLink={getEntityFeedLink(entityType, entityFQN)}
|
||||
threadType={ThreadType.Announcement}
|
||||
updateThreadHandler={updateThreadHandler}
|
||||
/>
|
||||
</Drawer>
|
||||
const handleSaveAnnouncement = useCallback(() => {
|
||||
handleCloseAnnouncementModal();
|
||||
setRefetchThread((prev) => !prev);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
closable={false}
|
||||
open={open}
|
||||
placement="right"
|
||||
title={title}
|
||||
width={576}
|
||||
onClose={onClose}>
|
||||
<div className="d-flex justify-end">
|
||||
<Tooltip
|
||||
title={!createPermission && t('message.no-permission-to-view')}>
|
||||
<Button
|
||||
data-testid="add-announcement"
|
||||
disabled={!createPermission}
|
||||
type="primary"
|
||||
onClick={handleOpenAnnouncementModal}>
|
||||
{t('label.add-entity', { entity: t('label.announcement') })}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
{isAnnouncement && (
|
||||
<AnnouncementThreadBody
|
||||
deletePostHandler={deletePostHandler}
|
||||
editPermission={createPermission}
|
||||
postFeedHandler={postFeedHandler}
|
||||
refetchThread={refetchThread}
|
||||
threadLink={getEntityFeedLink(entityType, entityFQN)}
|
||||
updateThreadHandler={updateThreadHandler}
|
||||
/>
|
||||
|
||||
{isAddAnnouncementOpen && (
|
||||
<AddAnnouncementModal
|
||||
entityFQN={entityFQN || ''}
|
||||
entityType={entityType || ''}
|
||||
open={isAnnouncement}
|
||||
onCancel={() => setIsAnnouncement(false)}
|
||||
open={isAddAnnouncementOpen}
|
||||
onCancel={handleCloseAnnouncementModal}
|
||||
onSave={handleSaveAnnouncement}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
</Drawer>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -237,6 +237,7 @@ const UserPopOverCard: FC<Props> = ({
|
||||
}) => {
|
||||
const profilePicture = (
|
||||
<ProfilePicture
|
||||
avatarType="outlined"
|
||||
isTeam={type === UserTeam.Team}
|
||||
name={userName}
|
||||
width={`${profileWidth}`}
|
||||
|
||||
@ -11,7 +11,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { findByText, render } from '@testing-library/react';
|
||||
import { findByTestId, render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import ProfilePicture from './ProfilePicture';
|
||||
|
||||
@ -40,7 +40,7 @@ describe('Test ProfilePicture component', () => {
|
||||
it('ProfilePicture component should render with Avatar', async () => {
|
||||
const { container } = render(<ProfilePicture {...mockData} width="36" />);
|
||||
|
||||
const avatar = await findByText(container, 'Avatar');
|
||||
const avatar = await findByTestId(container, 'profile-avatar');
|
||||
|
||||
expect(avatar).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@ -11,16 +11,16 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Avatar } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
import { ImageShape } from 'Models';
|
||||
import React, { useMemo } from 'react';
|
||||
import { usePermissionProvider } from '../../../context/PermissionProvider/PermissionProvider';
|
||||
import { ResourceEntity } from '../../../context/PermissionProvider/PermissionProvider.interface';
|
||||
import { EntityReference, User } from '../../../generated/entity/teams/user';
|
||||
import { User } from '../../../generated/entity/teams/user';
|
||||
import { useUserProfile } from '../../../hooks/user-profile/useUserProfile';
|
||||
import { getEntityName } from '../../../utils/EntityUtils';
|
||||
import { getRandomColor } from '../../../utils/CommonUtils';
|
||||
import { userPermissions } from '../../../utils/PermissionsUtils';
|
||||
import Avatar from '../AvatarComponent/Avatar';
|
||||
import Loader from '../Loader/Loader';
|
||||
|
||||
type UserData = Pick<User, 'name' | 'displayName'>;
|
||||
@ -28,25 +28,28 @@ type UserData = Pick<User, 'name' | 'displayName'>;
|
||||
interface Props extends UserData {
|
||||
width?: string;
|
||||
type?: ImageShape;
|
||||
textClass?: string;
|
||||
className?: string;
|
||||
height?: string;
|
||||
profileImgClasses?: string;
|
||||
isTeam?: boolean;
|
||||
size?: number | 'small' | 'default' | 'large';
|
||||
avatarType?: 'solid' | 'outlined';
|
||||
}
|
||||
|
||||
const ProfilePicture = ({
|
||||
name,
|
||||
displayName,
|
||||
className = '',
|
||||
textClass = '',
|
||||
type = 'circle',
|
||||
width = '36',
|
||||
height,
|
||||
profileImgClasses,
|
||||
isTeam = false,
|
||||
size,
|
||||
avatarType = 'solid',
|
||||
}: Props) => {
|
||||
const { permissions } = usePermissionProvider();
|
||||
const { color, character, backgroundColor } = getRandomColor(
|
||||
displayName ?? name
|
||||
);
|
||||
|
||||
const viewUserPermission = useMemo(() => {
|
||||
return userPermissions.hasViewPermissions(ResourceEntity.USER, permissions);
|
||||
@ -61,12 +64,17 @@ const ProfilePicture = ({
|
||||
const getAvatarByName = () => {
|
||||
return (
|
||||
<Avatar
|
||||
className={className}
|
||||
height={height}
|
||||
name={getEntityName({ name, displayName } as EntityReference)}
|
||||
textClass={textClass}
|
||||
type={type}
|
||||
width={width}
|
||||
className={classNames('flex-center', className)}
|
||||
data-testid="profile-avatar"
|
||||
icon={character}
|
||||
shape={type}
|
||||
size={size ?? parseInt(width)}
|
||||
style={{
|
||||
color: avatarType === 'solid' ? 'default' : color,
|
||||
backgroundColor: avatarType === 'solid' ? color : backgroundColor,
|
||||
fontWeight: avatarType === 'solid' ? 400 : 500,
|
||||
border: `0.5px solid ${avatarType === 'solid' ? 'default' : color}`,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -97,17 +105,13 @@ const ProfilePicture = ({
|
||||
};
|
||||
|
||||
return profileURL ? (
|
||||
<div
|
||||
className={classNames('profile-image', type, className)}
|
||||
style={{ height: `${height || width}px`, width: `${width}px` }}>
|
||||
<img
|
||||
alt="user"
|
||||
className={profileImgClasses}
|
||||
data-testid="profile-image"
|
||||
referrerPolicy="no-referrer"
|
||||
src={profileURL}
|
||||
/>
|
||||
</div>
|
||||
<Avatar
|
||||
className={className}
|
||||
data-testid="profile-image"
|
||||
shape={type}
|
||||
size={size ?? parseInt(width)}
|
||||
src={profileURL}
|
||||
/>
|
||||
) : (
|
||||
getAvatarElement()
|
||||
);
|
||||
|
||||
@ -0,0 +1,124 @@
|
||||
/*
|
||||
* Copyright 2024 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 { ThreadType } from '../generated/api/feed/createThread';
|
||||
|
||||
export const MOCK_ANNOUNCEMENT_DATA = {
|
||||
data: [
|
||||
{
|
||||
id: '36ea94c9-7f12-489c-94df-56cbefe14b2f',
|
||||
type: ThreadType.Announcement,
|
||||
href: 'http://localhost:8585/api/v1/feed/36ea94c9-7f12-489c-94df-56cbefe14b2f',
|
||||
threadTs: 1714026576902,
|
||||
about:
|
||||
'<#E::database::cy-database-service-373851.cypress-database-1714026557974>',
|
||||
entityId: '123f24e3-2a00-432e-b42b-b709f7ae74c0',
|
||||
createdBy: 'admin',
|
||||
updatedAt: 1714037939788,
|
||||
updatedBy: 'shreya',
|
||||
resolved: false,
|
||||
message: 'Cypress announcement',
|
||||
postsCount: 4,
|
||||
posts: [
|
||||
{
|
||||
id: 'ccf1ad4a-4cf0-4be9-bcc7-1459f533bab0',
|
||||
message: 'this is done!',
|
||||
postTs: 1714036398114,
|
||||
from: 'admin',
|
||||
reactions: [],
|
||||
},
|
||||
{
|
||||
id: '738eb0ae-0b71-4a13-8dd2-d7d7d73073b6',
|
||||
message: 'having a look on it!',
|
||||
postTs: 1714037894068,
|
||||
from: 'david',
|
||||
reactions: [],
|
||||
},
|
||||
{
|
||||
id: 'fdc984e7-2d69-4f06-8b94-531ff8b696f7',
|
||||
message: 'this if fixed and RCA given!',
|
||||
postTs: 1714037939785,
|
||||
from: 'shreya',
|
||||
reactions: [],
|
||||
},
|
||||
{
|
||||
id: '62434a57-57ec-4b5f-83c1-9ae5870337b6',
|
||||
message: 'test',
|
||||
postTs: 1714027952172,
|
||||
from: 'admin',
|
||||
reactions: [],
|
||||
},
|
||||
],
|
||||
reactions: [],
|
||||
announcement: {
|
||||
description: 'Cypress announcement description',
|
||||
startTime: 1713983400,
|
||||
endTime: 1714415400,
|
||||
},
|
||||
},
|
||||
],
|
||||
paging: {
|
||||
total: 1,
|
||||
},
|
||||
};
|
||||
|
||||
export const MOCK_ANNOUNCEMENT_FEED_DATA = {
|
||||
id: '36ea94c9-7f12-489c-94df-56cbefe14b2f',
|
||||
type: 'Announcement',
|
||||
href: 'http://localhost:8585/api/v1/feed/36ea94c9-7f12-489c-94df-56cbefe14b2f',
|
||||
threadTs: 1714026576902,
|
||||
about:
|
||||
'<#E::database::cy-database-service-373851.cypress-database-1714026557974>',
|
||||
entityId: '123f24e3-2a00-432e-b42b-b709f7ae74c0',
|
||||
createdBy: 'admin',
|
||||
updatedAt: 1714047427117,
|
||||
updatedBy: 'admin',
|
||||
resolved: false,
|
||||
message: 'Cypress announcement',
|
||||
postsCount: 4,
|
||||
posts: [
|
||||
{
|
||||
id: '62434a57-57ec-4b5f-83c1-9ae5870337b6',
|
||||
message: 'test',
|
||||
postTs: 1714027952172,
|
||||
from: 'admin',
|
||||
reactions: [],
|
||||
},
|
||||
{
|
||||
id: 'ccf1ad4a-4cf0-4be9-bcc7-1459f533bab0',
|
||||
message: 'this is done!',
|
||||
postTs: 1714036398114,
|
||||
from: 'admin',
|
||||
reactions: [],
|
||||
},
|
||||
{
|
||||
id: '738eb0ae-0b71-4a13-8dd2-d7d7d73073b6',
|
||||
message: 'having a look on it!',
|
||||
postTs: 1714037894068,
|
||||
from: 'david',
|
||||
reactions: [],
|
||||
},
|
||||
{
|
||||
id: 'fdc984e7-2d69-4f06-8b94-531ff8b696f7',
|
||||
message: 'this if fixed and RCA given!',
|
||||
postTs: 1714037939785,
|
||||
from: 'shreya',
|
||||
reactions: [],
|
||||
},
|
||||
],
|
||||
reactions: [],
|
||||
announcement: {
|
||||
description: 'Cypress announcement description.',
|
||||
startTime: 1713983400,
|
||||
endTime: 1714415400,
|
||||
},
|
||||
};
|
||||
@ -81,9 +81,10 @@
|
||||
@global-border: 1px solid @border-color;
|
||||
@active-color: #e8f4ff;
|
||||
@background-color: #ffffff;
|
||||
@announcement-background: #e3f2fd30;
|
||||
@announcement-background-dark: #9dd6ff;
|
||||
@announcement-border: @info-color;
|
||||
@announcement-background: #0950c50d;
|
||||
@announcement-background-dark: #e1edff;
|
||||
@announcement-border: @blue-3;
|
||||
@announcement-border-light: #e2e2e2;
|
||||
@test-parameter-bg-color: #e7ebf0;
|
||||
@group-title-color: #76746f;
|
||||
@light-border-color: #f0f0f0;
|
||||
|
||||
@ -184,7 +184,6 @@ export const generateSearchDropdownLabel = (
|
||||
<ProfilePicture
|
||||
displayName={option.label}
|
||||
name={option.label || ''}
|
||||
textClass="text-xs"
|
||||
width="18"
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -421,17 +421,18 @@ export const getNameFromFQN = (fqn: string): string => {
|
||||
|
||||
export const getRandomColor = (name: string) => {
|
||||
const firstAlphabet = name.charAt(0).toLowerCase();
|
||||
const asciiCode = firstAlphabet.charCodeAt(0);
|
||||
const colorNum =
|
||||
asciiCode.toString() + asciiCode.toString() + asciiCode.toString();
|
||||
// Convert the user's name to a numeric value
|
||||
let nameValue = 0;
|
||||
for (let i = 0; i < name.length; i++) {
|
||||
nameValue += name.charCodeAt(i) * 8;
|
||||
}
|
||||
|
||||
const num = Math.round(0xffffff * parseInt(colorNum));
|
||||
const r = (num >> 16) & 255;
|
||||
const g = (num >> 8) & 255;
|
||||
const b = num & 255;
|
||||
// Generate a random hue based on the name value
|
||||
const hue = nameValue % 360;
|
||||
|
||||
return {
|
||||
color: 'rgb(' + r + ', ' + g + ', ' + b + ', 0.6)',
|
||||
color: `hsl(${hue}, 70%, 40%)`,
|
||||
backgroundColor: `hsl(${hue}, 100%, 92%)`,
|
||||
character: firstAlphabet.toUpperCase(),
|
||||
};
|
||||
};
|
||||
|
||||
@ -385,9 +385,9 @@ export const deletePost = async (
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const deletResponse = await deletePostById(threadId, postId);
|
||||
const deleteResponse = await deletePostById(threadId, postId);
|
||||
// get updated thread only if delete response and callback is present
|
||||
if (deletResponse && callback) {
|
||||
if (deleteResponse && callback) {
|
||||
const data = await getUpdatedThread(threadId);
|
||||
callback((pre) => {
|
||||
return pre.map((thread) => {
|
||||
@ -437,62 +437,60 @@ export const getEntityFieldDisplay = (entityField: string) => {
|
||||
return null;
|
||||
};
|
||||
|
||||
export const updateThreadData = (
|
||||
export const updateThreadData = async (
|
||||
threadId: string,
|
||||
postId: string,
|
||||
isThread: boolean,
|
||||
data: Operation[],
|
||||
callback: (value: React.SetStateAction<Thread[]>) => void
|
||||
) => {
|
||||
): Promise<void> => {
|
||||
if (isThread) {
|
||||
updateThread(threadId, data)
|
||||
.then((res) => {
|
||||
callback((prevData) => {
|
||||
return prevData.map((thread) => {
|
||||
if (isEqual(threadId, thread.id)) {
|
||||
return {
|
||||
...thread,
|
||||
reactions: res.reactions,
|
||||
message: res.message,
|
||||
announcement: res?.announcement,
|
||||
};
|
||||
} else {
|
||||
return thread;
|
||||
}
|
||||
});
|
||||
try {
|
||||
const res = await updateThread(threadId, data);
|
||||
callback((prevData) => {
|
||||
return prevData.map((thread) => {
|
||||
if (isEqual(threadId, thread.id)) {
|
||||
return {
|
||||
...thread,
|
||||
reactions: res.reactions,
|
||||
message: res.message,
|
||||
announcement: res?.announcement,
|
||||
};
|
||||
} else {
|
||||
return thread;
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch((err: AxiosError) => {
|
||||
showErrorToast(err);
|
||||
});
|
||||
} catch (error) {
|
||||
showErrorToast(error as AxiosError);
|
||||
}
|
||||
} else {
|
||||
updatePost(threadId, postId, data)
|
||||
.then((res) => {
|
||||
callback((prevData) => {
|
||||
return prevData.map((thread) => {
|
||||
if (isEqual(threadId, thread.id)) {
|
||||
const updatedPosts = (thread.posts || []).map((post) => {
|
||||
if (isEqual(postId, post.id)) {
|
||||
return {
|
||||
...post,
|
||||
reactions: res.reactions,
|
||||
message: res.message,
|
||||
};
|
||||
} else {
|
||||
return post;
|
||||
}
|
||||
});
|
||||
try {
|
||||
const res = await updatePost(threadId, postId, data);
|
||||
callback((prevData) => {
|
||||
return prevData.map((thread) => {
|
||||
if (isEqual(threadId, thread.id)) {
|
||||
const updatedPosts = (thread.posts || []).map((post) => {
|
||||
if (isEqual(postId, post.id)) {
|
||||
return {
|
||||
...post,
|
||||
reactions: res.reactions,
|
||||
message: res.message,
|
||||
};
|
||||
} else {
|
||||
return post;
|
||||
}
|
||||
});
|
||||
|
||||
return { ...thread, posts: updatedPosts };
|
||||
} else {
|
||||
return thread;
|
||||
}
|
||||
});
|
||||
return { ...thread, posts: updatedPosts };
|
||||
} else {
|
||||
return thread;
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch((err: AxiosError) => {
|
||||
showErrorToast(err);
|
||||
});
|
||||
} catch (error) {
|
||||
showErrorToast(error as AxiosError);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user