diff --git a/clients/admin-ui/cypress/e2e/systems-plus.cy.ts b/clients/admin-ui/cypress/e2e/systems-plus.cy.ts index 1f36b6a41d..67e25321ec 100644 --- a/clients/admin-ui/cypress/e2e/systems-plus.cy.ts +++ b/clients/admin-ui/cypress/e2e/systems-plus.cy.ts @@ -45,16 +45,6 @@ describe("System management with Plus features", () => { ); }); - it("can switch entries", () => { - cy.getSelectValueContainer("input-vendor_id").type("Aniview{enter}"); - cy.getSelectValueContainer("input-vendor_id").contains("Aniview LTD"); - - cy.getSelectValueContainer("input-vendor_id").type("Anzu{enter}"); - cy.getSelectValueContainer("input-vendor_id").contains( - "Anzu Virtual Reality LTD" - ); - }); - it("locks editing for a GVL vendor when TCF is enabled", () => { cy.getSelectValueContainer("input-vendor_id").type("Aniview{enter}"); cy.getByTestId("locked-for-GVL-notice"); @@ -66,8 +56,6 @@ describe("System management with Plus features", () => { it("can switch between tabs after populating from dictionary", () => { cy.wait("@getSystems"); cy.getSelectValueContainer("input-vendor_id").type("Anzu{enter}"); - cy.getByTestId("dict-suggestions-btn").click(); - cy.getByTestId("toggle-dict-suggestions").click(); // the form fetches the system again after saving, so update the intercept with dictionary values cy.fixture("systems/dictionary-system.json").then((dictSystem) => { cy.fixture("systems/system.json").then((origSystem) => { diff --git a/clients/admin-ui/src/features/system/SystemInformationForm.tsx b/clients/admin-ui/src/features/system/SystemInformationForm.tsx index b1b4314faf..b2486b8cf4 100644 --- a/clients/admin-ui/src/features/system/SystemInformationForm.tsx +++ b/clients/admin-ui/src/features/system/SystemInformationForm.tsx @@ -20,7 +20,6 @@ import { } from "~/features/common/custom-fields"; import { useFeatures } from "~/features/common/features/features.slice"; import { - CustomCreatableSelect, CustomSelect, CustomSwitch, CustomTextInput, @@ -29,6 +28,7 @@ import { extractVendorSource, getErrorMessage, isErrorResult, + isFetchBaseQueryError, VendorSources, } from "~/features/common/helpers"; import { FormGuard } from "~/features/common/hooks/useIsAnyFormDirty"; @@ -43,6 +43,7 @@ import { setSuggestions, } from "~/features/system/dictionary-form/dict-suggestion.slice"; import { + DictSuggestionCreatableSelect, DictSuggestionNumberInput, DictSuggestionSelect, DictSuggestionSwitch, @@ -72,11 +73,6 @@ import { responsibilityOptions, } from "./SystemInformationFormSelectOptions"; -const ValidationSchema = Yup.object().shape({ - name: Yup.string().required().label("System name"), - privacy_policy: Yup.string().min(1).url().nullable(), -}); - const SystemHeading = ({ system }: { system?: SystemResponse }) => { const isManual = !system; const headingName = isManual @@ -103,6 +99,8 @@ const SystemInformationForm = ({ withHeader, children, }: Props) => { + const systems = useAppSelector(selectAllSystems); + const dispatch = useAppDispatch(); const customFields = useCustomFields({ resourceType: ResourceTypes.SYSTEM, @@ -125,6 +123,23 @@ const SystemInformationForm = ({ [passedInSystem, customFields.customFieldValues] ); + const ValidationSchema = useMemo( + () => + Yup.object().shape({ + name: Yup.string() + .required() + .label("System name") + .notOneOf( + systems + .filter((s) => s.name !== initialValues.name) + .map((s) => s.name), + "System must have a unique name" + ), + privacy_policy: Yup.string().min(1).url().nullable(), + }), + [systems, initialValues.name] + ); + const features = useFeatures(); const [createSystemMutationTrigger, createSystemMutationResult] = @@ -139,7 +154,6 @@ const SystemInformationForm = ({ const dictionaryOptions = useAppSelector(selectAllDictEntries); const lockedForGVL = useAppSelector(selectLockedForGVL); - const systems = useAppSelector(selectAllSystems); const isEditing = useMemo( () => Boolean( @@ -167,16 +181,21 @@ const SystemInformationForm = ({ formikHelpers: FormikHelpers ) => { let dictionaryDeclarations; - if (lockedForGVL && values.privacy_declarations.length === 0) { + if (values.vendor_id && values.privacy_declarations.length === 0) { const dataUseQueryResult = await getDictionaryDataUseTrigger({ vendor_id: values.vendor_id!, }); if (dataUseQueryResult.isError) { - const dataUseErrorMsg = getErrorMessage( - dataUseQueryResult.error, - `A problem occurred while fetching data uses from the GVL for your system. Please try again.` - ); - toast({ status: "error", description: dataUseErrorMsg }); + const isNotFoundError = + isFetchBaseQueryError(dataUseQueryResult.error) && + dataUseQueryResult.error.status === 404; + if (!isNotFoundError) { + const dataUseErrorMsg = getErrorMessage( + dataUseQueryResult.error, + `A problem occurred while fetching data uses from Fides Compass for your system. Please try again.` + ); + toast({ status: "error", description: dataUseErrorMsg }); + } } else if ( dataUseQueryResult.data && dataUseQueryResult.data.items.length > 0 @@ -232,12 +251,17 @@ const SystemInformationForm = ({ handleResult(result); }; - const handleVendorSelected = (newVendorId: string) => { + const handleVendorSelected = (newVendorId: string | undefined) => { + if (!newVendorId) { + dispatch(setSuggestions("hiding")); + dispatch(setLockedForGVL(false)); + return; + } + dispatch(setSuggestions("showing")); if ( features.tcf && extractVendorSource(newVendorId) === VendorSources.GVL ) { - dispatch(setSuggestions("showing")); dispatch(setLockedForGVL(true)); } else { dispatch(setLockedForGVL(false)); @@ -275,6 +299,7 @@ const SystemInformationForm = ({ ) : null} - ({ @@ -318,7 +342,7 @@ const SystemInformationForm = ({ } tooltip="Are there any tags to associate with this system?" isMulti - isDisabled={lockedForGVL} + disabled={lockedForGVL} /> diff --git a/clients/admin-ui/src/features/system/VendorSelector.tsx b/clients/admin-ui/src/features/system/VendorSelector.tsx index 090686631b..40a5c1ae2f 100644 --- a/clients/admin-ui/src/features/system/VendorSelector.tsx +++ b/clients/admin-ui/src/features/system/VendorSelector.tsx @@ -1,19 +1,47 @@ import { Flex, FormControl, HStack, Text, VStack } from "@fidesui/react"; -import { Select, SingleValue } from "chakra-react-select"; +import { + ActionMeta, + chakraComponents, + GroupBase, + OptionProps, + Select, + SingleValue, +} from "chakra-react-select"; import { useField, useFormikContext } from "formik"; import { useState } from "react"; import { ErrorMessage, Label, Option } from "~/features/common/form/inputs"; import QuestionTooltip from "~/features/common/QuestionTooltip"; import { DictOption } from "~/features/plus/plus.slice"; -import { DictSuggestionToggle } from "~/features/system/dictionary-form/ToggleDictSuggestions"; interface Props { disabled?: boolean; options: DictOption[]; - onVendorSelected: (vendorId: string) => void; + onVendorSelected: (vendorId: string | undefined) => void; } +const CustomDictOption: React.FC< + OptionProps> +> = ({ children, ...props }) => ( + + + + {props.data.label} + + + {props.data.description ? ( + + {props.data.description} + + ) : null} + + +); const VendorSelector = ({ disabled, options, onVendorSelected }: Props) => { const [initialField, meta, { setValue }] = useField({ name: "vendor_id" }); const isInvalid = !!(meta.touched && meta.error); @@ -26,6 +54,8 @@ const VendorSelector = ({ disabled, options, onVendorSelected }: Props) => { opt.label.toLowerCase().startsWith(searchParam.toLowerCase()) ); + const selected = options.find((o) => o.value === field.value); + const handleTabPressed = () => { if (suggestions.length > 0 && searchParam !== suggestions[0].label) { setSearchParam(suggestions[0].label); @@ -33,10 +63,16 @@ const VendorSelector = ({ disabled, options, onVendorSelected }: Props) => { } }; - const handleChange = (newValue: SingleValue