Fix: #21276 Only 3 Comments Shown by Default in Incident View (#21577)

* Fix: #21276 Only 3 Comments Shown by Default in Incident View

* fixed failing unit test

* addressing comments.
This commit is contained in:
Shailesh Parmar 2025-06-09 11:22:18 +05:30 committed by GitHub
parent 78f523fd13
commit 436df10a7f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 207 additions and 75 deletions

View File

@ -365,6 +365,52 @@ test.describe('Activity feed', () => {
await checkTaskCountInActivityFeed(page, 0, 1);
});
test('Replies should be visible in the task feed', async ({ page }) => {
const value: TaskDetails = {
term: entity2.entity.displayName,
assignee: user1.responseData.name,
};
await redirectToHomePage(page);
await entity2.visitEntityPage(page);
await page.getByTestId('request-description').click();
await createDescriptionTask(page, value);
// Task 1 - Update Description right panel check
const descriptionTask = await page.getByTestId('task-title').innerText();
expect(descriptionTask).toContain('Request to update description');
for (let i = 0; i < 10; i++) {
const commentInput = page.locator('[data-testid="comments-input-field"]');
commentInput.click();
await page.fill(
'[data-testid="editor-wrapper"] .ql-editor',
`Reply message ${i}`
);
const sendReply = page.waitForResponse('/api/v1/feed/*/posts');
await page.getByTestId('send-button').click({ force: true });
await sendReply;
}
await page.reload();
await page.waitForSelector('[data-testid="loader"]', {
state: 'hidden',
});
await page.waitForLoadState('networkidle');
await expect(page.getByTestId('feed-reply-card')).toHaveCount(10);
for (let i = 0; i < 10; i++) {
await expect(
page.locator('.right-container [data-testid="feed-replies"]')
).toContainText(`Reply message ${i}`);
}
});
test('Open and Closed Task Tab with approve from Task Feed Card', async ({
page,
}) => {

View File

@ -10,10 +10,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Card, Col, Input, Space, Tooltip, Typography } from 'antd';
import { Card, Col, Input, Skeleton, Space, Tooltip, Typography } from 'antd';
import classNames from 'classnames';
import { compare } from 'fast-json-patch';
import { isUndefined } from 'lodash';
import { isUndefined, orderBy } from 'lodash';
import React, { useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
@ -75,10 +75,10 @@ const ActivityFeedCardNew = ({
}, [feed.about]);
const { t } = useTranslation();
const { currentUser } = useApplicationStore();
const { selectedThread, postFeed } = useActivityFeedProvider();
const { selectedThread, postFeed, updateFeed, isPostsLoading } =
useActivityFeedProvider();
const [showFeedEditor, setShowFeedEditor] = useState<boolean>(false);
const [isEditPost, setIsEditPost] = useState<boolean>(false);
const { updateFeed } = useActivityFeedProvider();
const [, , user] = useUserProfile({
permission: true,
name: feed.createdBy ?? '',
@ -183,6 +183,38 @@ const ActivityFeedCardNew = ({
setShowFeedEditor(false);
};
const posts = useMemo(() => {
if (!showThread) {
return null;
}
if (isPostsLoading) {
return (
<Space className="m-y-md" direction="vertical" size={16}>
<Skeleton active />
<Skeleton active />
<Skeleton active />
</Space>
);
}
const posts = orderBy(feed.posts, ['postTs'], ['desc']);
return (
<Col className="p-l-0 p-r-0" data-testid="feed-replies">
{posts.map((reply, index, arr) => (
<CommentCard
closeFeedEditor={closeFeedEditor}
feed={feed}
isLastReply={index === arr.length - 1}
key={reply.id}
post={reply}
/>
))}
</Col>
);
}, [feed, showThread, closeFeedEditor, isPostsLoading]);
return (
<Card
className={classNames(
@ -330,22 +362,7 @@ const ActivityFeedCardNew = ({
</div>
)}
{showThread && feed?.posts && feed?.posts?.length > 0 && (
<Col className="p-l-0 p-r-0" data-testid="feed-replies">
{feed?.posts
?.slice()
.sort((a, b) => (b.postTs as number) - (a.postTs as number))
.map((reply, index, arr) => (
<CommentCard
closeFeedEditor={closeFeedEditor}
feed={feed}
isLastReply={index === arr.length - 1}
key={reply.id}
post={reply}
/>
))}
</Col>
)}
{posts}
</div>
)}
</Card>

View File

@ -134,6 +134,7 @@ const CommentCard = ({
className={classNames('d-flex justify-start relative reply-card', {
'reply-card-border-bottom': !isLastReply,
})}
data-testid="feed-reply-card"
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}>
<div className="profile-picture m-r-xs">

View File

@ -14,7 +14,7 @@
import { Col, Drawer, Row } from 'antd';
import classNames from 'classnames';
import React, { FC } from 'react';
import { Thread, ThreadType } from '../../../generated/entity/feed/thread';
import { ThreadType } from '../../../generated/entity/feed/thread';
import FeedPanelBodyV1 from '../ActivityFeedPanel/FeedPanelBodyV1';
import FeedPanelHeader from '../ActivityFeedPanel/FeedPanelHeader';
import { useActivityFeedProvider } from '../ActivityFeedProvider/ActivityFeedProvider';
@ -31,6 +31,10 @@ const ActivityFeedDrawer: FC<ActivityFeedDrawerProps> = ({
}) => {
const { hideDrawer, selectedThread } = useActivityFeedProvider();
if (!selectedThread) {
return null;
}
return (
<Drawer
className={classNames('activity-feed-drawer', className)}
@ -57,7 +61,7 @@ const ActivityFeedDrawer: FC<ActivityFeedDrawerProps> = ({
showThreadIcon: false,
showRepliesContainer: false,
}}
feed={selectedThread as Thread}
feed={selectedThread}
hidePopover={false}
/>
</Col>

View File

@ -12,7 +12,7 @@
*/
import { Typography } from 'antd';
import classNames from 'classnames';
import { isEmpty } from 'lodash';
import { isEmpty, isUndefined } from 'lodash';
import React, { ReactNode, useEffect, useMemo, useState } from 'react';
import { ReactComponent as FeedEmptyIcon } from '../../../assets/svg/ic-task-empty.svg';
import { ERROR_PLACEHOLDER_TYPE } from '../../../enums/common.enum';
@ -69,11 +69,10 @@ const ActivityFeedListV1New = ({
}, [feedList]);
useEffect(() => {
if (onFeedClick) {
onFeedClick(
entityThread.find((feed) => feed.id === selectedThread?.id) ??
entityThread[0]
);
const thread = entityThread.find((feed) => feed.id === selectedThread?.id);
if (onFeedClick && (isUndefined(selectedThread) || isUndefined(thread))) {
onFeedClick(entityThread[0]);
}
}, [entityThread, selectedThread, onFeedClick]);

View File

@ -24,6 +24,7 @@ import React, {
} from 'react';
import { useTranslation } from 'react-i18next';
import { PAGE_SIZE_LARGE } from '../../../constants/constants';
import { POST_FEED_PAGE_COUNT } from '../../../constants/Feeds.constants';
import { EntityType } from '../../../enums/entity.enum';
import { FeedFilter } from '../../../enums/mydata.enum';
import { ReactionOperation } from '../../../enums/reactions.enum';
@ -43,6 +44,7 @@ import {
deleteThread,
getAllFeeds,
getFeedById,
getPostsFeedById,
postFeedById,
updatePost,
updateThread,
@ -71,6 +73,9 @@ const ActivityFeedProvider = ({ children, user }: Props) => {
const [entityPaging, setEntityPaging] = useState<Paging>({} as Paging);
const [focusReplyEditor, setFocusReplyEditor] = useState<boolean>(false);
const [loading, setLoading] = useState(false);
const [isPostsLoading, setIsPostsLoading] = useState(false);
const [isTestCaseResolutionLoading, setIsTestCaseResolutionLoading] =
useState(false);
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
const [selectedThread, setSelectedThread] = useState<Thread>();
const [testCaseResolutionStatus, setTestCaseResolutionStatus] = useState<
@ -80,6 +85,7 @@ const ActivityFeedProvider = ({ children, user }: Props) => {
const { currentUser } = useApplicationStore();
const fetchTestCaseResolution = useCallback(async (id: string) => {
setIsTestCaseResolutionLoading(true);
try {
const { data } = await getListTestCaseIncidentByStateId(id, {
limit: PAGE_SIZE_LARGE,
@ -90,22 +96,40 @@ const ActivityFeedProvider = ({ children, user }: Props) => {
);
} catch (error) {
setTestCaseResolutionStatus([]);
} finally {
setIsTestCaseResolutionLoading(false);
}
}, []);
const fetchPostsFeed = useCallback(async (active: Thread) => {
// If the posts count is greater than the page count, fetch the posts
if (
active?.postsCount &&
active?.postsCount > POST_FEED_PAGE_COUNT &&
active?.posts?.length !== active?.postsCount
) {
setIsPostsLoading(true);
try {
const { data } = await getPostsFeedById(active.id);
setSelectedThread((pre) =>
pre?.id === active?.id ? { ...active, posts: data } : pre
);
} finally {
setIsPostsLoading(false);
}
}
}, []);
const setActiveThread = useCallback((active?: Thread) => {
setSelectedThread(active);
active && fetchPostsFeed(active);
if (
active &&
active.task?.type === TaskType.RequestTestCaseFailureResolution &&
active.task?.testCaseResolutionStatusId
) {
setLoading(true);
fetchTestCaseResolution(active.task.testCaseResolutionStatusId).finally(
() => {
setLoading(false);
}
);
fetchTestCaseResolution(active.task.testCaseResolutionStatusId);
}
}, []);
@ -408,6 +432,8 @@ const ActivityFeedProvider = ({ children, user }: Props) => {
selectedThread,
isDrawerOpen,
loading,
isPostsLoading,
isTestCaseResolutionLoading,
focusReplyEditor,
refreshActivityFeed,
deleteFeed,
@ -431,6 +457,8 @@ const ActivityFeedProvider = ({ children, user }: Props) => {
selectedThread,
isDrawerOpen,
loading,
isPostsLoading,
isTestCaseResolutionLoading,
focusReplyEditor,
refreshActivityFeed,
deleteFeed,

View File

@ -26,6 +26,8 @@ import { Paging } from '../../../generated/type/paging';
export interface ActivityFeedProviderContextType {
loading: boolean;
isPostsLoading?: boolean;
isTestCaseResolutionLoading?: boolean;
entityThread: Thread[];
selectedThread: Thread | undefined;
isDrawerOpen: boolean;

View File

@ -294,9 +294,11 @@ export const ActivityFeedTab = ({
if (!feed && (isTaskActiveTab || isMentionTabSelected)) {
setIsFullWidth(false);
}
setActiveThread(feed);
if (selectedThread?.id !== feed?.id) {
setActiveThread(feed);
}
},
[setActiveThread, isTaskActiveTab, isMentionTabSelected]
[setActiveThread, isTaskActiveTab, isMentionTabSelected, selectedThread]
);
useEffect(() => {

View File

@ -336,17 +336,17 @@ const TaskFeedCard = ({
width={20}
onClick={isForFeedTab ? showReplies : undefined}
/>
{feed.posts && feed.posts?.length > 0 && (
{feed?.postsCount && feed?.postsCount > 0 && (
<Button
className="posts-length m-r-xss p-0 remove-button-default-styling"
data-testid="replies-count"
type="link"
onClick={isForFeedTab ? showReplies : undefined}>
{t(
feed.posts.length === 1
feed.postsCount === 1
? 'label.one-reply'
: 'label.number-reply-plural',
{ number: feed.posts.length }
{ number: feed.postsCount }
)}
</Button>
)}

View File

@ -33,11 +33,11 @@ import {
import { useElementInView } from '../../../../hooks/useElementInView';
import { useFqn } from '../../../../hooks/useFqn';
import { useTestCaseStore } from '../../../../pages/IncidentManager/IncidentManagerDetailPage/useTestCase.store';
import ActivityFeedListV1 from '../../../ActivityFeed/ActivityFeedList/ActivityFeedListV1.component';
import ActivityFeedListV1New from '../../../ActivityFeed/ActivityFeedList/ActivityFeedListV1New.component';
import { useActivityFeedProvider } from '../../../ActivityFeed/ActivityFeedProvider/ActivityFeedProvider';
import { TaskFilter } from '../../../ActivityFeed/ActivityFeedTab/ActivityFeedTab.interface';
import Loader from '../../../common/Loader/Loader';
import { TaskTab } from '../../../Entity/Task/TaskTab/TaskTab.component';
import { TaskTabNew } from '../../../Entity/Task/TaskTab/TaskTabNew.component';
import './test-case-incident-tab.style.less';
const TestCaseIncidentTab = () => {
@ -83,9 +83,11 @@ const TestCaseIncidentTab = () => {
const handleFeedClick = useCallback(
(feed: Thread) => {
setActiveThread(feed);
if (selectedThread?.id !== feed?.id) {
setActiveThread(feed);
}
},
[setActiveThread]
[setActiveThread, selectedThread]
);
const loader = useMemo(() => (loading ? <Loader /> : null), [loading]);
@ -140,7 +142,7 @@ const TestCaseIncidentTab = () => {
className="left-container"
data-testid="left-container"
id="left-container">
<div className="d-flex gap-4 p-sm p-x-lg activity-feed-task">
<div className="d-flex gap-4 p-sm p-x-lg">
<Typography.Text
className={classNames(
'cursor-pointer p-l-xss d-flex items-center',
@ -164,13 +166,14 @@ const TestCaseIncidentTab = () => {
</Typography.Text>
</div>
<ActivityFeedListV1
<ActivityFeedListV1New
hidePopover
activeFeedId={selectedThread?.id}
emptyPlaceholderText={t('message.no-tasks-assigned')}
feedList={threads}
isForFeedTab={false}
isLoading={false}
selectedThread={selectedThread}
showThread={false}
onFeedClick={handleFeedClick}
/>
@ -187,7 +190,7 @@ const TestCaseIncidentTab = () => {
{loader}
{selectedThread && !loading && (
<div id="task-panel">
<TaskTab
<TaskTabNew
entityType={EntityType.TEST_CASE}
isForFeedTab={false}
owners={owners}

View File

@ -41,9 +41,9 @@ jest.mock(
})
);
jest.mock('../../../Entity/Task/TaskTab/TaskTab.component', () => {
jest.mock('../../../Entity/Task/TaskTab/TaskTabNew.component', () => {
return {
TaskTab: jest.fn().mockImplementation(({ onAfterClose }) => (
TaskTabNew: jest.fn().mockImplementation(({ onAfterClose }) => (
<div>
TaskTab
<button data-testid="close-btn" onClick={onAfterClose}>
@ -57,7 +57,7 @@ jest.mock('../../../common/Loader/Loader', () => {
return jest.fn().mockImplementation(() => <div>Loader</div>);
});
jest.mock(
'../../../ActivityFeed/ActivityFeedList/ActivityFeedListV1.component',
'../../../ActivityFeed/ActivityFeedList/ActivityFeedListV1New.component',
() => {
return jest.fn().mockImplementation(({ onFeedClick }) => (
<div>

View File

@ -21,6 +21,7 @@ import {
MenuProps,
Row,
Select,
Skeleton,
Space,
Tooltip,
Typography,
@ -35,6 +36,7 @@ import {
isEqual,
isUndefined,
last,
orderBy,
startCase,
unionBy,
} from 'lodash';
@ -163,6 +165,7 @@ export const TaskTabNew = ({
fetchUpdatedThread,
updateTestCaseIncidentStatus,
testCaseResolutionStatus,
isPostsLoading,
} = useActivityFeedProvider();
const isTaskDescription = isDescriptionTask(taskDetails?.type as TaskType);
@ -1047,6 +1050,34 @@ export const TaskTabNew = ({
setShowFeedEditor(false);
};
const posts = useMemo(() => {
if (isPostsLoading) {
return (
<Space className="m-y-md" direction="vertical" size={16}>
<Skeleton active />
<Skeleton active />
<Skeleton active />
</Space>
);
}
const posts = orderBy(taskThread.posts, ['postTs'], ['desc']);
return (
<Col className="p-l-0 p-r-0" data-testid="feed-replies">
{posts.map((reply, index, arr) => (
<CommentCard
closeFeedEditor={closeFeedEditor}
feed={taskThread}
isLastReply={index === arr.length - 1}
key={reply.id}
post={reply}
/>
))}
</Col>
);
}, [taskThread, closeFeedEditor, isPostsLoading]);
useEffect(() => {
closeFeedEditor();
}, [taskThread.id]);
@ -1149,22 +1180,7 @@ export const TaskTabNew = ({
)
)}
{taskThread?.posts && taskThread?.posts?.length > 0 && (
<Col className="p-l-0 p-r-0" data-testid="feed-replies">
{taskThread?.posts
?.slice()
.sort((a, b) => (b.postTs as number) - (a.postTs as number))
.map((reply, index, arr) => (
<CommentCard
closeFeedEditor={closeFeedEditor}
feed={taskThread}
isLastReply={index === arr.length - 1}
key={reply.id}
post={reply}
/>
))}
</Col>
)}
{posts}
</div>
</Col>
</Col>

View File

@ -10,7 +10,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Col, Row, Steps, Typography } from 'antd';
import { Col, Row, Skeleton, Steps, Typography } from 'antd';
import { last, toLower } from 'lodash';
import React, { ReactNode, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
@ -36,7 +36,8 @@ import './task-tab-incident-manager-header.style.less';
const TaskTabIncidentManagerHeaderNew = ({ thread }: { thread: Thread }) => {
const { t } = useTranslation();
const { testCaseResolutionStatus } = useActivityFeedProvider();
const { testCaseResolutionStatus, isTestCaseResolutionLoading } =
useActivityFeedProvider();
const testCaseResolutionStepper = useMemo(() => {
const updatedData = [...testCaseResolutionStatus];
const lastStatusType = last(
@ -215,14 +216,18 @@ const TaskTabIncidentManagerHeaderNew = ({ thread }: { thread: Thread }) => {
)}
<Col className="p-l-0" span={24}>
<div className="task-resolution-steps-container-new">
<Steps
className="task-resolution-steps w-full"
current={testCaseResolutionStatus.length}
data-testid="task-resolution-steps"
items={testCaseResolutionStepper}
labelPlacement="vertical"
size="small"
/>
{isTestCaseResolutionLoading ? (
<Skeleton active />
) : (
<Steps
className="task-resolution-steps w-full"
current={testCaseResolutionStatus.length}
data-testid="task-resolution-steps"
items={testCaseResolutionStepper}
labelPlacement="vertical"
size="small"
/>
)}
</div>
</Col>
</Row>

View File

@ -24,6 +24,9 @@ export const teamsLinkRegEx = /\((.+?\/\/.+?)\/(.+?\/.+?\/.+?)\/(.+?)\)/;
export const entityLinkRegEx = /<#E::([^<>]+?)::([^<>]+?)>/g;
export const entityRegex = /<#E::([^<>]+?)::([^<>]+?)\|(\[(.+?)?\]\((.+?)?\))>/;
// 3 is the default page count for the post feed in list API
export const POST_FEED_PAGE_COUNT = 3;
export const ENTITY_URL_MAP = {
team: 'settings/members/teams',
user: 'users',

View File

@ -126,6 +126,12 @@ export const postFeedById = async (id: string, data: Post) => {
return response.data;
};
export const getPostsFeedById = async (id: string) => {
const response = await APIClient.get<{ data: Post[] }>(`/feed/${id}/posts`);
return response.data;
};
export const deletePostById = (threadId: string, postId: string) => {
return APIClient.delete<Post>(`/feed/${threadId}/posts/${postId}`);
};