From fe4d8657cb0e3b5bbe827dfa9a6188eef65c8e36 Mon Sep 17 00:00:00 2001 From: Lucano Vera Date: Tue, 20 May 2025 13:36:46 -0300 Subject: [PATCH 01/32] Initial implementation for comments ui --- .../privacy-requests/PrivacyRequest.tsx | 4 +- .../events-and-logs/ActivityTab.tsx | 41 +++++++++ .../events-and-logs/CommentInput.tsx | 83 +++++++++++++++++++ 3 files changed, 126 insertions(+), 2 deletions(-) create mode 100644 clients/admin-ui/src/features/privacy-requests/events-and-logs/ActivityTab.tsx create mode 100644 clients/admin-ui/src/features/privacy-requests/events-and-logs/CommentInput.tsx diff --git a/clients/admin-ui/src/features/privacy-requests/PrivacyRequest.tsx b/clients/admin-ui/src/features/privacy-requests/PrivacyRequest.tsx index 48b431f0743..933584c3e30 100644 --- a/clients/admin-ui/src/features/privacy-requests/PrivacyRequest.tsx +++ b/clients/admin-ui/src/features/privacy-requests/PrivacyRequest.tsx @@ -4,7 +4,7 @@ import { useMemo, useState } from "react"; import { useGetAllPrivacyRequestsQuery } from "~/features/privacy-requests"; import { PrivacyRequestStatus } from "~/types/api"; -import ActivityTimeline from "./events-and-logs/ActivityTimeline"; +import ActivityTab from "./events-and-logs/ActivityTab"; import ManualProcessingList from "./manual-processing/ManualProcessingList"; import RequestDetails from "./RequestDetails"; import { PrivacyRequestEntity } from "./types"; @@ -46,7 +46,7 @@ const PrivacyRequest = ({ data: initialData }: PrivacyRequestProps) => { { key: "activity", label: "Activity", - children: , + children: , }, { key: "manual-steps", diff --git a/clients/admin-ui/src/features/privacy-requests/events-and-logs/ActivityTab.tsx b/clients/admin-ui/src/features/privacy-requests/events-and-logs/ActivityTab.tsx new file mode 100644 index 00000000000..7fd00c9491e --- /dev/null +++ b/clients/admin-ui/src/features/privacy-requests/events-and-logs/ActivityTab.tsx @@ -0,0 +1,41 @@ +import { AntButton as Button, AntFlex as Flex } from "fidesui"; +import { useState } from "react"; + +import { PrivacyRequestEntity } from "../types"; +import ActivityTimeline from "./ActivityTimeline"; +import { CommentInput } from "./CommentInput"; + +type ActivityTabProps = { + subjectRequest: PrivacyRequestEntity; +}; + +const ActivityTab = ({ subjectRequest }: ActivityTabProps) => { + const [showCommentInput, setShowCommentInput] = useState(false); + + return ( +
+ + +
+ {showCommentInput ? ( + setShowCommentInput(false)} + /> + ) : ( + + + + )} +
+
+ ); +}; + +export default ActivityTab; diff --git a/clients/admin-ui/src/features/privacy-requests/events-and-logs/CommentInput.tsx b/clients/admin-ui/src/features/privacy-requests/events-and-logs/CommentInput.tsx new file mode 100644 index 00000000000..5f52c877ac9 --- /dev/null +++ b/clients/admin-ui/src/features/privacy-requests/events-and-logs/CommentInput.tsx @@ -0,0 +1,83 @@ +import { + AntButton as Button, + AntFlex as Flex, + AntInput as Input, + AntTabs as Tabs, + AntTabsProps as TabsProps, +} from "fidesui"; +import { useEffect, useRef, useState } from "react"; + +export interface CommentInputProps { + privacyRequestId: string; + onCancel: () => void; +} + +export const CommentInput = ({ + privacyRequestId, + onCancel, +}: CommentInputProps) => { + const [commentText, setCommentText] = useState(""); + const textAreaRef = useRef(null); + + // Focus the textarea when the component mounts + useEffect(() => { + if (textAreaRef.current) { + textAreaRef.current.focus(); + } + }, []); + + const handleSubmit = () => { + if (commentText.trim()) { + // Just log the comment for now + console.log("Comment submitted:", { + privacyRequestId, + commentText, + }); + + // Reset and close + setCommentText(""); + onCancel(); + } + }; + + const items: TabsProps["items"] = [ + { + key: "internal", + label: "Internal comment", + children: ( +
+ setCommentText(e.target.value)} + placeholder="Add a note about this privacy request..." + rows={3} + className="mb-4 w-full" + data-testid="comment-input" + /> +
+ ), + }, + ]; + + return ( +
+ + + + + + +
+ ); +}; From 1003598dca4b9d19a7a00a658a5e6bd9474f1bd9 Mon Sep 17 00:00:00 2001 From: Lucano Vera Date: Tue, 20 May 2025 13:45:58 -0300 Subject: [PATCH 02/32] remove placeholder --- .../features/privacy-requests/events-and-logs/CommentInput.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/clients/admin-ui/src/features/privacy-requests/events-and-logs/CommentInput.tsx b/clients/admin-ui/src/features/privacy-requests/events-and-logs/CommentInput.tsx index 5f52c877ac9..91112d8983d 100644 --- a/clients/admin-ui/src/features/privacy-requests/events-and-logs/CommentInput.tsx +++ b/clients/admin-ui/src/features/privacy-requests/events-and-logs/CommentInput.tsx @@ -50,7 +50,6 @@ export const CommentInput = ({ ref={textAreaRef} value={commentText} onChange={(e) => setCommentText(e.target.value)} - placeholder="Add a note about this privacy request..." rows={3} className="mb-4 w-full" data-testid="comment-input" From 5bc4b80762e0ab5d68f9774ef9f7efc19975add8 Mon Sep 17 00:00:00 2001 From: Lucano Vera Date: Tue, 20 May 2025 13:47:58 -0300 Subject: [PATCH 03/32] update types --- ...request__privacy_request_id__attachment_post.ts} | 2 +- ...cy_request__privacy_request_id__comment_post.ts} | 2 +- .../src/types/api/models/Page_CommentResponse_.ts | 13 +++++++++++++ 3 files changed, 15 insertions(+), 2 deletions(-) rename clients/admin-ui/src/types/api/models/{Body_create_privacy_request_attachment_api_v1_plus_privacy_request__privacy_request_id__attachment_post.ts => Body_create_attachment_api_v1_plus_privacy_request__privacy_request_id__attachment_post.ts} (62%) rename clients/admin-ui/src/types/api/models/{Body_create_privacy_request_comment_api_v1_plus_privacy_request__privacy_request_id__comment_post.ts => Body_create_comment_api_v1_plus_privacy_request__privacy_request_id__comment_post.ts} (73%) create mode 100644 clients/admin-ui/src/types/api/models/Page_CommentResponse_.ts diff --git a/clients/admin-ui/src/types/api/models/Body_create_privacy_request_attachment_api_v1_plus_privacy_request__privacy_request_id__attachment_post.ts b/clients/admin-ui/src/types/api/models/Body_create_attachment_api_v1_plus_privacy_request__privacy_request_id__attachment_post.ts similarity index 62% rename from clients/admin-ui/src/types/api/models/Body_create_privacy_request_attachment_api_v1_plus_privacy_request__privacy_request_id__attachment_post.ts rename to clients/admin-ui/src/types/api/models/Body_create_attachment_api_v1_plus_privacy_request__privacy_request_id__attachment_post.ts index 02c097ac211..bc949d18d09 100644 --- a/clients/admin-ui/src/types/api/models/Body_create_privacy_request_attachment_api_v1_plus_privacy_request__privacy_request_id__attachment_post.ts +++ b/clients/admin-ui/src/types/api/models/Body_create_attachment_api_v1_plus_privacy_request__privacy_request_id__attachment_post.ts @@ -4,7 +4,7 @@ import type { AttachmentType } from "./AttachmentType"; -export type Body_create_privacy_request_attachment_api_v1_plus_privacy_request__privacy_request_id__attachment_post = +export type Body_create_attachment_api_v1_plus_privacy_request__privacy_request_id__attachment_post = { attachment_type: AttachmentType; attachment_file: Blob; diff --git a/clients/admin-ui/src/types/api/models/Body_create_privacy_request_comment_api_v1_plus_privacy_request__privacy_request_id__comment_post.ts b/clients/admin-ui/src/types/api/models/Body_create_comment_api_v1_plus_privacy_request__privacy_request_id__comment_post.ts similarity index 73% rename from clients/admin-ui/src/types/api/models/Body_create_privacy_request_comment_api_v1_plus_privacy_request__privacy_request_id__comment_post.ts rename to clients/admin-ui/src/types/api/models/Body_create_comment_api_v1_plus_privacy_request__privacy_request_id__comment_post.ts index c7abbd35b2f..2fef4454559 100644 --- a/clients/admin-ui/src/types/api/models/Body_create_privacy_request_comment_api_v1_plus_privacy_request__privacy_request_id__comment_post.ts +++ b/clients/admin-ui/src/types/api/models/Body_create_comment_api_v1_plus_privacy_request__privacy_request_id__comment_post.ts @@ -5,7 +5,7 @@ import type { AttachmentType } from "./AttachmentType"; import type { CommentType } from "./CommentType"; -export type Body_create_privacy_request_comment_api_v1_plus_privacy_request__privacy_request_id__comment_post = +export type Body_create_comment_api_v1_plus_privacy_request__privacy_request_id__comment_post = { comment_text: string; comment_type: CommentType; diff --git a/clients/admin-ui/src/types/api/models/Page_CommentResponse_.ts b/clients/admin-ui/src/types/api/models/Page_CommentResponse_.ts new file mode 100644 index 00000000000..2e5207fd995 --- /dev/null +++ b/clients/admin-ui/src/types/api/models/Page_CommentResponse_.ts @@ -0,0 +1,13 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { CommentResponse } from "./CommentResponse"; + +export type Page_CommentResponse_ = { + items: Array; + total: number | null; + page: number | null; + size: number | null; + pages?: number | null; +}; From 8909068a2fd06c15e5f5a3ac93e69192f2a74ba2 Mon Sep 17 00:00:00 2001 From: Lucano Vera Date: Tue, 20 May 2025 15:34:11 -0300 Subject: [PATCH 04/32] Implement comments saving --- .../admin-ui/src/features/common/api.slice.ts | 1 + .../privacy-request-comments.slice.ts | 77 +++++++++++++++++++ .../events-and-logs/CommentInput.tsx | 34 +++++--- 3 files changed, 103 insertions(+), 9 deletions(-) create mode 100644 clients/admin-ui/src/features/privacy-requests/comments/privacy-request-comments.slice.ts diff --git a/clients/admin-ui/src/features/common/api.slice.ts b/clients/admin-ui/src/features/common/api.slice.ts index bd513ec75b3..00d41823631 100644 --- a/clients/admin-ui/src/features/common/api.slice.ts +++ b/clients/admin-ui/src/features/common/api.slice.ts @@ -60,6 +60,7 @@ export const baseApi = createApi({ "Privacy Notices", "Privacy Notice Translations", "Privacy Request Attachments", + "Privacy Request Comments", "Property", "Property-Specific Messaging Templates", "Purpose", diff --git a/clients/admin-ui/src/features/privacy-requests/comments/privacy-request-comments.slice.ts b/clients/admin-ui/src/features/privacy-requests/comments/privacy-request-comments.slice.ts new file mode 100644 index 00000000000..dbf0c98c73b --- /dev/null +++ b/clients/admin-ui/src/features/privacy-requests/comments/privacy-request-comments.slice.ts @@ -0,0 +1,77 @@ +import { createSlice } from "@reduxjs/toolkit"; + +import { baseApi } from "~/features/common/api.slice"; +import { CommentResponse } from "~/types/api/models/CommentResponse"; +import { CommentType } from "~/types/api/models/CommentType"; +import { Page_CommentResponse_ } from "~/types/api/models/Page_CommentResponse_"; + +export interface State {} + +const initialState: State = {}; + +interface GetCommentsParams { + privacy_request_id: string; + page?: number; + size?: number; +} + +interface CreateCommentParams { + privacy_request_id: string; + comment_text: string; + comment_type: CommentType; +} + +const privacyRequestCommentsApi = baseApi.injectEndpoints({ + endpoints: (build) => ({ + getComments: build.query({ + query: ({ privacy_request_id, page = 1, size }) => ({ + url: `plus/privacy-request/${privacy_request_id}/comment`, + method: "GET", + params: { + page, + size, + }, + }), + providesTags: ["Privacy Request Comments"], + }), + createComment: build.mutation({ + query: ({ privacy_request_id, comment_text, comment_type }) => { + const formData = new FormData(); + formData.append("comment_text", comment_text); + formData.append("comment_type", comment_type); + + return { + url: `plus/privacy-request/${privacy_request_id}/comment`, + method: "POST", + body: formData, + formData: true, + }; + }, + invalidatesTags: ["Privacy Request Comments", "Request"], + }), + getCommentDetails: build.query< + CommentResponse, + { privacy_request_id: string; comment_id: string } + >({ + query: ({ privacy_request_id, comment_id }) => ({ + url: `plus/privacy-request/${privacy_request_id}/comment/${comment_id}`, + method: "GET", + }), + }), + }), +}); + +export const { + useGetCommentsQuery, + useCreateCommentMutation, + useGetCommentDetailsQuery, + useLazyGetCommentDetailsQuery, +} = privacyRequestCommentsApi; + +export const privacyRequestCommentsSlice = createSlice({ + name: "privacyRequestComments", + initialState, + reducers: {}, +}); + +export const { reducer } = privacyRequestCommentsSlice; diff --git a/clients/admin-ui/src/features/privacy-requests/events-and-logs/CommentInput.tsx b/clients/admin-ui/src/features/privacy-requests/events-and-logs/CommentInput.tsx index 91112d8983d..8a89c368d01 100644 --- a/clients/admin-ui/src/features/privacy-requests/events-and-logs/CommentInput.tsx +++ b/clients/admin-ui/src/features/privacy-requests/events-and-logs/CommentInput.tsx @@ -2,11 +2,16 @@ import { AntButton as Button, AntFlex as Flex, AntInput as Input, + AntMessage as message, AntTabs as Tabs, AntTabsProps as TabsProps, } from "fidesui"; import { useEffect, useRef, useState } from "react"; +import { CommentType } from "~/types/api/models/CommentType"; + +import { useCreateCommentMutation } from "../comments/privacy-request-comments.slice"; + export interface CommentInputProps { privacyRequestId: string; onCancel: () => void; @@ -18,6 +23,7 @@ export const CommentInput = ({ }: CommentInputProps) => { const [commentText, setCommentText] = useState(""); const textAreaRef = useRef(null); + const [createComment, { isLoading }] = useCreateCommentMutation(); // Focus the textarea when the component mounts useEffect(() => { @@ -26,17 +32,26 @@ export const CommentInput = ({ } }, []); - const handleSubmit = () => { + const handleSubmit = async () => { if (commentText.trim()) { - // Just log the comment for now - console.log("Comment submitted:", { - privacyRequestId, - commentText, - }); + try { + await createComment({ + privacy_request_id: privacyRequestId, + comment_text: commentText, + comment_type: CommentType.NOTE, + }).unwrap(); - // Reset and close - setCommentText(""); - onCancel(); + // Reset and close + setCommentText(""); + onCancel(); + } catch (error) { + // eslint-disable-next-line no-console + console.error("Failed to add comment:", error); + message.error({ + content: "Failed to add comment", + duration: 5, + }); + } } }; @@ -69,6 +84,7 @@ export const CommentInput = ({ - {content && {content}} -
- {logs} +
+ + {author}: + + + {title} + {isError && " failed"} + + + {date} + + + {tag} + + {(isError || isSkipped) && ( + + View Log + + )}
-
+ ); }; + export default ActivityTimelineEntry; diff --git a/clients/admin-ui/src/features/privacy-requests/events-and-logs/ActivityTimelineList.module.scss b/clients/admin-ui/src/features/privacy-requests/events-and-logs/ActivityTimelineList.module.scss deleted file mode 100644 index 5757ecbe1ed..00000000000 --- a/clients/admin-ui/src/features/privacy-requests/events-and-logs/ActivityTimelineList.module.scss +++ /dev/null @@ -1,68 +0,0 @@ -$border-width: 1px; -$horizontal-padding: 20px; - -.itemButton { - display: block; - width: 100%; - border: $border-width solid transparent; - - border-radius: 6px; - transition: border-color 0.2s ease-in-out; - margin-bottom: 20px; - padding: 12px $horizontal-padding; - - &:hover, - &--error { - border-color: var(--fidesui-neutral-100); - } - - &:focus-visible { - border-color: var(--fidesui-neutral-700); - } - - &--error, - &--error:hover, - &--error:focus { - border-left: 8px solid var(--fidesui-error); - } -} - -.header { - cursor: pointer; - width: 100%; - - display: flex; - flex-wrap: wrap; - gap: 8px; - align-items: center; -} - -.title { - font-weight: 600; - - &--error { - color: var(--fidesui-error); - } -} - -.timestamp { - color: var(--fidesui-neutral-700); -} - -.viewLogs { - color: var(--fidesui-link); -} - -.logs { - height: 0; - overflow: hidden; - transition: height 0.2s ease-in-out; - box-sizing: border-box; - padding: 0 $horizontal-padding; - - &--open { - height: auto; - margin-top: 20px; - margin-bottom: 20px; - } -} diff --git a/clients/admin-ui/src/features/privacy-requests/events-and-logs/ActivityTimelineList.tsx b/clients/admin-ui/src/features/privacy-requests/events-and-logs/ActivityTimelineList.tsx deleted file mode 100644 index ceb7c2e431b..00000000000 --- a/clients/admin-ui/src/features/privacy-requests/events-and-logs/ActivityTimelineList.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import classNames from "classnames"; -import { AntList as List, AntTag as Tag } from "fidesui"; -import { map } from "lodash"; -import { useCallback } from "react"; - -import { formatDate } from "~/features/common/utils"; -import { ExecutionLogStatus } from "~/types/api"; - -import { ExecutionLog, PrivacyRequestResults } from "../types"; -import styles from "./ActivityTimelineList.module.scss"; - -interface ActivityTimelineItem { - // eslint-disable-next-line react/no-unused-prop-types - logs: ExecutionLog[]; - // eslint-disable-next-line react/no-unused-prop-types - key: string; -} - -interface ActivityTimelineProps { - results?: PrivacyRequestResults; - onItemClicked: ({ key, logs }: ActivityTimelineItem) => void; -} - -const ActivityTimelineList = ({ - results, - onItemClicked, -}: ActivityTimelineProps) => { - const items: ActivityTimelineItem[] = map(results, (logs, key) => ({ - logs, - key, - })); - - const renderItem = useCallback( - ({ logs, key }: ActivityTimelineItem) => { - const hasUnresolvedError = logs.some( - (log) => log.status === ExecutionLogStatus.ERROR, - ); - const hasSkippedEntry = logs.some( - (log) => log.status === ExecutionLogStatus.SKIPPED, - ); - - return ( - - ); - }, - [onItemClicked], - ); - - return ( - - {items.map(renderItem)} - - ); -}; - -export default ActivityTimelineList; diff --git a/clients/admin-ui/src/features/privacy-requests/types.ts b/clients/admin-ui/src/features/privacy-requests/types.ts index 953c63a5f64..bcb3afe475d 100644 --- a/clients/admin-ui/src/features/privacy-requests/types.ts +++ b/clients/admin-ui/src/features/privacy-requests/types.ts @@ -207,3 +207,15 @@ export interface ConfigMessagingSecretsRequest { twilio_sender_phone_number?: string; }; } + +export interface ActivityTimelineItem { + author: string; + title: string; + date: string; + tag: string; + showViewLog: boolean; + onClick: () => void; + description?: string; + isError: boolean; + isSkipped: boolean; +} From 970bf81e903efff5aa7766ba3fede8f9aec23ffd Mon Sep 17 00:00:00 2001 From: Lucano Vera Date: Tue, 20 May 2025 17:14:25 -0300 Subject: [PATCH 07/32] adjust styling --- .../events-and-logs/ActivityTimelineEntry.module.scss | 4 ---- .../events-and-logs/ActivityTimelineEntry.tsx | 1 - 2 files changed, 5 deletions(-) diff --git a/clients/admin-ui/src/features/privacy-requests/events-and-logs/ActivityTimelineEntry.module.scss b/clients/admin-ui/src/features/privacy-requests/events-and-logs/ActivityTimelineEntry.module.scss index 3fc04d65898..0ff84c17fa8 100644 --- a/clients/admin-ui/src/features/privacy-requests/events-and-logs/ActivityTimelineEntry.module.scss +++ b/clients/admin-ui/src/features/privacy-requests/events-and-logs/ActivityTimelineEntry.module.scss @@ -58,10 +58,6 @@ $horizontal-padding: 20px; &--error { color: var(--fidesui-error); } - - &--skipped { - color: var(--fidesui-warning); - } } .timestamp { diff --git a/clients/admin-ui/src/features/privacy-requests/events-and-logs/ActivityTimelineEntry.tsx b/clients/admin-ui/src/features/privacy-requests/events-and-logs/ActivityTimelineEntry.tsx index 54c9cf5d55b..6555f1496cd 100644 --- a/clients/admin-ui/src/features/privacy-requests/events-and-logs/ActivityTimelineEntry.tsx +++ b/clients/admin-ui/src/features/privacy-requests/events-and-logs/ActivityTimelineEntry.tsx @@ -28,7 +28,6 @@ const ActivityTimelineEntry = ({ item }: ActivityTimelineEntryProps) => { From 15691d7aaec44550890eaab83d655ad4c781ac7f Mon Sep 17 00:00:00 2001 From: Lucano Vera Date: Tue, 20 May 2025 17:37:56 -0300 Subject: [PATCH 08/32] Add comments --- .../events-and-logs/ActivityTimeline.tsx | 100 ++++++++++++++++-- .../ActivityTimelineEntry.module.scss | 10 +- .../events-and-logs/ActivityTimelineEntry.tsx | 6 +- .../src/features/privacy-requests/types.ts | 10 +- 4 files changed, 108 insertions(+), 18 deletions(-) diff --git a/clients/admin-ui/src/features/privacy-requests/events-and-logs/ActivityTimeline.tsx b/clients/admin-ui/src/features/privacy-requests/events-and-logs/ActivityTimeline.tsx index 4e67d63c2ab..bc7708da3a5 100644 --- a/clients/admin-ui/src/features/privacy-requests/events-and-logs/ActivityTimeline.tsx +++ b/clients/admin-ui/src/features/privacy-requests/events-and-logs/ActivityTimeline.tsx @@ -1,16 +1,25 @@ -import { AntList as List, Box, useDisclosure } from "fidesui"; +import { + AntList as List, + AntSkeleton as Skeleton, + Box, + useDisclosure, +} from "fidesui"; import { ActivityTimelineItem, + ActivityTimelineItemTypeEnum, ExecutionLog, ExecutionLogStatus, PrivacyRequestEntity, PrivacyRequestResults, } from "privacy-requests/types"; -import React, { useEffect, useState } from "react"; +import React, { useEffect, useMemo, useState } from "react"; import { formatDate } from "~/features/common/utils"; +import { useGetCommentsQuery } from "~/features/privacy-requests/comments/privacy-request-comments.slice"; +import { CommentResponse } from "~/types/api/models/CommentResponse"; import ActivityTimelineEntry from "./ActivityTimelineEntry"; +import styles from "./ActivityTimelineEntry.module.scss"; import LogDrawer from "./LogDrawer"; type ActivityTimelineProps = { @@ -27,7 +36,20 @@ const ActivityTimeline = ({ subjectRequest }: ActivityTimelineProps) => { ExecutionLogStatus.ERROR, ); - const { results } = subjectRequest; + const { results, id: privacyRequestId } = subjectRequest; + + // Fetch comments data for this privacy request + const { data: commentsData, isLoading: isCommentsLoading } = + useGetCommentsQuery({ + privacy_request_id: privacyRequestId, + size: 100, // Use a reasonable limit + }); + + // Determine if results are loading + const isResultsLoading = !results; + + // Combined loading state + const isLoading = isCommentsLoading || isResultsLoading; // Update currentLogs when results change and we have a selected key useEffect(() => { @@ -82,16 +104,73 @@ const ActivityTimeline = ({ subjectRequest }: ActivityTimelineProps) => { author: "Fides", title: key, date: formatDate(logs[0].updated_at), - tag: "Request update", + tag: ActivityTimelineItemTypeEnum.REQUEST_UPDATE, showViewLog: hasUnresolvedError || hasSkippedEntry, onClick: () => showLogs(key, logs), isError: hasUnresolvedError, isSkipped: hasSkippedEntry, + id: `request-${key}`, }; }); }; - const timelineItems = mapResultsToTimelineItems(results); + // Map comments to ActivityTimelineItem + const mapCommentsToTimelineItems = ( + comments?: CommentResponse[], + ): ActivityTimelineItem[] => { + if (!comments || comments.length === 0) { + return []; + } + + return comments.map((comment) => { + const author = + comment.user_first_name && comment.user_last_name + ? `${comment.user_first_name} ${comment.user_last_name}` + : comment.username || "Unknown"; + + return { + author, + title: comment.comment_text, + date: formatDate(comment.created_at), + tag: ActivityTimelineItemTypeEnum.COMMENT, + showViewLog: false, + description: comment.comment_text, + isError: false, + isSkipped: false, + id: `comment-${comment.id}`, + }; + }); + }; + + // Combine and sort all timeline items + const timelineItems = useMemo(() => { + const requestItems = mapResultsToTimelineItems(results); + const commentItems = mapCommentsToTimelineItems(commentsData?.items); + + // Combine both arrays + const allItems = [...requestItems, ...commentItems]; + + // Sort by date (newest first) + return allItems.sort((a, b) => { + return new Date(b.date).getTime() - new Date(a.date).getTime(); + }); + }, [results, commentsData]); + + // Render skeleton items when loading + const renderSkeletonItems = () => { + // Use fixed IDs instead of array indices + const skeletonIds = [ + "timeline-skeleton-1", + "timeline-skeleton-2", + "timeline-skeleton-3", + ]; + + return skeletonIds.map((id) => ( +
+ +
+ )); + }; return ( @@ -101,12 +180,11 @@ const ActivityTimeline = ({ subjectRequest }: ActivityTimelineProps) => { split={false} data-testid="activity-timeline-list" > - {timelineItems.map((item) => ( - - ))} + {isLoading + ? renderSkeletonItems() + : timelineItems.map((item) => ( + + ))} { const { author, title, date, tag, onClick, isError, isSkipped } = item; + const isClickable = !!onClick; + const handleClick = onClick || (() => {}); + return ( ); }; diff --git a/clients/admin-ui/src/features/privacy-requests/types.ts b/clients/admin-ui/src/features/privacy-requests/types.ts index 1e3d21caf2d..8a7ad810537 100644 --- a/clients/admin-ui/src/features/privacy-requests/types.ts +++ b/clients/admin-ui/src/features/privacy-requests/types.ts @@ -223,7 +223,7 @@ export const TimelineItemColorMap: Record< export interface ActivityTimelineItem { author: string; - title: string; + title?: string; date: string; tag: ActivityTimelineItemTypeEnum; showViewLog: boolean; From 1ea75d88912e28516e6e5ab90c8d12f6387e21c0 Mon Sep 17 00:00:00 2001 From: Lucano Vera Date: Wed, 21 May 2025 13:01:43 -0300 Subject: [PATCH 11/32] Improve comments on timeline --- .../events-and-logs/ActivityTab.tsx | 34 +++++++++---------- .../events-and-logs/ActivityTimeline.tsx | 4 +-- .../ActivityTimelineEntry.module.scss | 1 + .../events-and-logs/ActivityTimelineEntry.tsx | 28 +++++++++++---- .../events-and-logs/CommentInput.tsx | 20 +++++------ .../src/features/privacy-requests/types.ts | 2 +- 6 files changed, 51 insertions(+), 38 deletions(-) diff --git a/clients/admin-ui/src/features/privacy-requests/events-and-logs/ActivityTab.tsx b/clients/admin-ui/src/features/privacy-requests/events-and-logs/ActivityTab.tsx index 7fd00c9491e..89930c6e935 100644 --- a/clients/admin-ui/src/features/privacy-requests/events-and-logs/ActivityTab.tsx +++ b/clients/admin-ui/src/features/privacy-requests/events-and-logs/ActivityTab.tsx @@ -16,24 +16,22 @@ const ActivityTab = ({ subjectRequest }: ActivityTabProps) => {
-
- {showCommentInput ? ( - setShowCommentInput(false)} - /> - ) : ( - - - - )} -
+ {showCommentInput ? ( + setShowCommentInput(false)} + /> + ) : ( + + + + )}
); }; diff --git a/clients/admin-ui/src/features/privacy-requests/events-and-logs/ActivityTimeline.tsx b/clients/admin-ui/src/features/privacy-requests/events-and-logs/ActivityTimeline.tsx index d3099d2a38d..55185f8a681 100644 --- a/clients/admin-ui/src/features/privacy-requests/events-and-logs/ActivityTimeline.tsx +++ b/clients/admin-ui/src/features/privacy-requests/events-and-logs/ActivityTimeline.tsx @@ -104,7 +104,7 @@ const ActivityTimeline = ({ subjectRequest }: ActivityTimelineProps) => { author: "Fides", title: key, date: formatDate(logs[0].updated_at), - tag: ActivityTimelineItemTypeEnum.REQUEST_UPDATE, + type: ActivityTimelineItemTypeEnum.REQUEST_UPDATE, showViewLog: hasUnresolvedError || hasSkippedEntry, onClick: () => showLogs(key, logs), isError: hasUnresolvedError, @@ -131,7 +131,7 @@ const ActivityTimeline = ({ subjectRequest }: ActivityTimelineProps) => { return { author, date: formatDate(comment.created_at), - tag: ActivityTimelineItemTypeEnum.INTERNAL_COMMENT, + type: ActivityTimelineItemTypeEnum.INTERNAL_COMMENT, showViewLog: false, description: comment.comment_text, isError: false, diff --git a/clients/admin-ui/src/features/privacy-requests/events-and-logs/ActivityTimelineEntry.module.scss b/clients/admin-ui/src/features/privacy-requests/events-and-logs/ActivityTimelineEntry.module.scss index bee88b627df..f2f4362f3d6 100644 --- a/clients/admin-ui/src/features/privacy-requests/events-and-logs/ActivityTimelineEntry.module.scss +++ b/clients/admin-ui/src/features/privacy-requests/events-and-logs/ActivityTimelineEntry.module.scss @@ -24,6 +24,7 @@ $horizontal-padding: 20px; background: transparent; cursor: default; + &--comment, &:hover { border-color: var(--fidesui-neutral-100); } diff --git a/clients/admin-ui/src/features/privacy-requests/events-and-logs/ActivityTimelineEntry.tsx b/clients/admin-ui/src/features/privacy-requests/events-and-logs/ActivityTimelineEntry.tsx index 6bb1b6ef9e5..31ad35cb78b 100644 --- a/clients/admin-ui/src/features/privacy-requests/events-and-logs/ActivityTimelineEntry.tsx +++ b/clients/admin-ui/src/features/privacy-requests/events-and-logs/ActivityTimelineEntry.tsx @@ -2,7 +2,11 @@ import classNames from "classnames"; import { AntTag as Tag, AntTypography as Typography } from "fidesui"; import React from "react"; -import { ActivityTimelineItem, TimelineItemColorMap } from "../types"; +import { + ActivityTimelineItem, + ActivityTimelineItemTypeEnum, + TimelineItemColorMap, +} from "../types"; import styles from "./ActivityTimelineEntry.module.scss"; interface ActivityTimelineEntryProps { @@ -10,8 +14,16 @@ interface ActivityTimelineEntryProps { } const ActivityTimelineEntry = ({ item }: ActivityTimelineEntryProps) => { - const { author, title, date, tag, onClick, isError, isSkipped, description } = - item; + const { + author, + title, + date, + type, + onClick, + isError, + isSkipped, + description, + } = item; const isClickable = !!onClick; const handleClick = onClick || (() => {}); @@ -23,6 +35,8 @@ const ActivityTimelineEntry = ({ item }: ActivityTimelineEntryProps) => { className={classNames(styles.itemButton, { [styles["itemButton--error"]]: isError, [styles["itemButton--clickable"]]: isClickable, + [styles["itemButton--comment"]]: + type === ActivityTimelineItemTypeEnum.INTERNAL_COMMENT, })} data-testid="activity-timeline-item" > @@ -49,10 +63,10 @@ const ActivityTimelineEntry = ({ item }: ActivityTimelineEntryProps) => {
- {tag} + {type} {(isError || isSkipped) && ( { {description && (
- {description} + + {description} +
)} diff --git a/clients/admin-ui/src/features/privacy-requests/events-and-logs/CommentInput.tsx b/clients/admin-ui/src/features/privacy-requests/events-and-logs/CommentInput.tsx index 8a89c368d01..33f39eb53ee 100644 --- a/clients/admin-ui/src/features/privacy-requests/events-and-logs/CommentInput.tsx +++ b/clients/admin-ui/src/features/privacy-requests/events-and-logs/CommentInput.tsx @@ -60,16 +60,14 @@ export const CommentInput = ({ key: "internal", label: "Internal comment", children: ( -
- setCommentText(e.target.value)} - rows={3} - className="mb-4 w-full" - data-testid="comment-input" - /> -
+ setCommentText(e.target.value)} + rows={3} + className="mb-3 h-[150px] w-full !resize-none" + data-testid="comment-input" + /> ), }, ]; @@ -78,7 +76,7 @@ export const CommentInput = ({
- + diff --git a/clients/admin-ui/src/features/privacy-requests/types.ts b/clients/admin-ui/src/features/privacy-requests/types.ts index 8a7ad810537..88354d74e52 100644 --- a/clients/admin-ui/src/features/privacy-requests/types.ts +++ b/clients/admin-ui/src/features/privacy-requests/types.ts @@ -225,7 +225,7 @@ export interface ActivityTimelineItem { author: string; title?: string; date: string; - tag: ActivityTimelineItemTypeEnum; + type: ActivityTimelineItemTypeEnum; showViewLog: boolean; onClick?: () => void; description?: string; From 2dd88755659eaeffb85c9172f5c6f4d0ecc2520c Mon Sep 17 00:00:00 2001 From: Lucano Vera Date: Thu, 22 May 2025 10:59:03 -0300 Subject: [PATCH 12/32] activity timeline, sort by oldest first --- .../privacy-requests/events-and-logs/ActivityTimeline.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/clients/admin-ui/src/features/privacy-requests/events-and-logs/ActivityTimeline.tsx b/clients/admin-ui/src/features/privacy-requests/events-and-logs/ActivityTimeline.tsx index 55185f8a681..0ddb9a12fe8 100644 --- a/clients/admin-ui/src/features/privacy-requests/events-and-logs/ActivityTimeline.tsx +++ b/clients/admin-ui/src/features/privacy-requests/events-and-logs/ActivityTimeline.tsx @@ -149,9 +149,9 @@ const ActivityTimeline = ({ subjectRequest }: ActivityTimelineProps) => { // Combine both arrays const allItems = [...requestItems, ...commentItems]; - // Sort by date (newest first) + // Sort by date (oldest first) return allItems.sort((a, b) => { - return new Date(b.date).getTime() - new Date(a.date).getTime(); + return new Date(a.date).getTime() - new Date(b.date).getTime(); }); }, [results, commentsData]); From 410420075dca3a9d0c70d2616fd0b5a9feca4d7c Mon Sep 17 00:00:00 2001 From: Lucano Vera Date: Thu, 22 May 2025 11:03:56 -0300 Subject: [PATCH 13/32] Update clipbord icon to Carbon --- clients/admin-ui/src/features/common/ClipboardButton.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/clients/admin-ui/src/features/common/ClipboardButton.tsx b/clients/admin-ui/src/features/common/ClipboardButton.tsx index 54920fa3a32..f4a52992a41 100644 --- a/clients/admin-ui/src/features/common/ClipboardButton.tsx +++ b/clients/admin-ui/src/features/common/ClipboardButton.tsx @@ -2,12 +2,11 @@ import { AntButton as Button, AntButtonProps as ButtonProps, AntTooltip as Tooltip, + Icons, useClipboard, } from "fidesui"; import React, { useState } from "react"; -import { CopyIcon } from "./Icon"; - enum TooltipText { COPY = "Copy", COPIED = "Copied!", @@ -51,7 +50,7 @@ const ClipboardButton = ({ copyText, ...props }: ClipboardButtonProps) => { }} > + ) : ( +
{content}
); }; From db4010697e778eaf6a229ede9bbdf7713272f7ac Mon Sep 17 00:00:00 2001 From: Lucano Vera Date: Thu, 22 May 2025 13:04:09 -0300 Subject: [PATCH 17/32] add cypress tests --- .../cypress/e2e/privacy-requests.cy.ts | 90 +++++++++++++++++++ .../comments/comment-created.json | 11 +++ .../comments/comments-list.json | 18 ++++ .../comments/empty-comments.json | 6 ++ 4 files changed, 125 insertions(+) create mode 100644 clients/admin-ui/cypress/fixtures/privacy-requests/comments/comment-created.json create mode 100644 clients/admin-ui/cypress/fixtures/privacy-requests/comments/comments-list.json create mode 100644 clients/admin-ui/cypress/fixtures/privacy-requests/comments/empty-comments.json diff --git a/clients/admin-ui/cypress/e2e/privacy-requests.cy.ts b/clients/admin-ui/cypress/e2e/privacy-requests.cy.ts index c0d90e5ae7e..96d005e55b5 100644 --- a/clients/admin-ui/cypress/e2e/privacy-requests.cy.ts +++ b/clients/admin-ui/cypress/e2e/privacy-requests.cy.ts @@ -512,4 +512,94 @@ describe("Privacy Requests", () => { ); }); }); + + /** + * Tests for privacy request comments functionality + */ + describe("Request Comments", () => { + beforeEach(() => { + cy.assumeRole(RoleRegistryEnum.OWNER); + + cy.intercept("GET", "/api/v1/plus/privacy-request/*/comment*", { + statusCode: 200, + fixture: "privacy-requests/comments/comments-list.json", + }).as("getComments"); + + cy.intercept("POST", "/api/v1/plus/privacy-request/*/comment", { + statusCode: 200, + fixture: "privacy-requests/comments/comment-created.json", + }).as("createComment"); + + cy.intercept("GET", "/api/v1/privacy-request*", { + fixture: "privacy-requests/with-logs.json", + }).as("getPrivacyRequestWithLogs"); + + cy.visit("/privacy-requests/pri_96bb91d3-cdb9-46c3-9546-0c276eb05a5c"); + cy.wait("@getPrivacyRequestWithLogs"); + cy.wait("@getComments"); + }); + + it("displays existing comments in the activity timeline", () => { + cy.getByTestId("activity-timeline-item") + .contains("This is a test comment") + .should("exist"); + cy.contains("Test User:").should("exist"); + cy.getByTestId("activity-timeline-type") + .contains("Internal comment") + .should("exist"); + }); + + it("allows creating a new comment", () => { + cy.contains("Add comment").click(); + cy.getByTestId("comment-input").should("exist"); + cy.getByTestId("comment-input").type("New comment from test"); + cy.getByTestId("submit-comment-button").click(); + + // Check that the request was made with the correct form data + cy.wait("@createComment").then((interception) => { + const requestBody = interception.request.body; + expect(requestBody).to.include('name="comment_text"'); + expect(requestBody).to.include("New comment from test"); + expect(requestBody).to.include('name="comment_type"'); + expect(requestBody).to.include("note"); + }); + }); + + it("allows canceling comment creation", () => { + cy.contains("Add comment").click(); + cy.getByTestId("comment-input").should("exist"); + cy.getByTestId("comment-input").type("Comment that will be canceled"); + cy.getByTestId("cancel-comment-button").click(); + cy.getByTestId("comment-input").should("not.exist"); + }); + + it("shows loading state while fetching comments", () => { + cy.intercept("GET", "/api/v1/plus/privacy-request/*/comment*", { + statusCode: 200, + fixture: "privacy-requests/comments/empty-comments.json", + delay: 1000, + }).as("getCommentsDelayed"); + + cy.visit("/privacy-requests/pri_96bb91d3-cdb9-46c3-9546-0c276eb05a5c"); + + // Check for skeleton loading state before comments load + cy.get(".ant-skeleton").should("exist"); + + cy.wait("@getCommentsDelayed"); + + // Verify skeletons are gone after loading + cy.get(".ant-skeleton").should("not.exist"); + }); + + it.only("restricts comment functionality based on user role", () => { + cy.contains("Add comment").should("exist"); + + cy.assumeRole(RoleRegistryEnum.VIEWER); + cy.reload(); + cy.wait("@getPrivacyRequestWithLogs"); + cy.wait("@getComments"); + + cy.contains("Add comment").should("not.exist"); + }); + }); }); diff --git a/clients/admin-ui/cypress/fixtures/privacy-requests/comments/comment-created.json b/clients/admin-ui/cypress/fixtures/privacy-requests/comments/comment-created.json new file mode 100644 index 00000000000..9e4be199a23 --- /dev/null +++ b/clients/admin-ui/cypress/fixtures/privacy-requests/comments/comment-created.json @@ -0,0 +1,11 @@ +{ + "id": "comment_456", + "privacy_request_id": "pri_123", + "comment_text": "New comment from test", + "comment_type": "NOTE", + "created_at": "2023-01-02T12:00:00Z", + "user_id": "usr_123", + "username": "testuser", + "user_first_name": "Test", + "user_last_name": "User" +} diff --git a/clients/admin-ui/cypress/fixtures/privacy-requests/comments/comments-list.json b/clients/admin-ui/cypress/fixtures/privacy-requests/comments/comments-list.json new file mode 100644 index 00000000000..ec0356b8301 --- /dev/null +++ b/clients/admin-ui/cypress/fixtures/privacy-requests/comments/comments-list.json @@ -0,0 +1,18 @@ +{ + "items": [ + { + "id": "comment_123", + "privacy_request_id": "pri_123", + "comment_text": "This is a test comment", + "comment_type": "NOTE", + "created_at": "2023-01-01T12:00:00Z", + "user_id": "usr_123", + "username": "testuser", + "user_first_name": "Test", + "user_last_name": "User" + } + ], + "total": 1, + "page": 1, + "size": 10 +} diff --git a/clients/admin-ui/cypress/fixtures/privacy-requests/comments/empty-comments.json b/clients/admin-ui/cypress/fixtures/privacy-requests/comments/empty-comments.json new file mode 100644 index 00000000000..871c13aedaf --- /dev/null +++ b/clients/admin-ui/cypress/fixtures/privacy-requests/comments/empty-comments.json @@ -0,0 +1,6 @@ +{ + "items": [], + "total": 0, + "page": 1, + "size": 10 +} From 588749b83d2523eb5bae652d7bea0a7538209006 Mon Sep 17 00:00:00 2001 From: Lucano Vera Date: Thu, 22 May 2025 13:05:21 -0300 Subject: [PATCH 18/32] add comment registry --- clients/admin-ui/src/types/api/models/ScopeRegistryEnum.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/clients/admin-ui/src/types/api/models/ScopeRegistryEnum.ts b/clients/admin-ui/src/types/api/models/ScopeRegistryEnum.ts index ef3a4959ebe..29e1d678511 100644 --- a/clients/admin-ui/src/types/api/models/ScopeRegistryEnum.ts +++ b/clients/admin-ui/src/types/api/models/ScopeRegistryEnum.ts @@ -21,6 +21,8 @@ export enum ScopeRegistryEnum { CLIENT_DELETE = "client:delete", CLIENT_READ = "client:read", CLIENT_UPDATE = "client:update", + COMMENT_CREATE = "comment:create", + COMMENT_READ = "comment:read", CONFIG_READ = "config:read", CONFIG_UPDATE = "config:update", CONNECTION_AUTHORIZE = "connection:authorize", From 1b4329ce13775d506df1196188f13f6b77dbc693 Mon Sep 17 00:00:00 2001 From: Lucano Vera Date: Thu, 22 May 2025 13:14:53 -0300 Subject: [PATCH 19/32] Fix comments cypress tests --- .../cypress/e2e/privacy-requests.cy.ts | 21 +++++++++++----- .../fixtures/scopes/roles-to-scopes.json | 8 +++++++ .../events-and-logs/ActivityTab.tsx | 24 ++++++++++++------- 3 files changed, 38 insertions(+), 15 deletions(-) diff --git a/clients/admin-ui/cypress/e2e/privacy-requests.cy.ts b/clients/admin-ui/cypress/e2e/privacy-requests.cy.ts index 96d005e55b5..a6c58ec63d5 100644 --- a/clients/admin-ui/cypress/e2e/privacy-requests.cy.ts +++ b/clients/admin-ui/cypress/e2e/privacy-requests.cy.ts @@ -550,7 +550,7 @@ describe("Privacy Requests", () => { }); it("allows creating a new comment", () => { - cy.contains("Add comment").click(); + cy.getByTestId("add-comment-button").click(); cy.getByTestId("comment-input").should("exist"); cy.getByTestId("comment-input").type("New comment from test"); cy.getByTestId("submit-comment-button").click(); @@ -566,7 +566,7 @@ describe("Privacy Requests", () => { }); it("allows canceling comment creation", () => { - cy.contains("Add comment").click(); + cy.getByTestId("add-comment-button").click(); cy.getByTestId("comment-input").should("exist"); cy.getByTestId("comment-input").type("Comment that will be canceled"); cy.getByTestId("cancel-comment-button").click(); @@ -591,15 +591,24 @@ describe("Privacy Requests", () => { cy.get(".ant-skeleton").should("not.exist"); }); - it.only("restricts comment functionality based on user role", () => { - cy.contains("Add comment").should("exist"); - + it("restricts comment functionality based on user permissions", () => { + // First check with a viewer role (has comment:read but not comment:create) cy.assumeRole(RoleRegistryEnum.VIEWER); cy.reload(); cy.wait("@getPrivacyRequestWithLogs"); cy.wait("@getComments"); - cy.contains("Add comment").should("not.exist"); + // Button should be hidden for users without COMMENT_CREATE scope + cy.getByTestId("add-comment-button").should("not.exist"); + + // Then check with an owner role (has comment:create scope) + cy.assumeRole(RoleRegistryEnum.OWNER); + cy.reload(); + cy.wait("@getPrivacyRequestWithLogs"); + cy.wait("@getComments"); + + // Button should be visible for users with COMMENT_CREATE scope + cy.getByTestId("add-comment-button").should("exist"); }); }); }); diff --git a/clients/admin-ui/cypress/fixtures/scopes/roles-to-scopes.json b/clients/admin-ui/cypress/fixtures/scopes/roles-to-scopes.json index 7bcb3d301b8..57abd508030 100644 --- a/clients/admin-ui/cypress/fixtures/scopes/roles-to-scopes.json +++ b/clients/admin-ui/cypress/fixtures/scopes/roles-to-scopes.json @@ -18,6 +18,8 @@ "client:delete", "client:read", "client:update", + "comment:create", + "comment:read", "config:read", "config:update", "connection:authorize", @@ -157,6 +159,7 @@ "classify_instance:read", "cli-objects:read", "client:read", + "comment:read", "connection:read", "connection_type:read", "consent:read", @@ -201,6 +204,7 @@ "classify_instance:read", "cli-objects:read", "client:read", + "comment:read", "connection:read", "connection_type:read", "consent:read", @@ -237,6 +241,8 @@ "webhook:read" ], "approver": [ + "comment:create", + "comment:read", "privacy-request:read", "privacy-request:resume", "privacy-request:review", @@ -259,6 +265,8 @@ "client:delete", "client:read", "client:update", + "comment:create", + "comment:read", "config:read", "config:update", "connection:authorize", diff --git a/clients/admin-ui/src/features/privacy-requests/events-and-logs/ActivityTab.tsx b/clients/admin-ui/src/features/privacy-requests/events-and-logs/ActivityTab.tsx index 89930c6e935..e2590008436 100644 --- a/clients/admin-ui/src/features/privacy-requests/events-and-logs/ActivityTab.tsx +++ b/clients/admin-ui/src/features/privacy-requests/events-and-logs/ActivityTab.tsx @@ -1,6 +1,9 @@ import { AntButton as Button, AntFlex as Flex } from "fidesui"; import { useState } from "react"; +import Restrict from "~/features/common/Restrict"; +import { ScopeRegistryEnum } from "~/types/api"; + import { PrivacyRequestEntity } from "../types"; import ActivityTimeline from "./ActivityTimeline"; import { CommentInput } from "./CommentInput"; @@ -22,15 +25,18 @@ const ActivityTab = ({ subjectRequest }: ActivityTabProps) => { onCancel={() => setShowCommentInput(false)} /> ) : ( - - - + + + + + )}
); From 822e441e07ad41937bb533f5185f1000ba10022d Mon Sep 17 00:00:00 2001 From: Lucano Vera Date: Thu, 22 May 2025 13:20:34 -0300 Subject: [PATCH 20/32] add test --- .../cypress/e2e/privacy-requests.cy.ts | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/clients/admin-ui/cypress/e2e/privacy-requests.cy.ts b/clients/admin-ui/cypress/e2e/privacy-requests.cy.ts index a6c58ec63d5..78e43f17320 100644 --- a/clients/admin-ui/cypress/e2e/privacy-requests.cy.ts +++ b/clients/admin-ui/cypress/e2e/privacy-requests.cy.ts @@ -610,5 +610,27 @@ describe("Privacy Requests", () => { // Button should be visible for users with COMMENT_CREATE scope cy.getByTestId("add-comment-button").should("exist"); }); + + it("handles 404 errors from comments API gracefully", () => { + // Intercept comments API and return a 404 error + cy.intercept("GET", "/api/v1/plus/privacy-request/*/comment*", { + statusCode: 404, + body: { + detail: "Not found", + }, + }).as("commentsNotFound"); + + // Load the page + cy.visit("/privacy-requests/pri_96bb91d3-cdb9-46c3-9546-0c276eb05a5c"); + cy.wait("@getPrivacyRequestWithLogs"); + cy.wait("@commentsNotFound"); + + // Verify the timeline still shows request updates even if comments failed to load + cy.getByTestId("activity-timeline-list").should("exist"); + cy.getByTestId("activity-timeline-item").should("exist"); + + // The Add comment button should still be available + cy.getByTestId("add-comment-button").should("exist"); + }); }); }); From 9018ea290808ab7929468dd01ba269cafe2acfa6 Mon Sep 17 00:00:00 2001 From: Lucano Vera Date: Thu, 22 May 2025 13:57:20 -0300 Subject: [PATCH 21/32] update changelog --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 478bfd0a93d..a6fd0f82b84 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,7 +21,8 @@ Changes can also be flagged with a GitHub label for tracking purposes. The URL o ## [Unreleased](https://github.com/ethyca/fides/compare/2.62.0...main) - +### Added +- Added ability to add internal comments to privacy requests [#6165](https://github.com/ethyca/fides/pull/6165) ## [2.62.0](https://github.com/ethyca/fides/compare/2.61.1...2.62.0) From bce26e95de99eecfd753054ba2538e0d7787868b Mon Sep 17 00:00:00 2001 From: Lucano Vera Date: Thu, 22 May 2025 15:22:50 -0300 Subject: [PATCH 22/32] simplify skeleton code --- .../cypress/e2e/privacy-requests.cy.ts | 6 +++--- .../events-and-logs/ActivityTimeline.tsx | 19 +++++-------------- 2 files changed, 8 insertions(+), 17 deletions(-) diff --git a/clients/admin-ui/cypress/e2e/privacy-requests.cy.ts b/clients/admin-ui/cypress/e2e/privacy-requests.cy.ts index 78e43f17320..1d521d0c1cf 100644 --- a/clients/admin-ui/cypress/e2e/privacy-requests.cy.ts +++ b/clients/admin-ui/cypress/e2e/privacy-requests.cy.ts @@ -573,7 +573,7 @@ describe("Privacy Requests", () => { cy.getByTestId("comment-input").should("not.exist"); }); - it("shows loading state while fetching comments", () => { + it.only("shows loading state while fetching comments", () => { cy.intercept("GET", "/api/v1/plus/privacy-request/*/comment*", { statusCode: 200, fixture: "privacy-requests/comments/empty-comments.json", @@ -583,12 +583,12 @@ describe("Privacy Requests", () => { cy.visit("/privacy-requests/pri_96bb91d3-cdb9-46c3-9546-0c276eb05a5c"); // Check for skeleton loading state before comments load - cy.get(".ant-skeleton").should("exist"); + cy.getByTestId("timeline-skeleton").should("exist"); cy.wait("@getCommentsDelayed"); // Verify skeletons are gone after loading - cy.get(".ant-skeleton").should("not.exist"); + cy.getByTestId("timeline-skeleton").should("not.exist"); }); it("restricts comment functionality based on user permissions", () => { diff --git a/clients/admin-ui/src/features/privacy-requests/events-and-logs/ActivityTimeline.tsx b/clients/admin-ui/src/features/privacy-requests/events-and-logs/ActivityTimeline.tsx index 85a31128d22..858d44d2e03 100644 --- a/clients/admin-ui/src/features/privacy-requests/events-and-logs/ActivityTimeline.tsx +++ b/clients/admin-ui/src/features/privacy-requests/events-and-logs/ActivityTimeline.tsx @@ -156,20 +156,11 @@ const ActivityTimeline = ({ subjectRequest }: ActivityTimelineProps) => { }, [results, commentsData]); // Render skeleton items when loading - const renderSkeletonItems = () => { - // Use fixed IDs instead of array indices - const skeletonIds = [ - "timeline-skeleton-1", - "timeline-skeleton-2", - "timeline-skeleton-3", - ]; - - return skeletonIds.map((id) => ( -
- -
- )); - }; + const renderSkeletonItems = () => ( +
+ +
+ ); return ( From f0012583a47f07dffb7b9c095d449a514ccdfdc7 Mon Sep 17 00:00:00 2001 From: Lucano Vera Date: Thu, 22 May 2025 15:30:40 -0300 Subject: [PATCH 23/32] refactor activity timeline to simplify code --- .../events-and-logs/ActivityTimeline.tsx | 103 ++++-------------- .../events-and-logs/hooks/index.ts | 2 + .../hooks/usePrivacyRequestComments.ts | 44 ++++++++ .../hooks/usePrivacyRequestEventLogs.ts | 44 ++++++++ 4 files changed, 114 insertions(+), 79 deletions(-) create mode 100644 clients/admin-ui/src/features/privacy-requests/events-and-logs/hooks/index.ts create mode 100644 clients/admin-ui/src/features/privacy-requests/events-and-logs/hooks/usePrivacyRequestComments.ts create mode 100644 clients/admin-ui/src/features/privacy-requests/events-and-logs/hooks/usePrivacyRequestEventLogs.ts diff --git a/clients/admin-ui/src/features/privacy-requests/events-and-logs/ActivityTimeline.tsx b/clients/admin-ui/src/features/privacy-requests/events-and-logs/ActivityTimeline.tsx index 858d44d2e03..3ffa8023b0d 100644 --- a/clients/admin-ui/src/features/privacy-requests/events-and-logs/ActivityTimeline.tsx +++ b/clients/admin-ui/src/features/privacy-requests/events-and-logs/ActivityTimeline.tsx @@ -4,22 +4,17 @@ import { Box, useDisclosure, } from "fidesui"; +import React, { useEffect, useMemo, useState } from "react"; + import { - ActivityTimelineItem, - ActivityTimelineItemTypeEnum, ExecutionLog, ExecutionLogStatus, PrivacyRequestEntity, - PrivacyRequestResults, -} from "privacy-requests/types"; -import React, { useEffect, useMemo, useState } from "react"; - -import { formatDate } from "~/features/common/utils"; -import { useGetCommentsQuery } from "~/features/privacy-requests/comments/privacy-request-comments.slice"; -import { CommentResponse } from "~/types/api/models/CommentResponse"; +} from "~/features/privacy-requests/types"; import ActivityTimelineEntry from "./ActivityTimelineEntry"; import styles from "./ActivityTimelineEntry.module.scss"; +import { usePrivacyRequestComments, usePrivacyRequestEventLogs } from "./hooks"; import LogDrawer from "./LogDrawer"; type ActivityTimelineProps = { @@ -38,15 +33,11 @@ const ActivityTimeline = ({ subjectRequest }: ActivityTimelineProps) => { const { results, id: privacyRequestId } = subjectRequest; - // Fetch comments data for this privacy request - const { data: commentsData, isLoading: isCommentsLoading } = - useGetCommentsQuery({ - privacy_request_id: privacyRequestId, - size: 100, // Use a reasonable limit - }); - - // Determine if results are loading - const isResultsLoading = !results; + // Use our custom hooks + const { commentItems, isLoading: isCommentsLoading } = + usePrivacyRequestComments(privacyRequestId); + const { eventItems, isLoading: isResultsLoading } = + usePrivacyRequestEventLogs(results); // Combined loading state const isLoading = isCommentsLoading || isResultsLoading; @@ -84,76 +75,30 @@ const ActivityTimeline = ({ subjectRequest }: ActivityTimelineProps) => { onOpen(); }; - // Map from source events to ActivityTimelineItems - const mapResultsToTimelineItems = ( - resultsData?: PrivacyRequestResults, - ): ActivityTimelineItem[] => { - if (!resultsData) { - return []; - } - - return Object.entries(resultsData).map(([key, logs]) => { - const hasUnresolvedError = logs.some( - (log) => log.status === ExecutionLogStatus.ERROR, - ); - const hasSkippedEntry = logs.some( - (log) => log.status === ExecutionLogStatus.SKIPPED, - ); - - return { - author: "Fides", - title: key, - date: formatDate(logs[0].updated_at), - type: ActivityTimelineItemTypeEnum.REQUEST_UPDATE, - showViewLog: hasUnresolvedError || hasSkippedEntry, - onClick: () => showLogs(key, logs), - isError: hasUnresolvedError, - isSkipped: hasSkippedEntry, - id: `request-${key}`, - }; - }); - }; - - // Map comments to ActivityTimelineItem - const mapCommentsToTimelineItems = ( - comments?: CommentResponse[], - ): ActivityTimelineItem[] => { - if (!comments || comments.length === 0) { - return []; - } - - return comments.map((comment) => { - const author = - comment.user_first_name && comment.user_last_name - ? `${comment.user_first_name} ${comment.user_last_name}` - : comment.username || "Unknown"; - - return { - author, - date: formatDate(comment.created_at), - type: ActivityTimelineItemTypeEnum.INTERNAL_COMMENT, - showViewLog: false, - description: comment.comment_text, - isError: false, - isSkipped: false, - id: `comment-${comment.id}`, - }; - }); - }; - // Combine and sort all timeline items const timelineItems = useMemo(() => { - const requestItems = mapResultsToTimelineItems(results); - const commentItems = mapCommentsToTimelineItems(commentsData?.items); + // Override the onClick handler for event items + const eventItemsWithClickHandler = eventItems.map((item) => { + if (item.type === "Request update" && item.title && results) { + const key = item.title; + if (results[key]) { + return { + ...item, + onClick: () => showLogs(key, results[key]), + }; + } + } + return item; + }); // Combine both arrays - const allItems = [...requestItems, ...commentItems]; + const allItems = [...eventItemsWithClickHandler, ...commentItems]; // Sort by date (oldest first) return allItems.sort((a, b) => { return new Date(a.date).getTime() - new Date(b.date).getTime(); }); - }, [results, commentsData]); + }, [eventItems, commentItems, results]); // Render skeleton items when loading const renderSkeletonItems = () => ( diff --git a/clients/admin-ui/src/features/privacy-requests/events-and-logs/hooks/index.ts b/clients/admin-ui/src/features/privacy-requests/events-and-logs/hooks/index.ts new file mode 100644 index 00000000000..c878eedfdaa --- /dev/null +++ b/clients/admin-ui/src/features/privacy-requests/events-and-logs/hooks/index.ts @@ -0,0 +1,2 @@ +export * from "./usePrivacyRequestComments"; +export * from "./usePrivacyRequestEventLogs"; diff --git a/clients/admin-ui/src/features/privacy-requests/events-and-logs/hooks/usePrivacyRequestComments.ts b/clients/admin-ui/src/features/privacy-requests/events-and-logs/hooks/usePrivacyRequestComments.ts new file mode 100644 index 00000000000..4099f88da26 --- /dev/null +++ b/clients/admin-ui/src/features/privacy-requests/events-and-logs/hooks/usePrivacyRequestComments.ts @@ -0,0 +1,44 @@ +import { formatDate } from "~/features/common/utils"; +import { useGetCommentsQuery } from "~/features/privacy-requests/comments/privacy-request-comments.slice"; +import { + ActivityTimelineItem, + ActivityTimelineItemTypeEnum, +} from "~/features/privacy-requests/types"; +import { CommentResponse } from "~/types/api/models/CommentResponse"; + +/** + * Hook for fetching and processing privacy request comments + */ +export const usePrivacyRequestComments = (privacyRequestId: string) => { + // Fetch comments data for this privacy request + const { data: commentsData, isLoading } = useGetCommentsQuery({ + privacy_request_id: privacyRequestId, + size: 100, // Use a reasonable limit + }); + + // Map comments to ActivityTimelineItem + const commentItems: ActivityTimelineItem[] = !commentsData?.items + ? [] + : commentsData.items.map((comment: CommentResponse) => { + const author = + comment.user_first_name && comment.user_last_name + ? `${comment.user_first_name} ${comment.user_last_name}` + : comment.username || "Unknown"; + + return { + author, + date: formatDate(comment.created_at), + type: ActivityTimelineItemTypeEnum.INTERNAL_COMMENT, + showViewLog: false, + description: comment.comment_text, + isError: false, + isSkipped: false, + id: `comment-${comment.id}`, + }; + }); + + return { + commentItems, + isLoading, + }; +}; diff --git a/clients/admin-ui/src/features/privacy-requests/events-and-logs/hooks/usePrivacyRequestEventLogs.ts b/clients/admin-ui/src/features/privacy-requests/events-and-logs/hooks/usePrivacyRequestEventLogs.ts new file mode 100644 index 00000000000..9634a931474 --- /dev/null +++ b/clients/admin-ui/src/features/privacy-requests/events-and-logs/hooks/usePrivacyRequestEventLogs.ts @@ -0,0 +1,44 @@ +import { formatDate } from "~/features/common/utils"; +import { + ActivityTimelineItem, + ActivityTimelineItemTypeEnum, + ExecutionLogStatus, + PrivacyRequestResults, +} from "~/features/privacy-requests/types"; + +/** + * Hook for processing privacy request event logs + */ +export const usePrivacyRequestEventLogs = (results?: PrivacyRequestResults) => { + // Determine if results are loading + const isLoading = !results; + + // Map from source events to ActivityTimelineItems + const eventItems: ActivityTimelineItem[] = !results + ? [] + : Object.entries(results).map(([key, logs]) => { + const hasUnresolvedError = logs.some( + (log) => log.status === ExecutionLogStatus.ERROR, + ); + const hasSkippedEntry = logs.some( + (log) => log.status === ExecutionLogStatus.SKIPPED, + ); + + return { + author: "Fides", + title: key, + date: formatDate(logs[0].updated_at), + type: ActivityTimelineItemTypeEnum.REQUEST_UPDATE, + showViewLog: hasUnresolvedError || hasSkippedEntry, + onClick: () => {}, // This will be overridden in the component + isError: hasUnresolvedError, + isSkipped: hasSkippedEntry, + id: `request-${key}`, + }; + }); + + return { + eventItems, + isLoading, + }; +}; From 0e8a24ad493c3c321520117ec1e9497f4114de14 Mon Sep 17 00:00:00 2001 From: Lucano Vera Date: Thu, 22 May 2025 15:37:49 -0300 Subject: [PATCH 24/32] Make date property be a Date to improve code --- .../privacy-requests/events-and-logs/ActivityTimeline.tsx | 2 +- .../events-and-logs/ActivityTimelineEntry.tsx | 7 ++++++- .../events-and-logs/hooks/usePrivacyRequestComments.ts | 3 +-- .../events-and-logs/hooks/usePrivacyRequestEventLogs.ts | 3 +-- clients/admin-ui/src/features/privacy-requests/types.ts | 2 +- 5 files changed, 10 insertions(+), 7 deletions(-) diff --git a/clients/admin-ui/src/features/privacy-requests/events-and-logs/ActivityTimeline.tsx b/clients/admin-ui/src/features/privacy-requests/events-and-logs/ActivityTimeline.tsx index 3ffa8023b0d..7de5bd16865 100644 --- a/clients/admin-ui/src/features/privacy-requests/events-and-logs/ActivityTimeline.tsx +++ b/clients/admin-ui/src/features/privacy-requests/events-and-logs/ActivityTimeline.tsx @@ -96,7 +96,7 @@ const ActivityTimeline = ({ subjectRequest }: ActivityTimelineProps) => { // Sort by date (oldest first) return allItems.sort((a, b) => { - return new Date(a.date).getTime() - new Date(b.date).getTime(); + return a.date.getTime() - b.date.getTime(); }); }, [eventItems, commentItems, results]); diff --git a/clients/admin-ui/src/features/privacy-requests/events-and-logs/ActivityTimelineEntry.tsx b/clients/admin-ui/src/features/privacy-requests/events-and-logs/ActivityTimelineEntry.tsx index c95b12650fb..2e5b976bd20 100644 --- a/clients/admin-ui/src/features/privacy-requests/events-and-logs/ActivityTimelineEntry.tsx +++ b/clients/admin-ui/src/features/privacy-requests/events-and-logs/ActivityTimelineEntry.tsx @@ -2,6 +2,8 @@ import classNames from "classnames"; import { AntTag as Tag, AntTypography as Typography } from "fidesui"; import React from "react"; +import { formatDate } from "~/features/common/utils"; + import { ActivityTimelineItem, ActivityTimelineItemTypeEnum, @@ -25,6 +27,9 @@ const ActivityTimelineEntry = ({ item }: ActivityTimelineEntryProps) => { description, } = item; + // Format the date for display + const formattedDate = formatDate(date); + const isClickable = !!onClick; const content = ( @@ -48,7 +53,7 @@ const ActivityTimelineEntry = ({ item }: ActivityTimelineEntryProps) => { className={styles.timestamp} data-testid="activity-timeline-timestamp" > - {date} + {formattedDate}
{ return { author, - date: formatDate(comment.created_at), + date: new Date(comment.created_at), type: ActivityTimelineItemTypeEnum.INTERNAL_COMMENT, showViewLog: false, description: comment.comment_text, diff --git a/clients/admin-ui/src/features/privacy-requests/events-and-logs/hooks/usePrivacyRequestEventLogs.ts b/clients/admin-ui/src/features/privacy-requests/events-and-logs/hooks/usePrivacyRequestEventLogs.ts index 9634a931474..974a606a411 100644 --- a/clients/admin-ui/src/features/privacy-requests/events-and-logs/hooks/usePrivacyRequestEventLogs.ts +++ b/clients/admin-ui/src/features/privacy-requests/events-and-logs/hooks/usePrivacyRequestEventLogs.ts @@ -1,4 +1,3 @@ -import { formatDate } from "~/features/common/utils"; import { ActivityTimelineItem, ActivityTimelineItemTypeEnum, @@ -27,7 +26,7 @@ export const usePrivacyRequestEventLogs = (results?: PrivacyRequestResults) => { return { author: "Fides", title: key, - date: formatDate(logs[0].updated_at), + date: new Date(logs[0].updated_at), type: ActivityTimelineItemTypeEnum.REQUEST_UPDATE, showViewLog: hasUnresolvedError || hasSkippedEntry, onClick: () => {}, // This will be overridden in the component diff --git a/clients/admin-ui/src/features/privacy-requests/types.ts b/clients/admin-ui/src/features/privacy-requests/types.ts index 88354d74e52..11168143e74 100644 --- a/clients/admin-ui/src/features/privacy-requests/types.ts +++ b/clients/admin-ui/src/features/privacy-requests/types.ts @@ -224,7 +224,7 @@ export const TimelineItemColorMap: Record< export interface ActivityTimelineItem { author: string; title?: string; - date: string; + date: Date; type: ActivityTimelineItemTypeEnum; showViewLog: boolean; onClick?: () => void; From f18462f99e50fa244aced9f4e5303378fd5cd5ea Mon Sep 17 00:00:00 2001 From: Lucano Vera Date: Thu, 22 May 2025 15:38:33 -0300 Subject: [PATCH 25/32] Fix hour format showing in 12hs to use 24hs format --- clients/admin-ui/src/features/common/utils.ts | 2 +- clients/admin-ui/src/features/privacy-requests/types.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/clients/admin-ui/src/features/common/utils.ts b/clients/admin-ui/src/features/common/utils.ts index 4b88bc02ace..e528e5c5645 100644 --- a/clients/admin-ui/src/features/common/utils.ts +++ b/clients/admin-ui/src/features/common/utils.ts @@ -32,7 +32,7 @@ export const debounce = (fn: (props?: any) => void, ms = 0) => { }; export const formatDate = (value: string | number | Date): string => - format(new Date(value), "MMMM d, y, KK:mm:ss z"); + format(new Date(value), "MMMM d, y, kk:mm:ss z"); export const utf8ToB64 = (str: string): string => window.btoa(unescape(encodeURIComponent(str))); diff --git a/clients/admin-ui/src/features/privacy-requests/types.ts b/clients/admin-ui/src/features/privacy-requests/types.ts index 11168143e74..88354d74e52 100644 --- a/clients/admin-ui/src/features/privacy-requests/types.ts +++ b/clients/admin-ui/src/features/privacy-requests/types.ts @@ -224,7 +224,7 @@ export const TimelineItemColorMap: Record< export interface ActivityTimelineItem { author: string; title?: string; - date: Date; + date: string; type: ActivityTimelineItemTypeEnum; showViewLog: boolean; onClick?: () => void; From 4e3239a18f86a26b18d3cc8b4ec63726a69121d8 Mon Sep 17 00:00:00 2001 From: Lucano Vera Date: Thu, 22 May 2025 15:43:18 -0300 Subject: [PATCH 26/32] Make date property be a Date to improve code --- clients/admin-ui/src/features/privacy-requests/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/clients/admin-ui/src/features/privacy-requests/types.ts b/clients/admin-ui/src/features/privacy-requests/types.ts index 88354d74e52..11168143e74 100644 --- a/clients/admin-ui/src/features/privacy-requests/types.ts +++ b/clients/admin-ui/src/features/privacy-requests/types.ts @@ -224,7 +224,7 @@ export const TimelineItemColorMap: Record< export interface ActivityTimelineItem { author: string; title?: string; - date: string; + date: Date; type: ActivityTimelineItemTypeEnum; showViewLog: boolean; onClick?: () => void; From eaab3be5d189aa8a5540543038b6876a66398872 Mon Sep 17 00:00:00 2001 From: Lucano Vera Date: Thu, 22 May 2025 15:43:37 -0300 Subject: [PATCH 27/32] remove unnecessary comments --- .../privacy-requests/events-and-logs/ActivityTimeline.tsx | 7 ------- 1 file changed, 7 deletions(-) diff --git a/clients/admin-ui/src/features/privacy-requests/events-and-logs/ActivityTimeline.tsx b/clients/admin-ui/src/features/privacy-requests/events-and-logs/ActivityTimeline.tsx index 7de5bd16865..c4eb233b1d4 100644 --- a/clients/admin-ui/src/features/privacy-requests/events-and-logs/ActivityTimeline.tsx +++ b/clients/admin-ui/src/features/privacy-requests/events-and-logs/ActivityTimeline.tsx @@ -33,16 +33,13 @@ const ActivityTimeline = ({ subjectRequest }: ActivityTimelineProps) => { const { results, id: privacyRequestId } = subjectRequest; - // Use our custom hooks const { commentItems, isLoading: isCommentsLoading } = usePrivacyRequestComments(privacyRequestId); const { eventItems, isLoading: isResultsLoading } = usePrivacyRequestEventLogs(results); - // Combined loading state const isLoading = isCommentsLoading || isResultsLoading; - // Update currentLogs when results change and we have a selected key useEffect(() => { if (currentKey && results && results[currentKey]) { setCurrentLogs(results[currentKey]); @@ -75,9 +72,7 @@ const ActivityTimeline = ({ subjectRequest }: ActivityTimelineProps) => { onOpen(); }; - // Combine and sort all timeline items const timelineItems = useMemo(() => { - // Override the onClick handler for event items const eventItemsWithClickHandler = eventItems.map((item) => { if (item.type === "Request update" && item.title && results) { const key = item.title; @@ -91,7 +86,6 @@ const ActivityTimeline = ({ subjectRequest }: ActivityTimelineProps) => { return item; }); - // Combine both arrays const allItems = [...eventItemsWithClickHandler, ...commentItems]; // Sort by date (oldest first) @@ -100,7 +94,6 @@ const ActivityTimeline = ({ subjectRequest }: ActivityTimelineProps) => { }); }, [eventItems, commentItems, results]); - // Render skeleton items when loading const renderSkeletonItems = () => (
From ab544da1586bbc5c6ce310764b1227a4d03572c2 Mon Sep 17 00:00:00 2001 From: Lucano Vera Date: Thu, 22 May 2025 15:46:55 -0300 Subject: [PATCH 28/32] fix eslint --- .../events-and-logs/ActivityTimeline.tsx | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/clients/admin-ui/src/features/privacy-requests/events-and-logs/ActivityTimeline.tsx b/clients/admin-ui/src/features/privacy-requests/events-and-logs/ActivityTimeline.tsx index c4eb233b1d4..879e7df0349 100644 --- a/clients/admin-ui/src/features/privacy-requests/events-and-logs/ActivityTimeline.tsx +++ b/clients/admin-ui/src/features/privacy-requests/events-and-logs/ActivityTimeline.tsx @@ -4,7 +4,7 @@ import { Box, useDisclosure, } from "fidesui"; -import React, { useEffect, useMemo, useState } from "react"; +import React, { useCallback, useEffect, useMemo, useState } from "react"; import { ExecutionLog, @@ -66,11 +66,14 @@ const ActivityTimeline = ({ subjectRequest }: ActivityTimelineProps) => { onClose(); }; - const showLogs = (key: string, logs: ExecutionLog[]) => { - setCurrentKey(key); - setCurrentLogs(logs); - onOpen(); - }; + const showLogs = useCallback( + (key: string, logs: ExecutionLog[]) => { + setCurrentKey(key); + setCurrentLogs(logs); + onOpen(); + }, + [onOpen], + ); const timelineItems = useMemo(() => { const eventItemsWithClickHandler = eventItems.map((item) => { @@ -90,9 +93,9 @@ const ActivityTimeline = ({ subjectRequest }: ActivityTimelineProps) => { // Sort by date (oldest first) return allItems.sort((a, b) => { - return a.date.getTime() - b.date.getTime(); + return new Date(a.date).getTime() - new Date(b.date).getTime(); }); - }, [eventItems, commentItems, results]); + }, [eventItems, commentItems, results, showLogs]); const renderSkeletonItems = () => (
From 46f51b4c528eae571fbf81775d6ddf4b256cfef6 Mon Sep 17 00:00:00 2001 From: Lucano Vera Date: Thu, 22 May 2025 15:59:18 -0300 Subject: [PATCH 29/32] add initial item to the activity timeline --- .../events-and-logs/ActivityTimeline.tsx | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/clients/admin-ui/src/features/privacy-requests/events-and-logs/ActivityTimeline.tsx b/clients/admin-ui/src/features/privacy-requests/events-and-logs/ActivityTimeline.tsx index 879e7df0349..1c87685ae84 100644 --- a/clients/admin-ui/src/features/privacy-requests/events-and-logs/ActivityTimeline.tsx +++ b/clients/admin-ui/src/features/privacy-requests/events-and-logs/ActivityTimeline.tsx @@ -7,6 +7,7 @@ import { import React, { useCallback, useEffect, useMemo, useState } from "react"; import { + ActivityTimelineItemTypeEnum, ExecutionLog, ExecutionLogStatus, PrivacyRequestEntity, @@ -89,13 +90,29 @@ const ActivityTimeline = ({ subjectRequest }: ActivityTimelineProps) => { return item; }); - const allItems = [...eventItemsWithClickHandler, ...commentItems]; + // Create initial access request item + const initialRequestItem = { + author: "Fides", + title: "Access request received", + date: new Date(subjectRequest.created_at), + type: ActivityTimelineItemTypeEnum.REQUEST_UPDATE, + showViewLog: false, + isError: false, + isSkipped: false, + id: "initial-request", + }; + + const allItems = [ + initialRequestItem, + ...eventItemsWithClickHandler, + ...commentItems, + ]; // Sort by date (oldest first) return allItems.sort((a, b) => { return new Date(a.date).getTime() - new Date(b.date).getTime(); }); - }, [eventItems, commentItems, results, showLogs]); + }, [eventItems, commentItems, results, showLogs, subjectRequest.created_at]); const renderSkeletonItems = () => (
From fa4531a7856a46b83c020357d256541f2a83fda0 Mon Sep 17 00:00:00 2001 From: Lucano Vera Date: Thu, 22 May 2025 16:03:18 -0300 Subject: [PATCH 30/32] remove .only --- clients/admin-ui/cypress/e2e/privacy-requests.cy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/clients/admin-ui/cypress/e2e/privacy-requests.cy.ts b/clients/admin-ui/cypress/e2e/privacy-requests.cy.ts index 1d521d0c1cf..a722bce8af1 100644 --- a/clients/admin-ui/cypress/e2e/privacy-requests.cy.ts +++ b/clients/admin-ui/cypress/e2e/privacy-requests.cy.ts @@ -573,7 +573,7 @@ describe("Privacy Requests", () => { cy.getByTestId("comment-input").should("not.exist"); }); - it.only("shows loading state while fetching comments", () => { + it("shows loading state while fetching comments", () => { cy.intercept("GET", "/api/v1/plus/privacy-request/*/comment*", { statusCode: 200, fixture: "privacy-requests/comments/empty-comments.json", From df0b1fe56ffd366693366eee6c25702fa8ac9108 Mon Sep 17 00:00:00 2001 From: Lucano Vera Date: Fri, 23 May 2025 13:30:26 -0300 Subject: [PATCH 31/32] update date format to show am or pm --- clients/admin-ui/src/features/common/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/clients/admin-ui/src/features/common/utils.ts b/clients/admin-ui/src/features/common/utils.ts index e528e5c5645..966fda9d525 100644 --- a/clients/admin-ui/src/features/common/utils.ts +++ b/clients/admin-ui/src/features/common/utils.ts @@ -32,7 +32,7 @@ export const debounce = (fn: (props?: any) => void, ms = 0) => { }; export const formatDate = (value: string | number | Date): string => - format(new Date(value), "MMMM d, y, kk:mm:ss z"); + format(new Date(value), "MMMM d, y, KK:mm:ss aaa z"); export const utf8ToB64 = (str: string): string => window.btoa(unescape(encodeURIComponent(str))); From 27ce721666d7c2cd8f59633f2c71f3934cf11720 Mon Sep 17 00:00:00 2001 From: Lucano Vera Date: Thu, 29 May 2025 09:36:01 -0300 Subject: [PATCH 32/32] improve error handling --- .../hooks/usePrivacyRequestComments.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/clients/admin-ui/src/features/privacy-requests/events-and-logs/hooks/usePrivacyRequestComments.ts b/clients/admin-ui/src/features/privacy-requests/events-and-logs/hooks/usePrivacyRequestComments.ts index 55189e80345..5ae0442887e 100644 --- a/clients/admin-ui/src/features/privacy-requests/events-and-logs/hooks/usePrivacyRequestComments.ts +++ b/clients/admin-ui/src/features/privacy-requests/events-and-logs/hooks/usePrivacyRequestComments.ts @@ -1,3 +1,6 @@ +import { AntMessage as message } from "fidesui"; +import { useEffect } from "react"; + import { useGetCommentsQuery } from "~/features/privacy-requests/comments/privacy-request-comments.slice"; import { ActivityTimelineItem, @@ -10,11 +13,22 @@ import { CommentResponse } from "~/types/api/models/CommentResponse"; */ export const usePrivacyRequestComments = (privacyRequestId: string) => { // Fetch comments data for this privacy request - const { data: commentsData, isLoading } = useGetCommentsQuery({ + const { + data: commentsData, + isLoading, + error, + } = useGetCommentsQuery({ privacy_request_id: privacyRequestId, size: 100, // Use a reasonable limit }); + // Handle error state + useEffect(() => { + if (error) { + message.error("Failed to fetch the request comments"); + } + }, [error]); + // Map comments to ActivityTimelineItem const commentItems: ActivityTimelineItem[] = !commentsData?.items ? []