Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
87 changes: 87 additions & 0 deletions clients/admin-ui/cypress/e2e/properties.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,93 @@ describe("Properties page", () => {
expect(body.paths).to.eql([]);
});
});

it("Should include paths in the create payload", () => {
cy.intercept("POST", "/api/v1/plus/property", {
statusCode: 200,
body: {
id: "FDS-NEW456",
name: "Test Property",
type: "Website",
paths: ["/privacy", "/dsr"],
experiences: [],
},
}).as("createProperty");

cy.visit(ADD_PROPERTY_ROUTE);
cy.getByTestId("input-name").type("Test Property");

// Add two paths via the Form.List
cy.contains("button", "Add path").click();
cy.get("#paths_0").type("/privacy");
cy.contains("button", "Add path").click();
cy.get("#paths_1").type("/dsr");

cy.getByTestId("save-btn").click();

cy.wait("@createProperty").then((interception) => {
const { body } = interception.request;
expect(body.paths).to.eql(["/privacy", "/dsr"]);
});
});

it("Should allow removing a path before saving", () => {
cy.intercept("POST", "/api/v1/plus/property", {
statusCode: 200,
body: {
id: "FDS-NEW789",
name: "Test Property",
type: "Website",
paths: ["/dsr"],
experiences: [],
},
}).as("createProperty");

cy.visit(ADD_PROPERTY_ROUTE);
cy.getByTestId("input-name").type("Test Property");

// Add two paths, then remove the first
cy.contains("button", "Add path").click();
cy.get("#paths_0").type("/privacy");
cy.contains("button", "Add path").click();
cy.get("#paths_1").type("/dsr");
cy.get("button[aria-label='Remove path']").first().click();

cy.getByTestId("save-btn").click();

cy.wait("@createProperty").then((interception) => {
const { body } = interception.request;
expect(body.paths).to.eql(["/dsr"]);
});
});
});

describe("Edit", () => {
it("Should load existing paths and include them in the update payload", () => {
cy.intercept("GET", "/api/v1/plus/property/*", {
fixture: "properties/property.json",
}).as("getProperty");
cy.intercept("PUT", "/api/v1/plus/property/*", {
fixture: "properties/property.json",
}).as("updateProperty");

cy.getAntTableRow("FDS-CEA9EV").contains("Property A").click();
cy.wait("@getProperty");

// Verify existing path is loaded
cy.get("#paths_0").should("have.value", "/privacy");

// Add another path
cy.contains("button", "Add path").click();
cy.get("#paths_1").type("/dsr");

cy.getByTestId("save-btn").click();

cy.wait("@updateProperty").then((interception) => {
const { body } = interception.request;
expect(body.paths).to.eql(["/privacy", "/dsr"]);
});
});
});

describe("Delete", () => {
Expand Down
2 changes: 1 addition & 1 deletion clients/admin-ui/cypress/fixtures/properties/property.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@
"name": "Property A",
"type": "Website",
"id": "FDS-CEA9EV",
"paths": [],
"paths": ["/privacy"],
"experiences": []
}
45 changes: 33 additions & 12 deletions clients/admin-ui/src/features/properties/PropertyForm.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
import { Button, Card, Flex, Form, Input, Select, Space, Spin } from "fidesui";
import {
Button,
Card,
Flex,
Form,
Icons,
Input,
Select,
Space,
Spin,
} from "fidesui";
import { useRouter } from "next/router";
import { useCallback, useEffect, useMemo, useState } from "react";

Expand All @@ -15,20 +25,11 @@ import {
} from "~/types/api";

import DeletePropertyModal from "./DeletePropertyModal";
import { PathsEditor } from "./PathsEditor";
Copy link
Copy Markdown
Contributor

@gilluminate gilluminate May 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this file be deleted now? Still used anywhere else? It has associated tests, should those be migrated to this now?

import {
PrivacyCenterConfigSection,
PrivacyCenterConfigValue,
} from "./privacy-center-config/PrivacyCenterConfigSection";

const PathsEditorAdapter = ({
value,
onChange,
}: {
value?: string[];
onChange?: (next: string[]) => void;
}) => <PathsEditor value={value ?? []} onChange={(next) => onChange?.(next)} />;

const PCConfigSectionAdapter = ({
propertyId,
value,
Expand Down Expand Up @@ -197,10 +198,30 @@ export const PropertyForm = ({
</Form.Item>
<Form.Item
label="Privacy center paths"
name="paths"
tooltip="Paths under your privacy center this property responds to. Each path must be unique across properties."
>
<PathsEditorAdapter />
<Form.List name="paths">
{(fields, { add, remove }) => (
<Flex vertical>
{fields.map((field) => (
<Flex className="my-1" key={field.key}>
<Form.Item name={field.name} className="mb-0 grow">
<Input placeholder="/privacy" />
</Form.Item>
<Button
aria-label="Remove path"
className="ml-2"
icon={<Icons.TrashCan />}
onClick={() => remove(field.name)}
/>
</Flex>
))}
<Button className="mt-2" onClick={() => add("")}>
Add path
</Button>
</Flex>
)}
</Form.List>
</Form.Item>
<Form.Item
name="privacy_center_config"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,14 +75,18 @@ export const ActionsTable = ({
<List.Item
key={row.policy_key}
actions={[
<Link
key="edit-form"
href={`/properties/${propertyId}/forms/${encodeURIComponent(
row.policy_key,
)}`}
>
<Button>Edit form</Button>
</Link>,
...(propertyId
? [
<Link
key="edit-form"
href={`/properties/${propertyId}/forms/${encodeURIComponent(
row.policy_key,
)}`}
>
<Button>Edit form</Button>
</Link>,
]
: []),
<Button
key="edit-action"
icon={<Icons.Edit />}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,36 @@ describe("ActionsTable", () => {
expect(screen.getByText("1 field")).toBeInTheDocument();
});

it("shows 'Edit form' link when propertyId is set", () => {
render(
<ActionsTable
propertyId="p1"
actions={[sampleAction]}
onEditAction={jest.fn()}
onAddAction={jest.fn()}
onDeleteAction={jest.fn()}
/>,
);
expect(
screen.getByRole("button", { name: /edit form/i }),
).toBeInTheDocument();
});

it("hides 'Edit form' link when propertyId is empty", () => {
render(
<ActionsTable
propertyId=""
actions={[sampleAction]}
onEditAction={jest.fn()}
onAddAction={jest.fn()}
onDeleteAction={jest.fn()}
/>,
);
expect(
screen.queryByRole("button", { name: /edit form/i }),
).not.toBeInTheDocument();
});

it("calls onEditAction when 'Edit action' is clicked", async () => {
const onEdit = jest.fn();
render(
Expand Down
Loading