Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

System form/vendor selector fixes #4420

Merged
merged 11 commits into from
Nov 13, 2023
12 changes: 0 additions & 12 deletions clients/admin-ui/cypress/e2e/systems-plus.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -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) => {
Expand Down
60 changes: 42 additions & 18 deletions clients/admin-ui/src/features/system/SystemInformationForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import {
} from "~/features/common/custom-fields";
import { useFeatures } from "~/features/common/features/features.slice";
import {
CustomCreatableSelect,
CustomSelect,
CustomSwitch,
CustomTextInput,
Expand All @@ -29,6 +28,7 @@ import {
extractVendorSource,
getErrorMessage,
isErrorResult,
isFetchBaseQueryError,
VendorSources,
} from "~/features/common/helpers";
import { FormGuard } from "~/features/common/hooks/useIsAnyFormDirty";
Expand All @@ -43,6 +43,7 @@ import {
setSuggestions,
} from "~/features/system/dictionary-form/dict-suggestion.slice";
import {
DictSuggestionCreatableSelect,
DictSuggestionNumberInput,
DictSuggestionSelect,
DictSuggestionSwitch,
Expand Down Expand Up @@ -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
Expand All @@ -103,6 +99,8 @@ const SystemInformationForm = ({
withHeader,
children,
}: Props) => {
const systems = useAppSelector(selectAllSystems);

const dispatch = useAppDispatch();
const customFields = useCustomFields({
resourceType: ResourceTypes.SYSTEM,
Expand All @@ -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] =
Expand All @@ -139,7 +154,6 @@ const SystemInformationForm = ({
const dictionaryOptions = useAppSelector(selectAllDictEntries);
const lockedForGVL = useAppSelector(selectLockedForGVL);

const systems = useAppSelector(selectAllSystems);
const isEditing = useMemo(
() =>
Boolean(
Expand Down Expand Up @@ -167,16 +181,21 @@ const SystemInformationForm = ({
formikHelpers: FormikHelpers<FormValues>
) => {
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
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -275,6 +299,7 @@ const SystemInformationForm = ({
<VendorSelector
options={dictionaryOptions}
onVendorSelected={handleVendorSelected}
disabled={!!passedInSystem && lockedForGVL}
/>
) : null}
<DictSuggestionTextInput
Expand Down Expand Up @@ -303,11 +328,10 @@ const SystemInformationForm = ({
tooltip="What services does this system perform?"
disabled={lockedForGVL}
/>
<CustomCreatableSelect
<DictSuggestionCreatableSelect
id="tags"
name="tags"
label="System Tags"
variant="stacked"
options={
initialValues.tags
? initialValues.tags.map((s) => ({
Expand All @@ -318,7 +342,7 @@ const SystemInformationForm = ({
}
tooltip="Are there any tags to associate with this system?"
isMulti
isDisabled={lockedForGVL}
disabled={lockedForGVL}
/>
</SystemFormInputGroup>
<SystemFormInputGroup heading="Dataset reference">
Expand Down
51 changes: 44 additions & 7 deletions clients/admin-ui/src/features/system/VendorSelector.tsx
Original file line number Diff line number Diff line change
@@ -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<Option, false, GroupBase<Option>>
> = ({ children, ...props }) => (
<chakraComponents.Option {...props} type="option">
<Flex flexDirection="column" padding={2}>
<Text color="gray.700" fontSize="14px" lineHeight={5} fontWeight="medium">
{props.data.label}
</Text>

{props.data.description ? (
<Text
color="gray.500"
fontSize="12px"
lineHeight={4}
fontWeight="normal"
>
{props.data.description}
</Text>
) : null}
</Flex>
</chakraComponents.Option>
);
const VendorSelector = ({ disabled, options, onVendorSelected }: Props) => {
const [initialField, meta, { setValue }] = useField({ name: "vendor_id" });
const isInvalid = !!(meta.touched && meta.error);
Expand All @@ -26,17 +54,25 @@ 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);
setValue(suggestions[0].value);
}
};

const handleChange = (newValue: SingleValue<Option>) => {
const handleChange = (
newValue: SingleValue<Option>,
actionMeta: ActionMeta<Option>
) => {
if (newValue) {
setValue(newValue.value);
onVendorSelected(newValue.value);
} else if (actionMeta.action === "clear") {
setValue("");
onVendorSelected(undefined);
}
};

Expand All @@ -45,7 +81,7 @@ const VendorSelector = ({ disabled, options, onVendorSelected }: Props) => {
<FormControl isInvalid={isInvalid}>
<VStack alignItems="start" position="relative">
<Flex alignItems="center">
<Label htmlFor="vendor" fontSize="xs" my={0} mr={1}>
<Label htmlFor="vendor_id" fontSize="xs" my={0} mr={1}>
Vendor
</Label>
<QuestionTooltip label="Enter the vendor to associate with the system" />
Expand All @@ -57,8 +93,9 @@ const VendorSelector = ({ disabled, options, onVendorSelected }: Props) => {
>
<Select
options={suggestions}
value={selected}
onBlur={(e) => {
setTouched({ ...touched, test_vendor: true });
setTouched({ ...touched, vendor_id: true });
field.onBlur(e);
}}
onChange={handleChange}
Expand Down Expand Up @@ -102,6 +139,7 @@ const VendorSelector = ({ disabled, options, onVendorSelected }: Props) => {
display: "none",
}),
}}
components={{ Option: CustomDictOption }}
/>
<Text
aria-hidden
Expand Down Expand Up @@ -131,7 +169,6 @@ const VendorSelector = ({ disabled, options, onVendorSelected }: Props) => {
/>
</VStack>
</FormControl>
<DictSuggestionToggle />
</HStack>
);
};
Expand Down
Loading