Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
fe4d865
Initial implementation for comments ui
lucanovera May 20, 2025
1003598
remove placeholder
lucanovera May 20, 2025
5bc4b80
update types
lucanovera May 20, 2025
8909068
Implement comments saving
lucanovera May 20, 2025
fba36b0
fix type
lucanovera May 20, 2025
6b5fe12
Refactor activitytimelist list into more generic activitytimelineentr…
lucanovera May 20, 2025
970bf81
adjust styling
lucanovera May 20, 2025
15691d7
Add comments
lucanovera May 20, 2025
c108b3b
improve styling
lucanovera May 20, 2025
0bd99e9
Add description
lucanovera May 20, 2025
1ea75d8
Improve comments on timeline
lucanovera May 21, 2025
2dd8875
activity timeline, sort by oldest first
lucanovera May 22, 2025
4104200
Update clipbord icon to Carbon
lucanovera May 22, 2025
3b6b7f7
Update clipboard button to be integrated with input
lucanovera May 22, 2025
8adc799
Adjust style for error records
lucanovera May 22, 2025
bfb4a93
markup improvements
lucanovera May 22, 2025
db40106
add cypress tests
lucanovera May 22, 2025
588749b
add comment registry
lucanovera May 22, 2025
1b4329c
Fix comments cypress tests
lucanovera May 22, 2025
822e441
add test
lucanovera May 22, 2025
e38cecd
Merge branch 'main' of github.com:ethyca/fides into LJ-399-FE-Add-a-m…
lucanovera May 22, 2025
9018ea2
update changelog
lucanovera May 22, 2025
bce26e9
simplify skeleton code
lucanovera May 22, 2025
f001258
refactor activity timeline to simplify code
lucanovera May 22, 2025
0e8a24a
Make date property be a Date to improve code
lucanovera May 22, 2025
f18462f
Fix hour format showing in 12hs to use 24hs format
lucanovera May 22, 2025
4e3239a
Make date property be a Date to improve code
lucanovera May 22, 2025
eaab3be
remove unnecessary comments
lucanovera May 22, 2025
ab544da
fix eslint
lucanovera May 22, 2025
46f51b4
add initial item to the activity timeline
lucanovera May 22, 2025
fa4531a
remove .only
lucanovera May 22, 2025
df0b1fe
update date format to show am or pm
lucanovera May 23, 2025
27ce721
improve error handling
lucanovera May 29, 2025
b12768a
Merge branch 'main' of github.com:ethyca/fides into LJ-399-FE-Add-a-m…
lucanovera May 29, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@ 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)
- Attachments can now be stored with GCS [#6161](https://github.com/ethyca/fides/pull/6161)
- Attachments can now retrieve their content as well as their download urls [#6169 ](https://github.com/ethyca/fides/pull/6169)



## [2.62.0](https://github.com/ethyca/fides/compare/2.61.1...2.62.0)

### Added
Expand Down
121 changes: 121 additions & 0 deletions clients/admin-ui/cypress/e2e/privacy-requests.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -512,4 +512,125 @@ 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.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();

// 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.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();
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.getByTestId("timeline-skeleton").should("exist");

cy.wait("@getCommentsDelayed");

// Verify skeletons are gone after loading
cy.getByTestId("timeline-skeleton").should("not.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");

// 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");
});

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");
});
});
});
Original file line number Diff line number Diff line change
@@ -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"
}
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"items": [],
"total": 0,
"page": 1,
"size": 10
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
"client:delete",
"client:read",
"client:update",
"comment:create",
"comment:read",
"config:read",
"config:update",
"connection:authorize",
Expand Down Expand Up @@ -157,6 +159,7 @@
"classify_instance:read",
"cli-objects:read",
"client:read",
"comment:read",
"connection:read",
"connection_type:read",
"consent:read",
Expand Down Expand Up @@ -201,6 +204,7 @@
"classify_instance:read",
"cli-objects:read",
"client:read",
"comment:read",
"connection:read",
"connection_type:read",
"consent:read",
Expand Down Expand Up @@ -237,6 +241,8 @@
"webhook:read"
],
"approver": [
"comment:create",
"comment:read",
"privacy-request:read",
"privacy-request:resume",
"privacy-request:review",
Expand All @@ -259,6 +265,8 @@
"client:delete",
"client:read",
"client:update",
"comment:create",
"comment:read",
"config:read",
"config:update",
"connection:authorize",
Expand Down
5 changes: 2 additions & 3 deletions clients/admin-ui/src/features/common/ClipboardButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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!",
Expand Down Expand Up @@ -51,7 +50,7 @@ const ClipboardButton = ({ copyText, ...props }: ClipboardButtonProps) => {
}}
>
<Button
icon={<CopyIcon />}
icon={<Icons.Copy />}
aria-label="copy"
type="text"
data-testid="clipboard-btn"
Expand Down
1 change: 1 addition & 0 deletions clients/admin-ui/src/features/common/api.slice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion clients/admin-ui/src/features/common/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -46,7 +46,7 @@ const PrivacyRequest = ({ data: initialData }: PrivacyRequestProps) => {
{
key: "activity",
label: "Activity",
children: <ActivityTimeline subjectRequest={subjectRequest} />,
children: <ActivityTab subjectRequest={subjectRequest} />,
},
{
key: "manual-steps",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
import {
AntButton as Button,
AntFlex as Flex,
AntForm as Form,
AntInput as Input,
AntSpace as Space,
AntTooltip as Tooltip,
AntTypography as Typography,
} from "fidesui";

import ClipboardButton from "~/features/common/ClipboardButton";
import DaysLeftTag from "~/features/common/DaysLeftTag";
import { useFeatures } from "~/features/common/features";
import RequestStatusBadge from "~/features/common/RequestStatusBadge";
import RequestType from "~/features/common/RequestType";
import { PrivacyRequestEntity } from "~/features/privacy-requests/types";
import { PrivacyRequestStatus as ApiPrivacyRequestStatus } from "~/types/api/models/PrivacyRequestStatus";

import ClipboardButton from "../common/ClipboardButton";
import RequestAttachments from "./attachments/RequestAttachments";
import RequestCustomFields from "./RequestCustomFields";
import RequestDetailsRow from "./RequestDetailsRow";
Expand Down Expand Up @@ -79,8 +81,14 @@ const RequestDetails = ({ subjectRequest }: RequestDetailsProps) => {
<Form layout="vertical">
<Form.Item label="Request ID:" className="mb-4">
<Flex gap={1}>
<Input readOnly value={id} data-testid="request-detail-value-id" />
<ClipboardButton copyText={id} size="small" />
<Space.Compact style={{ width: "100%" }}>
<Input
readOnly
value={id}
data-testid="request-detail-value-id"
/>
<Button icon={<ClipboardButton copyText={id} />} />
</Space.Compact>
</Flex>
</Form.Item>
<Form.Item label="Policy key:" className="mb-4">
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Page_CommentResponse_, GetCommentsParams>({
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<CommentResponse, CreateCommentParams>({
query: ({ privacy_request_id, comment_text, comment_type }) => {
const formData = new FormData();
formData.append("comment_text", comment_text);
formData.append("comment_type", comment_type);
Comment thread
gilluminate marked this conversation as resolved.

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