diff --git a/CHANGELOG.md b/CHANGELOG.md index f1bb06d08b..1504bceac0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,8 @@ The types of changes are: - Access and erasure support for Unbounce [#2697](https://github.com/ethyca/fides/pull/2697) - Support pseudonymous consent requests with `fides_user_device_id` [#3158](https://github.com/ethyca/fides/pull/3158) - Update `fides_consent` cookie format [#3158](https://github.com/ethyca/fides/pull/3158) +- Add custom fields to the data use declaration form [#3197](https://github.com/ethyca/fides/pull/3197) + ### Changed diff --git a/clients/admin-ui/cypress/e2e/systems.cy.ts b/clients/admin-ui/cypress/e2e/systems.cy.ts index abd7a039b6..c7423ceaf0 100644 --- a/clients/admin-ui/cypress/e2e/systems.cy.ts +++ b/clients/admin-ui/cypress/e2e/systems.cy.ts @@ -1,4 +1,8 @@ -import { stubSystemCrud, stubTaxonomyEntities } from "cypress/support/stubs"; +import { + stubPlus, + stubSystemCrud, + stubTaxonomyEntities, +} from "cypress/support/stubs"; import { ADD_SYSTEMS_MANUAL_ROUTE, @@ -12,6 +16,7 @@ describe("System management page", () => { cy.intercept("GET", "/api/v1/system", { fixture: "systems/systems.json", }).as("getSystems"); + stubPlus(false); }); it("Can navigate to the system management page", () => { @@ -171,9 +176,6 @@ describe("System management page", () => { dataset_references: ["demo_users_dataset_2"], }); }); - cy.getByTestId("new-declaration-form").within(() => { - cy.getByTestId("header").contains("System"); - }); }); }); @@ -332,16 +334,18 @@ describe("System management page", () => { }); // edit the existing declaration - const newDataUse = "collect"; cy.getByTestId("accordion-header-improve.system").click(); cy.getByTestId("improve.system-form").within(() => { - cy.getByTestId("input-data_use").type(`${newDataUse}{enter}`); + cy.getByTestId("input-data_subjects").type(`anonymous{enter}`); cy.getByTestId("save-btn").click(); }); cy.wait("@putSystem").then((interception) => { const { body } = interception.request; expect(body.privacy_declarations.length).to.eql(1); - expect(body.privacy_declarations[0].data_use).to.eql(newDataUse); + expect(body.privacy_declarations[0].data_subjects).to.eql([ + "customer", + "anonymous_user", + ]); }); cy.getByTestId("saved-indicator"); }); @@ -470,7 +474,8 @@ describe("System management page", () => { cy.getByTestId("toast-success-msg"); }); - it("warns when a data use is being edited to one that is already used", () => { + // Cannot currently edit the data use or name fields—they have been disabled + it.skip("warns when a data use is being edited to one that is already used", () => { cy.fixture("systems/systems_with_data_uses.json").then((systems) => { cy.intercept("GET", "/api/v1/system/*", { body: systems[0], @@ -589,12 +594,12 @@ describe("System management page", () => { }); it("Can open both accordion items", () => { - cy.getByTestId("data-flow-accordion").within(()=>{ + cy.getByTestId("data-flow-accordion").within(() => { cy.getByTestId("data-flow-button-Source").click(); cy.getByTestId("data-flow-panel-Source").should("exist"); cy.getByTestId("data-flow-button-Destination").click(); cy.getByTestId("data-flow-panel-Destination").should("exist"); - }) + }); }); }); }); diff --git a/clients/admin-ui/src/features/common/custom-fields/hooks.ts b/clients/admin-ui/src/features/common/custom-fields/hooks.ts index 2a5495ad0b..5313f5b189 100644 --- a/clients/admin-ui/src/features/common/custom-fields/hooks.ts +++ b/clients/admin-ui/src/features/common/custom-fields/hooks.ts @@ -136,7 +136,11 @@ export const useCustomFields = ({ // When creating an resource, the fides key may have initially been blank. But by the time the // form is submitted it must not be blank (not undefined, not an empty string). - const fidesKey = formValues.fides_key || resourceFidesKey; + const fidesKey = + "fides_key" in formValues && formValues.fides_key !== "" + ? formValues.fides_key + : resourceFidesKey; + if (!fidesKey) { return; } diff --git a/clients/admin-ui/src/features/custom-fields/CustomFieldForm.tsx b/clients/admin-ui/src/features/custom-fields/CustomFieldForm.tsx index 9efabb7ab1..614f8dab2a 100644 --- a/clients/admin-ui/src/features/custom-fields/CustomFieldForm.tsx +++ b/clients/admin-ui/src/features/custom-fields/CustomFieldForm.tsx @@ -26,6 +26,7 @@ type Props = FormikProps & { isLoading: boolean; onClose: () => void; handleDropdownChange: (value: FieldTypes) => void; + isEditing: boolean; }; export const CustomFieldForm = ({ @@ -38,6 +39,7 @@ export const CustomFieldForm = ({ isLoading, onClose, handleDropdownChange, + isEditing, }: Props) => { const { validateForm } = useFormikContext(); useEffect(() => { @@ -71,6 +73,7 @@ export const CustomFieldForm = ({ name="resource_type" options={RESOURCE_TYPE_OPTIONS} labelProps={CustomFieldLabelStyles} + isDisabled={isEditing} /> diff --git a/clients/admin-ui/src/features/custom-fields/CustomFieldModal.tsx b/clients/admin-ui/src/features/custom-fields/CustomFieldModal.tsx index 31b67f4ef8..748c1dcddc 100644 --- a/clients/admin-ui/src/features/custom-fields/CustomFieldModal.tsx +++ b/clients/admin-ui/src/features/custom-fields/CustomFieldModal.tsx @@ -146,6 +146,7 @@ export const CustomFieldModal = ({ } const transformedCustomField = transformCustomField(customField, allowList); + const isEditing = !!transformedCustomField; const initialValues = transformedCustomField || initialValuesTemplate; const handleDropdownChange = (value: FieldTypes) => { @@ -276,6 +277,7 @@ export const CustomFieldModal = ({ > {(props) => ( diff --git a/clients/admin-ui/src/features/datamap/privacy-declarations/PrivacyDeclarationForm.tsx b/clients/admin-ui/src/features/datamap/privacy-declarations/PrivacyDeclarationForm.tsx index 5e3b632e91..4d8bd1dad4 100644 --- a/clients/admin-ui/src/features/datamap/privacy-declarations/PrivacyDeclarationForm.tsx +++ b/clients/admin-ui/src/features/datamap/privacy-declarations/PrivacyDeclarationForm.tsx @@ -79,6 +79,7 @@ export interface DataProps { allDataCategories: DataCategory[]; allDataUses: DataUse[]; allDataSubjects: DataSubject[]; + isEditing?: boolean; } export const PrivacyDeclarationFormComponents = ({ @@ -86,6 +87,7 @@ export const PrivacyDeclarationFormComponents = ({ allDataCategories, allDataSubjects, onDelete, + isEditing, }: DataProps & Pick) => { const { dirty, isSubmitting, isValid, initialValues } = useFormikContext(); @@ -110,12 +112,14 @@ export const PrivacyDeclarationFormComponents = ({ tooltip="What is the system using the data for. For example, is it for third party advertising or perhaps simply providing system operations." variant="stacked" singleValueBlock + isDisabled={isEditing} /> Promise; - onDelete: (declaration: PrivacyDeclarationWithId) => Promise; + ) => Promise; + onDelete: ( + declaration: PrivacyDeclarationWithId + ) => Promise; } const PrivacyDeclarationAccordionItem = ({ @@ -34,13 +36,14 @@ const PrivacyDeclarationAccordionItem = ({ AccordionProps, "privacyDeclarations" >) => { - const handleEdit = (newValues: PrivacyDeclarationWithId) => - onEdit(privacyDeclaration, newValues); + const handleEdit = (values: PrivacyDeclarationWithId) => + onEdit(privacyDeclaration, values); const { initialValues, renderHeader, handleSubmit } = usePrivacyDeclarationForm({ initialValues: privacyDeclaration, onSubmit: handleEdit, + privacyDeclarationId: privacyDeclaration.id, ...dataProps, }); @@ -71,6 +74,7 @@ const PrivacyDeclarationAccordionItem = ({ diff --git a/clients/admin-ui/src/features/system/privacy-declarations/PrivacyDeclarationForm.tsx b/clients/admin-ui/src/features/system/privacy-declarations/PrivacyDeclarationForm.tsx index ade499eb3e..c48781c3d9 100644 --- a/clients/admin-ui/src/features/system/privacy-declarations/PrivacyDeclarationForm.tsx +++ b/clients/admin-ui/src/features/system/privacy-declarations/PrivacyDeclarationForm.tsx @@ -13,6 +13,11 @@ import { Text, useDisclosure, } from "@fidesui/react"; +import { + CustomFieldsList, + CustomFieldValues, + useCustomFields, +} from "common/custom-fields"; import { Form, Formik, FormikHelpers, useFormikContext } from "formik"; import { useMemo, useState } from "react"; import * as Yup from "yup"; @@ -25,6 +30,7 @@ import { DataSubject, DataUse, PrivacyDeclaration, + ResourceTypes, } from "~/types/api"; import { PrivacyDeclarationWithId } from "./types"; @@ -39,23 +45,35 @@ export const ValidationSchema = Yup.object().shape({ .label("Data subjects"), }); -const defaultInitialValues: PrivacyDeclaration = { +export type FormValues = PrivacyDeclarationWithId & { + customFieldValues: CustomFieldValues; +}; + +const defaultInitialValues: FormValues = { data_categories: [], data_subjects: [], data_use: "", dataset_references: [], + customFieldValues: {}, + id: "", }; -type FormValues = typeof defaultInitialValues; - const transformPrivacyDeclarationToHaveId = ( privacyDeclaration: PrivacyDeclaration -) => ({ - ...privacyDeclaration, - id: privacyDeclaration.name - ? `${privacyDeclaration.data_use} - ${privacyDeclaration.name}` - : privacyDeclaration.data_use, -}); +) => { + // TODO: there's a typing problem here: the backend types still show PrivacyDeclaration + // instead of PrivacyDeclarationResponse (which has an id) + // @ts-ignore + const { id, name, data_use: dataUse } = privacyDeclaration; + let declarationId: string | undefined = id; + if (!declarationId) { + declarationId = name ? `${dataUse} - ${name}` : dataUse; + } + return { + ...privacyDeclaration, + id: declarationId, + }; +}; export const transformPrivacyDeclarationsToHaveId = ( privacyDeclarations: PrivacyDeclaration[] @@ -75,7 +93,8 @@ export const PrivacyDeclarationFormComponents = ({ allDataSubjects, allDatasets, onDelete, -}: DataProps & Pick) => { + privacyDeclarationId, +}: DataProps & Pick & { privacyDeclarationId?: string }) => { const { dirty, isSubmitting, isValid, initialValues } = useFormikContext(); const deleteModal = useDisclosure(); @@ -107,6 +126,7 @@ export const PrivacyDeclarationFormComponents = ({ tooltip="What is the system using the data for. For example, is it for third party advertising or perhaps simply providing system operations." variant="stacked" singleValueBlock + isDisabled={!!privacyDeclarationId} /> ) : null} +