Skip to content

Commit

Permalink
ui/custom-metadata: Refactor taxonomy to use a single form submission…
Browse files Browse the repository at this point in the history
… flow (#2586)
  • Loading branch information
ssangervasi authored Feb 15, 2023
1 parent bb3f4af commit 368a407
Show file tree
Hide file tree
Showing 22 changed files with 840 additions and 295 deletions.
19 changes: 10 additions & 9 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,17 @@ The types of changes are:

## [Unreleased](https://github.com/ethyca/fides/compare/2.6.6...main)

* Access and erasure support for Braintree [#2223](https://github.com/ethyca/fides/pull/2223)
* Added route to send a test message [#2585](https://github.com/ethyca/fides/pull/2585)
* Added new Wunderkind Consent Saas Connector [#2600](https://github.com/ethyca/fides/pull/2600)

* Fides API
* Access and erasure support for Braintree [#2223](https://github.com/ethyca/fides/pull/2223)
* Added route to send a test message [#2585](https://github.com/ethyca/fides/pull/2585)
* Added new Wunderkind Consent Saas Connector [#2600](https://github.com/ethyca/fides/pull/2600)
* Admin UI
* Create custom fields from a resource screen - Button to Trigger modal [#524](https://github.com/ethyca/fides/pull/2536)
* Create Custom Lists [#525](https://github.com/ethyca/fides/pull/2536)
* Create Custom Field Definition [#526](https://github.com/ethyca/fides/pull/2536)
* Provide a custom field value in a resource [#528](https://github.com/ethyca/fides/pull/2536)

* Custom Metadata [#2536](https://github.com/ethyca/fides/pull/2536)
* Create Custom Lists
* Create Custom Field Definition
* Create custom fields from a the taxonomy editor
* Provide a custom field value in a resource
* Bulk edit custom field values [#2612](https://github.com/ethyca/fides/issues/2612)
* Privacy Center
* The consent config default value can depend on whether Global Privacy Control is enabled. [#2341](https://github.com/ethyca/fides/pull/2341)

Expand Down
143 changes: 135 additions & 8 deletions clients/admin-ui/cypress/e2e/taxonomy-plus.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,48 @@ describe("Taxonomy management with Plus features", () => {
cy.visit("/taxonomy");
});

const RESOURCE_TYPE = {
label: "Data Categories",
key: ResourceTypes.DATA_CATEGORY,
};
const RESOURCE_PARENT = {
label: "User Data",
key: "user",
};
const RESOURCE_CHILD = {
label: "Biometric Data",
key: "user.biometric",
};

const navigateToEditor = () => {
cy.getByTestId("accordion-item-User Data").click();
cy.getByTestId("item-Biometric Data")
cy.getByTestId(`accordion-item-${RESOURCE_PARENT.label}`).click();
cy.getByTestId(`item-${RESOURCE_CHILD.label}`)
.click()
.within(() => {
cy.getByTestId("edit-btn").click();
});
};

// TODO: Extract these to a cypress support file.
const getSelectValueContainer = (selectorId: string) =>
cy.getByTestId(selectorId).find(`.custom-select__value-container`);

const getSelectOptionList = (selectorId: string) =>
cy.getByTestId(selectorId).click().find(`.custom-select__menu-list`);

const selectOption = (selectorId: string, optionText: string) => {
cy.getByTestId(selectorId)
.click()
.within(() => {
cy.contains(optionText).click();
});
getSelectOptionList(selectorId).contains(optionText).click();
};

const removeMultiValue = (selectorId: string, optionText: string) =>
getSelectValueContainer(selectorId)
.contains(optionText)
.siblings(".custom-select__multi-value__remove")
.click();

const clearSingleValue = (selectorId: string) =>
cy.getByTestId(selectorId).find(".custom-select__clear-indicator").click();

describe("Defining custom lists", () => {
beforeEach(() => {
navigateToEditor();
Expand Down Expand Up @@ -137,7 +162,7 @@ describe("Taxonomy management with Plus features", () => {
active: true,
field_type: "string[]",
name: "Multi-select",
resource_type: ResourceTypes.DATA_CATEGORY,
resource_type: RESOURCE_TYPE.key,
});
});

Expand All @@ -149,4 +174,106 @@ describe("Taxonomy management with Plus features", () => {
);
});
});

describe("Using custom fields", () => {
beforeEach(() => {
cy.intercept(
{
method: "GET",
pathname: "/api/v1/plus/custom-metadata/allow-list",
query: {
show_values: "true",
},
},
{
fixture: "taxonomy/custom-metadata/allow-list/list.json",
}
).as("getAllowLists");
cy.intercept(
"GET",
// Cypress route matching doesn't escape special characters (whitespace).
`/api/v1/plus/custom-metadata/custom-field-definition/resource-type/${encodeURIComponent(
RESOURCE_TYPE.key
)}`,

{
fixture: "taxonomy/custom-metadata/custom-field-definition/list.json",
}
).as("getCustomFieldDefinitions");
cy.intercept(
"GET",
`/api/v1/plus/custom-metadata/custom-field/resource/${RESOURCE_CHILD.key}`,
{
fixture: "taxonomy/custom-metadata/custom-field/list.json",
}
).as("getCustomFields");

navigateToEditor();

cy.wait([
"@getAllowLists",
"@getCustomFieldDefinitions",
"@getCustomFields",
]);
});

const testIdSingle =
"input-definitionIdToCustomFieldValue.id-custom-field-definition-starter-pokemon";
const testIdMulti =
"input-definitionIdToCustomFieldValue.id-custom-field-definition-pokemon-party";

it("initializes form fields with values returned by the API", () => {
cy.getByTestId("custom-fields-list");
getSelectValueContainer(testIdSingle).contains("Squirtle");

["Charmander", "Eevee", "Snorlax"].forEach((value) => {
getSelectValueContainer(testIdMulti).contains(value);
});
});

it("allows choosing and changing selections", () => {
cy.getByTestId("custom-fields-list");

clearSingleValue(testIdSingle);
selectOption(testIdSingle, "Snorlax");
getSelectValueContainer(testIdSingle).contains("Snorlax");
clearSingleValue(testIdSingle);

removeMultiValue(testIdMulti, "Eevee");
removeMultiValue(testIdMulti, "Snorlax");
selectOption(testIdMulti, "Eevee");

["Charmander", "Eevee"].forEach((value) => {
getSelectValueContainer(testIdMulti).contains(value);
});

cy.intercept(
"DELETE",
`/api/v1/plus/custom-metadata/custom-field/id-custom-field-starter-pokemon`,
{
statusCode: 204,
}
).as("deleteStarter");
cy.intercept("PUT", `/api/v1/plus/custom-metadata/custom-field`, {
fixture: "taxonomy/custom-metadata/custom-field/update-party.json",
}).as("updateParty");

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

cy.wait(["@updateParty", "@deleteStarter"]).then(
([updatePartyInterception]) => {
expect(updatePartyInterception.request.body.id).to.eql(
"id-custom-field-pokemon-party"
);
expect(updatePartyInterception.request.body.resource_id).to.eql(
RESOURCE_CHILD.key
);
expect(updatePartyInterception.request.body.value).to.eql([
"Charmander",
"Eevee",
]);
}
);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@
"name": "Created list",
"description": null,
"allowed_values": ["allowed", "values"],
"id": "plu_5401a766-b83a-44c5-8a21-f7a46c02ee40"
"id": "id-allow-list-created"
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,19 @@
{
"name": "Pokemon",
"description": "There's like a thousand of these now",
"allowed_values": ["Bulbasaur", "Charmander", "Squirtle"],
"id": "plu_e868c7a7-fc87-48cf-8219-192c7770d2b6"
"allowed_values": [
"Bulbasaur",
"Charmander",
"Squirtle",
"Eevee",
"Snorlax"
],
"id": "id-allow-list-pokemon"
},
{
"name": "Prime numbers",
"description": "Seems to be a pattern",
"allowed_values": ["2", "3", "5", "7"],
"id": "plu_93c3ea8a-e1f9-4e0c-955f-8025aa7c049a"
"id": "id-allow-list-prime-numbers"
}
]
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
{
"name": "11111111111111",
"name": "Created custom field",
"description": null,
"field_type": "string",
"allow_list_id": "plu_e868c7a7-fc87-48cf-8219-192c7770d2b6",
"allow_list_id": "id-allow-list-pokemon",
"resource_type": "data category",
"field_definition": null,
"active": true,
"id": "plu_27bc6001-3125-44b0-b9eb-90ecb9a58802"
"id": "id-custom-field-definition-created"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
[
{
"name": "Starter pokemon",
"description": null,
"field_type": "string",
"allow_list_id": "id-allow-list-pokemon",
"resource_type": "data category",
"field_definition": null,
"active": true,
"id": "id-custom-field-definition-starter-pokemon"
},
{
"name": "Pokemon party",
"description": null,
"field_type": "string[]",
"allow_list_id": "id-allow-list-pokemon",
"resource_type": "data category",
"field_definition": null,
"active": true,
"id": "id-custom-field-definition-pokemon-party"
},
{
"name": "Prime number",
"description": null,
"field_type": "string",
"allow_list_id": "id-allow-list-prime-numbers",
"resource_type": "data category",
"field_definition": null,
"active": true,
"id": "id-custom-field-definition-prime-number"
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[
{
"resource_id": "user.biometric",
"custom_field_definition_id": "id-custom-field-definition-starter-pokemon",
"value": "Squirtle",
"id": "id-custom-field-starter-pokemon"
},
{
"resource_id": "user.biometric",
"custom_field_definition_id": "id-custom-field-definition-pokemon-party",
"value": ["Charmander", "Eevee", "Snorlax"],
"id": "id-custom-field-pokemon-party"
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"resource_id": "user.biometric",
"custom_field_definition_id": "id-custom-field-definition-pokemon-party",
"value": ["Charmander", "Eevee"],
"id": "id-custom-field-pokemon-party"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { useField } from "formik";
import { useEffect } from "react";

import { CustomSelect } from "~/features/common/form/inputs";
import { AllowedTypes } from "~/types/api";

import {
AllowListWithOptions,
CustomFieldDefinitionExisting,
CustomFieldExisting,
} from "./types";

type Props = {
allowList: AllowListWithOptions;
customField?: CustomFieldExisting;
customFieldDefinition: CustomFieldDefinitionExisting;
};

export const CustomFieldSelector = ({
allowList,
customField,
customFieldDefinition,
}: Props) => {
const name = `definitionIdToCustomFieldValue.${customFieldDefinition.id}`;
const label = customFieldDefinition.name;
const tooltip = customFieldDefinition.description;
const { options } = allowList;

const [, meta, helpers] = useField({ name });

// This is a bit of a hack to set the initial value of the selector based on the CustomField
// returned by the API (if any). The "correct" way to do this would be to have the `initialValues`
// passed to `Formik` include the API values mapped by definition ID. However, because custom
// fields are so dynamic and depend on multiple API calls, mixing that logic into the (already
// complex) taxonomy form initialization isn't feasible for now. Those values are created by:
// src/features/taxonomy/hooks.tsx.
useEffect(
() => {
if (!customField?.value) {
return;
}

if (meta.touched) {
return;
}

helpers.setValue(customField.value, false);
},
// This should only ever run once, on first render.
// eslint-disable-next-line react-hooks/exhaustive-deps
[]
);

return (
<CustomSelect
isClearable
isMulti={customFieldDefinition.field_type !== AllowedTypes.STRING}
label={label}
name={name}
options={options}
tooltip={tooltip}
defaultValue={customField?.value ?? ""}
/>
);
};
Loading

0 comments on commit 368a407

Please sign in to comment.