From e0397b19e12b736a85b45b6eaae3de9dbce1c8c5 Mon Sep 17 00:00:00 2001 From: Jason Gill Date: Wed, 13 Nov 2024 14:38:34 -0700 Subject: [PATCH 01/25] remove obsolete components --- .../features/dataset/DataCategoryInput.tsx | 50 ++---- .../dataset/EditCollectionOrFieldForm.tsx | 37 +---- clients/admin-ui/src/features/plus/helpers.ts | 27 ---- .../features/system/DataFlowsAccordion.tsx | 44 ----- .../system/DataFlowsAccordionItem.tsx | 153 ------------------ .../ClassifiedDataCategoryDropdown.tsx | 117 -------------- .../index.ts | 1 - .../types.ts | 5 - clients/fidesui/src/index.ts | 1 - 9 files changed, 16 insertions(+), 419 deletions(-) delete mode 100644 clients/admin-ui/src/features/plus/helpers.ts delete mode 100644 clients/admin-ui/src/features/system/DataFlowsAccordion.tsx delete mode 100644 clients/admin-ui/src/features/system/DataFlowsAccordionItem.tsx delete mode 100644 clients/fidesui/src/components/classified-data-category-dropdown/ClassifiedDataCategoryDropdown.tsx delete mode 100644 clients/fidesui/src/components/classified-data-category-dropdown/index.ts delete mode 100644 clients/fidesui/src/components/classified-data-category-dropdown/types.ts diff --git a/clients/admin-ui/src/features/dataset/DataCategoryInput.tsx b/clients/admin-ui/src/features/dataset/DataCategoryInput.tsx index 5d06a73886..9ef15664f8 100644 --- a/clients/admin-ui/src/features/dataset/DataCategoryInput.tsx +++ b/clients/admin-ui/src/features/dataset/DataCategoryInput.tsx @@ -1,10 +1,4 @@ -import { - Box, - ClassifiedDataCategoryDropdown, - FormLabel, - Grid, - Stack, -} from "fidesui"; +import { Box, FormLabel, Grid, Stack } from "fidesui"; import { DataCategory } from "~/types/api"; @@ -14,7 +8,6 @@ import DataCategoryDropdown from "./DataCategoryDropdown"; export interface Props { dataCategories: DataCategory[]; - mostLikelyCategories?: DataCategory[]; checked: string[]; onChecked: (newChecked: string[]) => void; tooltip?: string; @@ -22,7 +15,6 @@ export interface Props { const DataCategoryInput = ({ dataCategories, - mostLikelyCategories, checked, onChecked, tooltip, @@ -38,43 +30,29 @@ const DataCategoryInput = ({ return ( Data Categories - {mostLikelyCategories ? ( + - - ) : ( - - - - - - - - - {sortedCheckedDataCategories.map((dc) => ( - { - handleRemoveDataCategory(dc); - }} - /> - ))} - + + {sortedCheckedDataCategories.map((dc) => ( + { + handleRemoveDataCategory(dc); + }} + /> + ))} - )} + ); }; diff --git a/clients/admin-ui/src/features/dataset/EditCollectionOrFieldForm.tsx b/clients/admin-ui/src/features/dataset/EditCollectionOrFieldForm.tsx index 3fd7df5905..852bfa74c9 100644 --- a/clients/admin-ui/src/features/dataset/EditCollectionOrFieldForm.tsx +++ b/clients/admin-ui/src/features/dataset/EditCollectionOrFieldForm.tsx @@ -1,12 +1,9 @@ import { Stack } from "fidesui"; import { Form, Formik } from "formik"; -import { useMemo, useState } from "react"; +import { useState } from "react"; import { useSelector } from "react-redux"; import { CustomTextInput } from "~/features/common/form/inputs"; -import { DataCategoryWithConfidence } from "~/features/dataset/types"; -import { initialDataCategories } from "~/features/plus/helpers"; -import { selectClassifyInstanceField } from "~/features/plus/plus.slice"; import { selectDataCategories } from "~/features/taxonomy/taxonomy.slice"; import { DatasetCollection, DatasetField } from "~/types/api"; @@ -42,37 +39,8 @@ const EditCollectionOrFieldForm = ({ (category) => category.active, ); - // This data is only relevant for editing a field. Maybe another reason to split the field/ - // collection cases into two components. - const classifyField = useSelector(selectClassifyInstanceField); - const mostLikelyCategories: DataCategoryWithConfidence[] | undefined = - useMemo(() => { - if (!(allEnabledDataCategories && classifyField)) { - return undefined; - } - - const dataCategoryMap = new Map( - allEnabledDataCategories.map((dc) => [dc.fides_key, dc]), - ); - return ( - classifyField.classifications?.map(({ label, score }) => { - const dc = dataCategoryMap.get(label); - - return { - fides_key: label, - confidence: score, - ...dc, - }; - }) ?? [] - ); - }, [allEnabledDataCategories, classifyField]); - const [checkedDataCategories, setCheckedDataCategories] = useState( - () => - initialDataCategories({ - dataCategories: initialValues.data_categories, - mostLikelyCategories, - }), + initialValues.data_categories || [], ); const descriptionTooltip = @@ -106,7 +74,6 @@ const EditCollectionOrFieldForm = ({ {showDataCategories && ( { - if (dataCategories?.length) { - return dataCategories; - } - - // If there are classifier suggestions, choose the highest-confidence option. - if (mostLikelyCategories?.length) { - const topCategory = mostLikelyCategories.reduce((maxCat, nextCat) => - (nextCat.confidence ?? 0) > (maxCat.confidence ?? 0) ? nextCat : maxCat, - ); - return [topCategory.fides_key]; - } - - return []; -}; diff --git a/clients/admin-ui/src/features/system/DataFlowsAccordion.tsx b/clients/admin-ui/src/features/system/DataFlowsAccordion.tsx deleted file mode 100644 index 98a5e40018..0000000000 --- a/clients/admin-ui/src/features/system/DataFlowsAccordion.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { Accordion, Stack, Text } from "fidesui"; - -import { ClassifySystem, DataFlow, System } from "~/types/api"; - -import DataFlowsAccordionItem from "./DataFlowsAccordionItem"; - -export interface IngressEgress { - ingress: DataFlow[]; - egress: DataFlow[]; -} - -const DataFlowsAccordion = ({ - system, - classificationInstance, -}: { - system: System; - classificationInstance?: ClassifySystem; -}) => { - const hasNoDataFlows = - (system.ingress == null || system.ingress.length === 0) && - (system.egress == null || system.egress.length === 0); - - if (hasNoDataFlows) { - return No data flows found on this system.; - } - - return ( - - {system.name} - - - - - - ); -}; - -export default DataFlowsAccordion; diff --git a/clients/admin-ui/src/features/system/DataFlowsAccordionItem.tsx b/clients/admin-ui/src/features/system/DataFlowsAccordionItem.tsx deleted file mode 100644 index 74c11bc746..0000000000 --- a/clients/admin-ui/src/features/system/DataFlowsAccordionItem.tsx +++ /dev/null @@ -1,153 +0,0 @@ -import { - AccordionButton, - AccordionIcon, - AccordionItem, - AccordionPanel, - Box, - ClassifiedDataCategoryDropdown, - Heading, - Text, -} from "fidesui"; -import { useFormikContext } from "formik"; -import { ReactNode } from "react"; - -import { useAppSelector } from "~/app/hooks"; -import { sentenceCase } from "~/features/common/utils"; -import { DataCategoryWithConfidence } from "~/features/dataset/types"; -import { initialDataCategories } from "~/features/plus/helpers"; -import { - selectDataCategories, - selectDataCategoriesMap, -} from "~/features/taxonomy"; -import { ClassifyDataFlow, DataFlow } from "~/types/api"; - -import type { IngressEgress } from "./DataFlowsAccordion"; - -interface Props { - classifyDataFlows?: ClassifyDataFlow[]; - type: "ingress" | "egress"; -} - -/** - * AccordionItem styling wrapper - */ -const AccordionItemContents = ({ - headingLevel, - title, - children, -}: { - headingLevel: "h2" | "h3"; - title: string; - children: ReactNode; -}) => ( - <> - - - - {title} - - - - - - {children} - - -); - -/** - * The individual accordion item for either an Ingress or Egress - */ -const DataFlowAccordionItem = ({ - flow, - index, - classifyDataFlows, - type, -}: { flow: DataFlow; index: number } & Props) => { - const { setFieldValue } = useFormikContext(); - const dataCategories = useAppSelector(selectDataCategories); - const dataCategoriesMap = useAppSelector(selectDataCategoriesMap); - - const handleChecked = (newChecked: string[]) => { - // Use formik's method of indexing into array fields to update the value - setFieldValue(`${type}[${index}].data_categories`, newChecked); - }; - const classifyDataFlow = classifyDataFlows?.find( - (cdf) => cdf.fides_key === flow.fides_key, - ); - - const mostLikelyCategories: DataCategoryWithConfidence[] = - classifyDataFlow?.classifications.map(({ label, score }) => ({ - ...dataCategoriesMap.get(label), - fides_key: label, - confidence: score, - })) ?? []; - const checked = initialDataCategories({ - dataCategories: flow.data_categories, - mostLikelyCategories, - }); - - return ( - - - {dataCategories ? ( - - - - ) : null} - - - ); -}; - -/** - * The entire list of Ingresses or Egresses, rendered as an AccordionItem - */ -const DataFlowsAccordionItem = ({ classifyDataFlows, type }: Props) => { - const { values } = useFormikContext(); - const flows = values[type]; - const typeLabel = { - // When type is `ingress` display `sources`, - ingress: "source", - // and when type is `egress` display `destinations` - egress: "destination", - }[type]; - - if (flows.length === 0) { - return No {type}es found.; - } - - return ( - - - {flows.map((flow, idx) => ( - - ))} - - - ); -}; - -export default DataFlowsAccordionItem; diff --git a/clients/fidesui/src/components/classified-data-category-dropdown/ClassifiedDataCategoryDropdown.tsx b/clients/fidesui/src/components/classified-data-category-dropdown/ClassifiedDataCategoryDropdown.tsx deleted file mode 100644 index 90a3ad2ec9..0000000000 --- a/clients/fidesui/src/components/classified-data-category-dropdown/ClassifiedDataCategoryDropdown.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import { MultiValue, Select } from "chakra-react-select"; -import { Box, ButtonProps } from "fidesui"; -import React, { useMemo } from "react"; - -import { DataCategoryDropdown } from "../data-category-dropdown"; -import { DataCategory } from "../types/api"; -import { DataCategoryWithConfidence } from "./types"; - -interface Props { - dataCategories: DataCategory[]; - mostLikelyCategories: DataCategoryWithConfidence[]; - checked: string[]; - onChecked: (newChecked: string[]) => void; -} - -export const ClassifiedDataCategoryDropdown = ({ - mostLikelyCategories, - onChecked, - checked, - dataCategories, -}: Props) => { - const menuButtonProps: ButtonProps = { - size: "sm", - colorScheme: "complimentary", - borderRadius: "6px 0px 0px 6px", - }; - - const mostLikelySorted = useMemo( - () => - mostLikelyCategories.sort((a, b) => { - if (a.confidence != null && b.confidence != null) { - return b.confidence - a.confidence; - } - if (a.confidence == null) { - return 1; - } - if (b.confidence == null) { - return -1; - } - return a.fides_key.localeCompare(b.fides_key); - }), - [mostLikelyCategories], - ); - - const options = useMemo( - () => - mostLikelySorted.map((c) => ({ - label: c.fides_key, - value: c.fides_key, - })), - [mostLikelySorted], - ); - - const selectedOptions = useMemo( - () => - checked.map((key) => ({ - label: key, - value: key, - })), - [checked], - ); - - const handleChange = ( - newValues: MultiValue<{ label: string; value: string }>, - ) => { - onChecked(newValues.map((o) => o.value)); - }; - - return ( - - - - - - ({ + label: _.upperFirst(action), + value: action, + }), + )} + onChange={(value) => { + form.setFieldValue(field.name, value); + }} + disabled={ + connectionOption.supported_actions.length === 1 + } + className="w-full" + /> {props.errors.enabled_actions as string} From e671ca3b33e3e9f5d6e1ddfa18ed5832b3b56259 Mon Sep 17 00:00:00 2001 From: Jason Gill Date: Wed, 13 Nov 2024 16:21:42 -0700 Subject: [PATCH 03/25] Update `colorPrimaryBg` as per designer request --- clients/admin-ui/src/theme/ant.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/clients/admin-ui/src/theme/ant.ts b/clients/admin-ui/src/theme/ant.ts index 9647c2b611..742640d365 100644 --- a/clients/admin-ui/src/theme/ant.ts +++ b/clients/admin-ui/src/theme/ant.ts @@ -29,7 +29,7 @@ export const antTheme: AntThemeConfig = { colorWarningBg: "#ffecc9", // custom override colorWarningBorder: "#ffdba1", // custom override colorSuccessBorder: palette.FIDESUI_SUCCESS, - colorPrimaryBg: palette.FIDESUI_SANDSTONE, + colorPrimaryBg: palette.FIDESUI_NEUTRAL_75, colorBorder: palette.FIDESUI_NEUTRAL_100, zIndexPopupBase: 1500, // supersede Chakra's modal z-index }, From 8abb19eeb18da65b2226ee5634e88cceb0efad60 Mon Sep 17 00:00:00 2001 From: Jason Gill Date: Thu, 14 Nov 2024 15:27:19 -0700 Subject: [PATCH 04/25] Replace disabled Select instances with Ant badges --- .../ConsentManagementModal.tsx | 183 +++++++----------- 1 file changed, 75 insertions(+), 108 deletions(-) diff --git a/clients/admin-ui/src/features/configure-consent/ConsentManagementModal.tsx b/clients/admin-ui/src/features/configure-consent/ConsentManagementModal.tsx index ba7eff95a3..a3705c407a 100644 --- a/clients/admin-ui/src/features/configure-consent/ConsentManagementModal.tsx +++ b/clients/admin-ui/src/features/configure-consent/ConsentManagementModal.tsx @@ -6,8 +6,11 @@ import { AccordionItem, AccordionPanel, AntButton as Button, + AntFlex as Flex, + AntSpace as Space, + AntTag as Tag, + AntTypography as Typography, Box, - Flex, Modal, ModalBody, ModalContent, @@ -18,15 +21,8 @@ import { Spinner, useDisclosure, } from "fidesui"; -import { FieldArray, Form, Formik } from "formik"; -import { - CustomCreatableSelect, - CustomTextInput, - Label, -} from "~/features/common/form/inputs"; import { useGetSystemPurposeSummaryQuery } from "~/features/plus/plus.slice"; -import { SystemPurposeSummary } from "~/types/api"; export const useConsentManagementModal = () => { const { isOpen, onOpen, onClose } = useDisclosure(); @@ -40,8 +36,6 @@ type Props = { fidesKey: string; }; -type FormValues = SystemPurposeSummary; - export const ConsentManagementModal = ({ isOpen, onClose, @@ -60,110 +54,83 @@ export const ConsentManagementModal = ({ > - Vendor + + {systemPurposeSummary ? systemPurposeSummary?.name : "Vendor"} + {isLoading ? ( - + ) : ( - - initialValues={ - systemPurposeSummary as unknown as SystemPurposeSummary - } - enableReinitialize - onSubmit={() => {}} - > - {({ values }) => ( -
- - - - {Object.entries(values?.purposes || {}).length > 0 ? ( - - ) : null} - ( - - {Object.entries(values.purposes).map( - ([purposeName], index: number) => ( - - {({ isExpanded }) => ( - <> - - - {purposeName} - - - - - - - - - - - )} - - ), + !!systemPurposeSummary && ( + <> + {Object.entries(systemPurposeSummary.purposes || {}).length > + 0 && Purposes} + + {Object.entries(systemPurposeSummary.purposes).map( + ([purposeName], index: number) => ( + + {({ isExpanded }) => ( + <> + + + {purposeName} + + + + + + + Data Categories + + + {systemPurposeSummary.purposes[ + purposeName + ].data_uses?.map((data_use) => ( + {data_use} + ))} + + + + + Data Categories + + + {systemPurposeSummary.purposes[ + purposeName + ].legal_bases?.map((legal_base) => ( + {legal_base} + ))} + + + + )} - - )} - /> - - - - - - )} - + + ), + )} + +
+ Features + + {systemPurposeSummary.features?.map((feature) => ( + {feature} + ))} + +
+
+ Data Categories + + {systemPurposeSummary.data_categories?.map((category) => ( + {category} + ))} + +
+ + ) )}
From 54fca9d62bfb17be3cb00fd3ce04b8ad17906585 Mon Sep 17 00:00:00 2001 From: Jason Gill Date: Fri, 15 Nov 2024 17:00:20 -0700 Subject: [PATCH 05/25] align fidesui prettier rules with other client projects --- clients/fidesui/.prettierrc.json | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 clients/fidesui/.prettierrc.json diff --git a/clients/fidesui/.prettierrc.json b/clients/fidesui/.prettierrc.json new file mode 100644 index 0000000000..805e2f196e --- /dev/null +++ b/clients/fidesui/.prettierrc.json @@ -0,0 +1,4 @@ +{ + "tabWidth": 2, + "endOfLine": "lf" +} From fde2d3e1423453afdf030bfee2a7c6bdecabe158 Mon Sep 17 00:00:00 2001 From: Jason Gill Date: Fri, 15 Nov 2024 18:05:14 -0700 Subject: [PATCH 06/25] enhanced HOC select component --- clients/fidesui/src/hoc/CustomSelect.tsx | 49 ++++++++++++++++++++---- clients/fidesui/src/index.ts | 4 ++ 2 files changed, 46 insertions(+), 7 deletions(-) diff --git a/clients/fidesui/src/hoc/CustomSelect.tsx b/clients/fidesui/src/hoc/CustomSelect.tsx index fe818f6ccd..ad5a999965 100644 --- a/clients/fidesui/src/hoc/CustomSelect.tsx +++ b/clients/fidesui/src/hoc/CustomSelect.tsx @@ -1,19 +1,54 @@ import { ChevronDown } from "@carbon/icons-react"; -import type { SelectProps } from "antd/lib"; -import { Select } from "antd/lib"; +import { Flex, Select, SelectProps, Typography } from "antd/lib"; import { BaseOptionType, DefaultOptionType } from "antd/lib/select"; import React from "react"; +const optionDescriptionRender = ( + option: DefaultOptionType | BaseOptionType, +) => { + const { label, description } = option.data; + if (!description) { + return option.label; + } + return ( + + {label} + {!!description && ( + + {description} + + )} + + ); +}; + /** * Higher-order component that adds a custom arrow icon to the Select component. */ -const withCustomArrowIcon = (WrappedComponent: typeof Select) => { - return function SpecifyCustomIcon< +const withCustomProps = (WrappedComponent: typeof Select) => { + const WrappedSelect = < ValueType = any, OptionType extends DefaultOptionType | BaseOptionType = DefaultOptionType, - >(props: SelectProps) { - return } {...props} />; + >({ + placeholder = "Select...", + optionRender = optionDescriptionRender, + className = "w-full", + suffixIcon = , + ...props + }: SelectProps) => { + const customProps = { + placeholder, + optionRender, + className, + suffixIcon, + ...props, + }; + return ; }; + return WrappedSelect; }; -export const CustomSelect = withCustomArrowIcon(Select); +export const CustomSelect = withCustomProps(Select); diff --git a/clients/fidesui/src/index.ts b/clients/fidesui/src/index.ts index ceb4407a1d..58d5abe067 100644 --- a/clients/fidesui/src/index.ts +++ b/clients/fidesui/src/index.ts @@ -31,6 +31,10 @@ export { Tooltip as AntTooltip, Typography as AntTypography, } from "antd/lib"; +export type { + BaseOptionType as AntBaseOptionType, + DefaultOptionType as AntDefaultOptionType, +} from "antd/lib/select"; // Higher-order components export { CustomSelect as AntSelect } from "./hoc"; From 8f0f25c0a6e9279dd412d7267aafddcc9b7ef66c Mon Sep 17 00:00:00 2001 From: Jason Gill Date: Fri, 15 Nov 2024 18:07:41 -0700 Subject: [PATCH 07/25] Create ControlledSelect.tsx --- .../features/common/form/ControlledSelect.tsx | 148 ++++++++++++++++++ 1 file changed, 148 insertions(+) create mode 100644 clients/admin-ui/src/features/common/form/ControlledSelect.tsx diff --git a/clients/admin-ui/src/features/common/form/ControlledSelect.tsx b/clients/admin-ui/src/features/common/form/ControlledSelect.tsx new file mode 100644 index 0000000000..5ddc00bb75 --- /dev/null +++ b/clients/admin-ui/src/features/common/form/ControlledSelect.tsx @@ -0,0 +1,148 @@ +import type { + AntDefaultOptionType as DefaultOptionType, + FormLabelProps, +} from "fidesui"; +import { + AntFlex as Flex, + AntSelect as Select, + AntSelectProps as SelectProps, + FormControl, + Grid, + VStack, +} from "fidesui"; +import { useField } from "formik"; +import { useState } from "react"; + +import QuestionTooltip from "../QuestionTooltip"; +import { ErrorMessage, Label } from "./inputs"; + +interface ControlledSelectProps extends SelectProps { + name: string; + label?: string; + labelProps?: FormLabelProps; + tooltip?: string | null; + isRequired?: boolean; + layout?: "inline" | "stacked"; +} + +export const ControlledSelect = ({ + name, + label, + labelProps, + tooltip, + isRequired, + layout = "inline", + ...props +}: ControlledSelectProps) => { + const [field, meta, { setValue }] = useField(name); + const isInvalid = !!(meta.touched && meta.error); + const [searchValue, setSearchValue] = useState(""); + + if (!field.value && (props.mode === "tags" || props.mode === "multiple")) { + field.value = []; + } + if (props.mode === "tags" && typeof field.value === "string") { + field.value = [field.value]; + } + + // Tags mode requires a custom option, everything else should just pass along the props or undefined + const optionRender = + props.mode === "tags" + ? (option: any, info: any) => { + if ( + option.value === searchValue && + !field.value.includes(searchValue) + ) { + return `Create "${searchValue}"`; + } + if (props.optionRender) { + return props.optionRender(option, info); + } + return option.label; + } + : props.optionRender || undefined; + + // this just supports the custom tag option, otherwise it's completely unnecessary + const handleSearch = (value: string) => { + setSearchValue(value); + if (props.onSearch) { + props.onSearch(value); + } + }; + + // Pass the value to the formik field + const handleChange = (newValue: any) => { + setValue(newValue); + }; + + if (layout === "inline") { + return ( + + + {label ? ( + + ) : null} + + + - - ({ @@ -130,11 +126,11 @@ export const PrivacyDeclarationFormComponents = ({ label: data.fides_key, }))} tooltip="What type of data is your system processing? This could be various types of user or system data." - isMulti - variant="stacked" - isDisabled + mode="multiple" + layout="stacked" + disabled /> - ({ @@ -142,28 +138,26 @@ export const PrivacyDeclarationFormComponents = ({ label: data.fides_key, }))} tooltip="Whose data are you processing? This could be customers, employees or any other type of user in your system." - isMulti - variant="stacked" - isDisabled + mode="multiple" + layout="stacked" + disabled /> {includeCookies ? ( - ) : null} {allDatasets ? ( - ) : null} {includeCustomFields ? ( diff --git a/clients/admin-ui/src/features/system/system-form-declaration-tab/PrivacyDeclarationForm.tsx b/clients/admin-ui/src/features/system/system-form-declaration-tab/PrivacyDeclarationForm.tsx index 4720eb2881..99807fe28e 100644 --- a/clients/admin-ui/src/features/system/system-form-declaration-tab/PrivacyDeclarationForm.tsx +++ b/clients/admin-ui/src/features/system/system-form-declaration-tab/PrivacyDeclarationForm.tsx @@ -20,12 +20,8 @@ import { CustomFieldValues, useCustomFields, } from "~/features/common/custom-fields"; -import { - CustomCreatableSelect, - CustomSelect, - CustomSwitch, - CustomTextInput, -} from "~/features/common/form/inputs"; +import { ControlledSelect } from "~/features/common/form/ControlledSelect"; +import { CustomSwitch, CustomTextInput } from "~/features/common/form/inputs"; import { FormGuard } from "~/features/common/hooks/useIsAnyFormDirty"; import { selectLockedForGVL } from "~/features/system/dictionary-form/dict-suggestion.slice"; import SystemFormInputGroup from "~/features/system/SystemFormInputGroup"; @@ -173,7 +169,7 @@ export const PrivacyDeclarationFormComponents = ({ disabled={isEditing || lockedForGVL} variant="stacked" /> - - ({ @@ -194,12 +190,12 @@ export const PrivacyDeclarationFormComponents = ({ label: data.fides_key, }))} tooltip="Which categories of personal data are collected for this purpose?" - isMulti + mode="multiple" isRequired - isDisabled={lockedForGVL} - variant="stacked" + disabled={lockedForGVL} + layout="stacked" /> - ({ @@ -207,26 +203,26 @@ export const PrivacyDeclarationFormComponents = ({ label: data.fides_key, }))} tooltip="Who are the subjects for this personal data?" - isMulti - isDisabled={lockedForGVL} - variant="stacked" + mode="multiple" + disabled={lockedForGVL} + layout="stacked" /> - {/* */} - - - @@ -300,14 +294,14 @@ export const PrivacyDeclarationFormComponents = ({ style={{ overflow: "visible" }} > - @@ -335,7 +329,7 @@ export const PrivacyDeclarationFormComponents = ({ variant="stacked" disabled={lockedForGVL} /> - ({ @@ -343,8 +337,8 @@ export const PrivacyDeclarationFormComponents = ({ label: c.fides_key, }))} tooltip="Which categories of personal data does this system share with third parties?" - variant="stacked" - isMulti + layout="stacked" + mode="multiple" disabled={lockedForGVL} /> @@ -352,7 +346,7 @@ export const PrivacyDeclarationFormComponents = ({ - ({ label: c.name, value: c.name })) : [] } - isMulti + mode="tags" tooltip="Which cookies are placed on consumer domains for this purpose?" - variant="stacked" - isDisabled={lockedForGVL} + layout="stacked" + disabled={lockedForGVL} /> {includeCustomFields ? ( diff --git a/clients/admin-ui/src/features/taxonomy/hooks.tsx b/clients/admin-ui/src/features/taxonomy/hooks.tsx index 093cd6ff06..37d0449606 100644 --- a/clients/admin-ui/src/features/taxonomy/hooks.tsx +++ b/clients/admin-ui/src/features/taxonomy/hooks.tsx @@ -16,7 +16,8 @@ import { } from "~/types/api"; import { YesNoOptions } from "../common/constants"; -import { CustomRadioGroup, CustomSelect } from "../common/form/inputs"; +import { ControlledSelect } from "../common/form/ControlledSelect"; +import { CustomRadioGroup } from "../common/form/inputs"; import { enumToOptions } from "../common/helpers"; import { useCreateDataSubjectMutation, @@ -434,20 +435,20 @@ export const useDataSubject = (): TaxonomyHookData => { const renderExtraFormFields = (entity: DataSubject) => ( <> - {/* @ts-ignore because of discrepancy between form and entity type, again */} - {entity.rights && entity.rights.length ? ( - - ) : null} + )} Date: Fri, 15 Nov 2024 18:20:17 -0700 Subject: [PATCH 09/25] clean up old Chakra selects & npm package --- clients/admin-ui/package.json | 1 - .../src/features/common/form/inputs.tsx | 557 +----------------- .../PrivacyExperienceForm.tsx | 7 +- .../src/features/system/VendorSelector.tsx | 2 +- clients/fidesui/package.json | 1 - clients/package-lock.json | 410 ------------- 6 files changed, 5 insertions(+), 973 deletions(-) diff --git a/clients/admin-ui/package.json b/clients/admin-ui/package.json index 9530159f46..9904c8428f 100644 --- a/clients/admin-ui/package.json +++ b/clients/admin-ui/package.json @@ -35,7 +35,6 @@ "@reduxjs/toolkit": "^1.9.3", "@tanstack/react-table": "^8.10.7", "@types/jest": "^29.5.12", - "chakra-react-select": "^3.3.7", "cytoscape": "^3.30.0", "cytoscape-klay": "^3.1.4", "date-fns": "^2.29.3", diff --git a/clients/admin-ui/src/features/common/form/inputs.tsx b/clients/admin-ui/src/features/common/form/inputs.tsx index 73b8d263ae..4ffc5a22fa 100644 --- a/clients/admin-ui/src/features/common/form/inputs.tsx +++ b/clients/admin-ui/src/features/common/form/inputs.tsx @@ -2,21 +2,9 @@ * Various common form inputs, styled specifically for Formik forms used throughout our app */ -import { - chakraComponents, - ChakraStylesConfig, - CreatableSelect, - GroupBase, - MenuPosition, - MultiValue, - OptionProps, - Select, - SelectComponentsConfig, - SingleValue, - Size, -} from "chakra-react-select"; import { AntButton as Button, + AntDefaultOptionType as DefaultOptionType, AntSwitch as Switch, Box, Checkbox, @@ -87,7 +75,6 @@ export interface CustomInputProps { // it just for the form. Therefore, we have our form components do the work of transforming // if the value they receive is undefined. export type StringField = FieldHookConfig; -type StringArrayField = FieldHookConfig; export const Label = ({ children, @@ -175,380 +162,13 @@ export const ErrorMessage = ({ ); }; -const ClearIndicator = () => null; - -export interface Option { +export interface Option extends DefaultOptionType { value: string; label: string; description?: string | null; tooltip?: string; } -const CustomOption = ({ - children, - ...props -}: OptionProps>) => ( - - - - {props.data.label} - - - {props.data.description ? ( - - {props.data.description} - - ) : null} - - -); - -export interface SelectProps { - label?: string; - labelProps?: FormLabelProps; - placeholder?: string; - tooltip?: string | null; - options?: Option[] | []; - isDisabled?: boolean; - isSearchable?: boolean; - isClearable?: boolean; - isRequired?: boolean; - size?: Size; - isMulti?: boolean; - variant?: Variant; - menuPosition?: MenuPosition; - /** - * If true, when isMulti=false, the selected value will be rendered as a block, - * similar to how the multi values are rendered - */ - singleValueBlock?: boolean; - isFormikOnChange?: boolean; - isCustomOption?: boolean; - textColor?: string; -} - -export const SELECT_STYLES: ChakraStylesConfig< - Option, - boolean, - GroupBase -
- - {...field} - id={props.id || name} - data-testid={`custom-select-${field.name}`} - {...props} - optionRender={optionRender} - onSearch={handleSearch} - onChange={handleChange} - value={field.value || undefined} // solves weird bug where placeholder won't appear if value is an empty string "" - /> -
+ { - if ( - option.value === searchValue && - !field.value.includes(searchValue) - ) { - return `Create "${searchValue}"`; - } - return option.label; - } - : undefined - } - value={field.value || []} - onChange={handleChange} - disabled={disabled} - data-testid={`input-${field.name}`} - onSearch={setSearchValue} - /> -
- - -
- ); -}; - export const DictSuggestionNumberInput = ({ label, tooltip, From e738a298cba3c5779cd4c8a8dacb8db72cf9992e Mon Sep 17 00:00:00 2001 From: Jason Gill Date: Mon, 18 Nov 2024 19:24:08 -0700 Subject: [PATCH 14/25] fix Cypress tests --- .../admin-ui/cypress/e2e/config-wizard.cy.ts | 2 +- .../cypress/e2e/consent-configuration.cy.ts | 12 ++--- .../admin-ui/cypress/e2e/custom-fields.cy.ts | 18 +++++--- .../cypress/e2e/integration-management.cy.ts | 25 +++++++---- .../cypress/e2e/privacy-experiences.cy.ts | 34 ++++++++++---- .../cypress/e2e/privacy-notices.cy.ts | 12 ++--- .../cypress/e2e/privacy-requests.cy.ts | 7 ++- .../admin-ui/cypress/e2e/systems-plus.cy.ts | 38 +++++++++------- .../admin-ui/cypress/e2e/taxonomy-plus.cy.ts | 41 +++++++---------- clients/admin-ui/cypress/e2e/taxonomy.cy.ts | 8 ++-- .../admin-ui/cypress/support/ant-support.ts | 44 +++++++++++++++++-- clients/admin-ui/cypress/support/commands.ts | 27 ++---------- .../common/custom-fields/CustomFieldsList.tsx | 38 +++++++--------- .../SubmitPrivacyRequestForm.tsx | 4 +- .../src/features/system/VendorSelector.tsx | 10 +++-- 15 files changed, 184 insertions(+), 136 deletions(-) diff --git a/clients/admin-ui/cypress/e2e/config-wizard.cy.ts b/clients/admin-ui/cypress/e2e/config-wizard.cy.ts index e933cea1aa..430aace75a 100644 --- a/clients/admin-ui/cypress/e2e/config-wizard.cy.ts +++ b/clients/admin-ui/cypress/e2e/config-wizard.cy.ts @@ -23,7 +23,7 @@ describe("Config Wizard", () => { cy.getByTestId("authenticate-aws-form"); cy.getByTestId("input-aws_access_key_id").type("fakeAccessKey"); cy.getByTestId("input-aws_secret_access_key").type("fakeSecretAccessKey"); - cy.getByTestId("input-region_name").type("us-east-1{Enter}"); + cy.getByTestId("controlled-select-region_name").type("us-east-1{Enter}"); }); it("Allows submitting the form and reviewing the results", () => { diff --git a/clients/admin-ui/cypress/e2e/consent-configuration.cy.ts b/clients/admin-ui/cypress/e2e/consent-configuration.cy.ts index 56838c4781..ae3f9d72fa 100644 --- a/clients/admin-ui/cypress/e2e/consent-configuration.cy.ts +++ b/clients/admin-ui/cypress/e2e/consent-configuration.cy.ts @@ -249,11 +249,11 @@ describe("Consent configuration", () => { cy.getByTestId("add-vendor-btn").click(); cy.getByTestId("input-name").type("Aniview LTD{enter}"); cy.wait("@getDictionaryDeclarations"); - cy.getSelectValueContainer( - "input-privacy_declarations.0.consent_use", + cy.getByTestId( + "controlled-select-privacy_declarations.0.consent_use", ).contains("Marketing"); - cy.getSelectValueContainer( - "input-privacy_declarations.0.data_use", + cy.getByTestId( + "controlled-select-privacy_declarations.0.data_use", ).contains("Profiling for Advertising"); ["av_*", "aniC", "2_C_*"].forEach((cookieName) => { cy.getByTestId("input-privacy_declarations.0.cookieNames").contains( @@ -262,8 +262,8 @@ describe("Consent configuration", () => { }); // Also check one that shouldn't have any cookies - cy.getSelectValueContainer( - "input-privacy_declarations.1.data_use", + cy.getByTestId( + "controlled-select-privacy_declarations.1.data_use", ).contains("Analytics for Insights"); cy.getByTestId("input-privacy_declarations.1.cookieNames").contains( "Select...", diff --git a/clients/admin-ui/cypress/e2e/custom-fields.cy.ts b/clients/admin-ui/cypress/e2e/custom-fields.cy.ts index bb19d51a33..137ddd178c 100644 --- a/clients/admin-ui/cypress/e2e/custom-fields.cy.ts +++ b/clients/admin-ui/cypress/e2e/custom-fields.cy.ts @@ -315,12 +315,12 @@ describe("Custom Fields", () => { "have.value", "Description!!", ); - cy.getSelectValueContainer("input-resource_type").contains( + cy.getByTestId("controlled-select-resource_type").contains( "taxonomy:data category", ); // Configuration - cy.getSelectValueContainer("input-field_type").contains( + cy.getByTestId("controlled-select-field_type").contains( "Single select", ); cy.getByTestId("custom-input-allow_list.allowed_values[0]").should( @@ -336,7 +336,9 @@ describe("Custom Fields", () => { it("can edit field information", () => { const newDescription = "new description"; cy.getByTestId("custom-input-description").clear().type(newDescription); - cy.selectOption("input-field_type", "Multiple select"); + cy.getByTestId("controlled-select-field_type").antSelect( + "Multiple select", + ); cy.getByTestId("save-btn").click(); cy.wait("@putCustomFieldDefinition").then((interception) => { const { body } = interception.request; @@ -399,7 +401,9 @@ describe("Custom Fields", () => { // Configuration const allowList = ["snorlax", "eevee"]; - cy.selectOption("input-field_type", "Single select"); + cy.getByTestId("controlled-select-field_type").antSelect( + "Single select", + ); allowList.forEach((item, idx) => { cy.getByTestId("add-list-value-btn").click(); cy.getByTestId(`custom-input-allow_list.allowed_values[${idx}]`).type( @@ -428,10 +432,12 @@ describe("Custom Fields", () => { // Field info cy.getByTestId("custom-input-name").type(payload.name); cy.getByTestId("custom-input-description").type(payload.description); - cy.selectOption("input-resource_type", "taxonomy:data category"); + cy.getByTestId("controlled-select-resource_type").antSelect( + "taxonomy:data category", + ); // Configuration - cy.selectOption("input-field_type", "Open Text"); + cy.getByTestId("controlled-select-field_type").antSelect("Open Text"); cy.getByTestId("save-btn").click(); cy.wait("@postCustomFieldDefinition").then((interception) => { diff --git a/clients/admin-ui/cypress/e2e/integration-management.cy.ts b/clients/admin-ui/cypress/e2e/integration-management.cy.ts index c6a58e8188..37ab892075 100644 --- a/clients/admin-ui/cypress/e2e/integration-management.cy.ts +++ b/clients/admin-ui/cypress/e2e/integration-management.cy.ts @@ -157,7 +157,9 @@ describe("Integration management for data detection & discovery", () => { parseSpecialCharSequences: false, }, ); - cy.selectOption("input-system_fides_key", "Fidesctl System"); + cy.getByTestId("controlled-select-system_fides_key").antSelect( + "Fidesctl System", + ); cy.getByTestId("save-btn").click(); cy.wait("@patchSystemConnection"); }); @@ -279,7 +281,9 @@ describe("Integration management for data detection & discovery", () => { cy.getByTestId("add-monitor-btn").click(); cy.getByTestId("add-modal-content").should("be.visible"); cy.getByTestId("input-name").type("A new monitor"); - cy.selectOption("input-execution_frequency", "Daily"); + cy.getByTestId("controlled-select-execution_frequency").antSelect( + "Daily", + ); cy.getByTestId("input-execution_start_date").type("2034-06-03T10:00"); cy.getByTestId("next-btn").click(); cy.wait("@getDatabasesPage1"); @@ -298,7 +302,9 @@ describe("Integration management for data detection & discovery", () => { cy.getByTestId("add-monitor-btn").click(); cy.getByTestId("add-modal-content").should("be.visible"); cy.getByTestId("input-name").type("A new monitor"); - cy.selectOption("input-execution_frequency", "Daily"); + cy.getByTestId("controlled-select-execution_frequency").antSelect( + "Daily", + ); cy.getByTestId("input-execution_start_date").type("2034-06-03T10:00"); cy.getByTestId("next-btn").click(); cy.wait("@getDatabasesPage1"); @@ -321,7 +327,9 @@ describe("Integration management for data detection & discovery", () => { cy.getByTestId("add-monitor-btn").click(); cy.getByTestId("add-modal-content").should("be.visible"); cy.getByTestId("input-name").type("A new monitor"); - cy.selectOption("input-execution_frequency", "Daily"); + cy.getByTestId("controlled-select-execution_frequency").antSelect( + "Daily", + ); cy.getByTestId("input-execution_start_date").type("2034-06-03T10:00"); cy.getByTestId("next-btn").click(); cy.wait("@getDatabasesPage1"); @@ -367,7 +375,7 @@ describe("Integration management for data detection & discovery", () => { it("can edit an existing monitor by clicking the table row", () => { cy.getByTestId("row-test monitor 2").click(); cy.getByTestId("input-name").should("have.value", "test monitor 2"); - cy.getSelectValueContainer("input-execution_frequency").should( + cy.getByTestId("controlled-select-execution_frequency").should( "contain", "Weekly", ); @@ -406,12 +414,11 @@ describe("Integration management for data detection & discovery", () => { cy.intercept("GET", "/api/v1/plus/discovery-monitor*", { fixture: "detection-discovery/monitors/monitor_list.json", }).as("getMonitors"); - cy.intercept("/api/v1/plus/discovery-monitor/databases", { + cy.intercept("POST", "/api/v1/plus/discovery-monitor/databases", { fixture: "empty-pagination.json", }).as("getEmptyDatabases"); cy.getByTestId("tab-Data discovery").click(); cy.wait("@getMonitors"); - cy.clock(new Date(2034, 5, 3)); }); it("skips the project/database selection step", () => { @@ -420,7 +427,9 @@ describe("Integration management for data detection & discovery", () => { }).as("putMonitor"); cy.getByTestId("add-monitor-btn").click(); cy.getByTestId("input-name").type("A new monitor"); - cy.selectOption("input-execution_frequency", "Daily"); + cy.getByTestId("controlled-select-execution_frequency").antSelect( + "Daily", + ); cy.getByTestId("input-execution_start_date").type("2034-06-03T10:00"); cy.getByTestId("next-btn").click(); cy.wait("@putMonitor"); diff --git a/clients/admin-ui/cypress/e2e/privacy-experiences.cy.ts b/clients/admin-ui/cypress/e2e/privacy-experiences.cy.ts index 1df388b341..3a6c2c6c7b 100644 --- a/clients/admin-ui/cypress/e2e/privacy-experiences.cy.ts +++ b/clients/admin-ui/cypress/e2e/privacy-experiences.cy.ts @@ -186,7 +186,9 @@ describe("Privacy experiences", () => { it("can create an experience", () => { cy.getByTestId("input-name").type("Test experience name"); - cy.selectOption("input-component", "Banner and modal"); + cy.getByTestId("controlled-select-component").antSelect( + "Banner and modal", + ); cy.getByTestId("add-privacy-notice").click(); cy.getByTestId("select-privacy-notice").antSelect(0); cy.getByTestId("add-location").click(); @@ -230,13 +232,20 @@ describe("Privacy experiences", () => { }); it("doesn't allow component type to be changed after selection", () => { - cy.selectOption("input-component", "Banner and modal"); - cy.getByTestId("input-component").find("input").should("be.disabled"); + cy.getByTestId("controlled-select-component").antSelect( + "Banner and modal", + ); + cy.getByTestId("controlled-select-component").should( + "have.class", + "ant-select-disabled", + ); cy.getByTestId("input-dismissable").should("be.visible"); }); it("doesn't show a preview for a privacy center", () => { - cy.selectOption("input-component", "Privacy center"); + cy.getByTestId("controlled-select-component").antSelect( + "Privacy center", + ); cy.getByTestId("input-dismissable").should("not.be.visible"); cy.getByTestId("no-preview-notice").contains( "Privacy center preview not available", @@ -244,7 +253,9 @@ describe("Privacy experiences", () => { }); it("doesn't show preview until privacy notice is added", () => { - cy.selectOption("input-component", "Banner and modal"); + cy.getByTestId("controlled-select-component").antSelect( + "Banner and modal", + ); cy.getByTestId("no-preview-notice").contains( "No privacy notices added", ); @@ -256,7 +267,9 @@ describe("Privacy experiences", () => { it("shows option to display privacy notices in banner and updates preview when clicked", () => { cy.getByTestId("input-show_layer1_notices").should("not.be.visible"); - cy.selectOption("input-component", "Banner and modal"); + cy.getByTestId("controlled-select-component").antSelect( + "Banner and modal", + ); cy.getByTestId("add-privacy-notice").click(); cy.getByTestId("select-privacy-notice").antSelect(0); cy.getByTestId("input-show_layer1_notices").click(); @@ -267,7 +280,9 @@ describe("Privacy experiences", () => { }); it("allows editing experience text and shows updated text in the preview", () => { - cy.selectOption("input-component", "Banner and modal"); + cy.getByTestId("controlled-select-component").antSelect( + "Banner and modal", + ); cy.getByTestId("add-privacy-notice").click(); cy.getByTestId("select-privacy-notice").antSelect(0); cy.getByTestId("edit-experience-btn").click(); @@ -288,7 +303,10 @@ describe("Privacy experiences", () => { it("populates the form and shows the preview with the existing values", () => { cy.wait("@getExperienceDetail"); - cy.getByTestId("input-component").find("input").should("be.disabled"); + cy.getByTestId("controlled-select-component").should( + "have.class", + "ant-select-disabled", + ); cy.getByTestId("input-name").should( "have.value", "Example modal experience", diff --git a/clients/admin-ui/cypress/e2e/privacy-notices.cy.ts b/clients/admin-ui/cypress/e2e/privacy-notices.cy.ts index 53950f0d7b..49eaaa27a4 100644 --- a/clients/admin-ui/cypress/e2e/privacy-notices.cy.ts +++ b/clients/admin-ui/cypress/e2e/privacy-notices.cy.ts @@ -238,7 +238,7 @@ describe("Privacy notices", () => { cy.getByTestId("input-name").should("have.value", notice.name); // consent mechanism section - cy.getSelectValueContainer("input-consent_mechanism").contains( + cy.getByTestId("controlled-select-consent_mechanism").contains( "Notice only", ); @@ -250,11 +250,11 @@ describe("Privacy notices", () => { // configuration section notice.data_uses.forEach((dataUse) => { - cy.getSelectValueContainer("input-data_uses").contains(dataUse); + cy.getByTestId("controlled-select-data_uses").contains(dataUse); }); // enforcement level - cy.getSelectValueContainer("input-enforcement_level").contains( + cy.getByTestId("controlled-select-enforcement_level").contains( "Not applicable", ); @@ -368,11 +368,13 @@ describe("Privacy notices", () => { cy.getByTestId("input-name").type(notice.name); // consent mechanism section - cy.selectOption("input-consent_mechanism", "Opt in"); + cy.getByTestId("controlled-select-consent_mechanism").antSelect("Opt in"); cy.getByTestId("input-has_gpc_flag").click(); // configuration section - cy.selectOption("input-data_uses", notice.data_uses[0]); + cy.getByTestId("controlled-select-data_uses").antSelect( + notice.data_uses[0], + ); // translations cy.getByTestId("input-translations.0.title").type("Title"); diff --git a/clients/admin-ui/cypress/e2e/privacy-requests.cy.ts b/clients/admin-ui/cypress/e2e/privacy-requests.cy.ts index 1a3e835c4e..47ad8341c5 100644 --- a/clients/admin-ui/cypress/e2e/privacy-requests.cy.ts +++ b/clients/admin-ui/cypress/e2e/privacy-requests.cy.ts @@ -274,7 +274,10 @@ describe("Privacy Requests", () => { it("shows configured fields and values", () => { cy.getByTestId("submit-request-btn").click(); cy.wait("@getPrivacyCenterConfig"); - cy.getSelectValueContainer("input-policy_key").type("a{enter}"); + + cy.getByTestId("controlled-select-policy_key").antSelect( + "Access your data", + ); cy.getByTestId("input-identity.phone").should("not.exist"); cy.getByTestId("input-identity.email").should("exist"); cy.getByTestId( @@ -292,7 +295,7 @@ describe("Privacy Requests", () => { it("can submit a privacy request", () => { cy.getByTestId("submit-request-btn").click(); cy.wait("@getPrivacyCenterConfig"); - cy.getSelectValueContainer("input-policy_key").type("a{enter}"); + cy.getByTestId("controlled-select-policy_key").type("a{enter}"); cy.getByTestId("input-identity.email").type("email@ethyca.com"); cy.getByTestId( "input-custom_privacy_request_fields.required_field.value", diff --git a/clients/admin-ui/cypress/e2e/systems-plus.cy.ts b/clients/admin-ui/cypress/e2e/systems-plus.cy.ts index 13ca61d19b..62e30278b2 100644 --- a/clients/admin-ui/cypress/e2e/systems-plus.cy.ts +++ b/clients/admin-ui/cypress/e2e/systems-plus.cy.ts @@ -51,29 +51,31 @@ describe("System management with Plus features", () => { }); it("can display the vendor list dropdown", () => { - cy.getSelectContainer("input-name"); + cy.getByTestId("vendor-name-select"); }); it("contains type ahead dictionary entries", () => { - cy.getSelectContainer("input-name").type("A"); + cy.getByTestId("vendor-name-select").find("input").type("A"); cy.get(".ant-select-item").eq(0).contains("Aniview LTD"); cy.get(".ant-select-item").eq(1).contains("Anzu Virtual Reality LTD"); }); it("can reset suggestions by clearing vendor input", () => { - cy.getSelectContainer("input-name").type("L{enter}"); + cy.getByTestId("vendor-name-select").find("input").type("L"); + cy.antSelectDropdownVisible(); + cy.getByTestId("vendor-name-select").realPress("Enter"); cy.getByTestId("input-legal_name").should("have.value", "LINE"); cy.getByTestId("clear-btn").click(); cy.getByTestId("input-legal_name").should("be.empty"); }); it("can't refresh suggestions immediately after populating", () => { - cy.getSelectContainer("input-name").type("A{enter}"); + cy.getByTestId("vendor-name-select").find("input").type("A{enter}"); cy.getByTestId("refresh-suggestions-btn").should("be.disabled"); }); it("can refresh suggestions when editing a saved system", () => { - cy.getSelectContainer("input-name").type("A{enter}"); + cy.getByTestId("vendor-name-select").find("input").type("A{enter}"); cy.fixture("systems/dictionary-system.json").then((dictSystem) => { cy.fixture("systems/system.json").then((origSystem) => { cy.intercept( @@ -99,7 +101,7 @@ describe("System management with Plus features", () => { // the form to be mistakenly marked as dirty and the "unsaved changes" // modal to pop up incorrectly when switching tabs it("can switch between tabs after populating from dictionary", () => { - cy.getSelectContainer("input-name").type("Anzu{enter}"); + cy.getByTestId("vendor-name-select").find("input").type("Anzu{enter}"); // 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) => { @@ -127,13 +129,13 @@ describe("System management with Plus features", () => { }); it("locks editing for a GVL vendor when TCF is enabled", () => { - cy.getSelectContainer("input-name").type("Aniview{enter}"); + cy.getByTestId("vendor-name-select").find("input").type("Aniview{enter}"); cy.getByTestId("locked-for-GVL-notice"); cy.getByTestId("input-description").should("be.disabled"); }); it("does not allow changes to data uses when locked", () => { - cy.getSelectContainer("input-name").type("Aniview{enter}"); + cy.getByTestId("vendor-name-select").find("input").type("Aniview{enter}"); cy.getByTestId("save-btn").click(); cy.wait(["@postSystem", "@getSystem", "@getSystems"]); cy.getByTestId("tab-Data uses").click(); @@ -144,7 +146,7 @@ describe("System management with Plus features", () => { }); it("does not lock editing for a non-GVL vendor", () => { - cy.getSelectContainer("input-name").type("L{enter}"); + cy.getByTestId("vendor-name-select").find("input").type("L{enter}"); cy.getByTestId("locked-for-GVL-notice").should("not.exist"); cy.getByTestId("input-description").should("not.be.disabled"); }); @@ -181,20 +183,22 @@ describe("System management with Plus features", () => { }); it("allows changes to data uses for non-GVL vendors", () => { - cy.getSelectContainer("input-name").type("L{enter}"); + cy.getByTestId("vendor-name-select").find("input").type("L"); + cy.antSelectDropdownVisible(); + cy.getByTestId("vendor-name-select").realPress("Enter"); cy.getByTestId("save-btn").click(); cy.wait(["@postSystem", "@getSystem", "@getSystems"]); cy.getByTestId("tab-Data uses").click(); cy.getByTestId("add-btn"); cy.getByTestId("delete-btn"); cy.getByTestId("row-functional.service.improve").click(); - cy.getByTestId("input-data_categories") + cy.getByTestId("controlled-select-data_categories") .find("input") .should("not.be.disabled"); }); it("don't allow editing declaration name after creation", () => { - cy.getSelectContainer("input-name").type("L{enter}"); + cy.getByTestId("vendor-name-select").find("input").type("L{enter}"); cy.getByTestId("save-btn").click(); cy.wait(["@postSystem", "@getSystem", "@getSystems"]); cy.getByTestId("tab-Data uses").click(); @@ -203,12 +207,16 @@ describe("System management with Plus features", () => { }); it("don't allow editing data uses after creation", () => { - cy.getSelectContainer("input-name").type("L{enter}"); + cy.getByTestId("vendor-name-select").find("input").type("L"); + cy.antSelectDropdownVisible(); + cy.getByTestId("vendor-name-select").realPress("Enter"); cy.getByTestId("save-btn").click(); cy.wait(["@postSystem", "@getSystem", "@getSystems"]); cy.getByTestId("tab-Data uses").click(); cy.getByTestId("row-functional.service.improve").click(); - cy.getByTestId("input-data_use").find("input").should("be.disabled"); + cy.getByTestId("controlled-select-data_use") + .find("input") + .should("be.disabled"); }); }); @@ -254,7 +262,7 @@ describe("System management with Plus features", () => { // Should not be able to save while form is untouched cy.getByTestId("save-btn").should("be.disabled"); const testId = - "input-customFieldValues.id-custom-field-definition-pokemon-party"; + "controlled-select-customFieldValues.id-custom-field-definition-pokemon-party"; cy.getByTestId(testId).contains("Charmander"); cy.getByTestId(testId).contains("Eevee"); cy.getByTestId(testId).contains("Snorlax"); diff --git a/clients/admin-ui/cypress/e2e/taxonomy-plus.cy.ts b/clients/admin-ui/cypress/e2e/taxonomy-plus.cy.ts index 063dd1653b..9079431a47 100644 --- a/clients/admin-ui/cypress/e2e/taxonomy-plus.cy.ts +++ b/clients/admin-ui/cypress/e2e/taxonomy-plus.cy.ts @@ -129,8 +129,8 @@ describe("Taxonomy management with Plus features", () => { it("can create a multi-select custom field", () => { cy.getByTestId("create-custom-fields-form").within(() => { cy.getByTestId("custom-input-name").type("Multi-select"); - cy.selectOption("input-field_type", "Multiple select"); - cy.selectOption("input-allow_list_id", "Prime numbers"); + cy.getByTestId("input-field_type").antSelect("Multiple select"); + cy.getByTestId("input-allow_list_id").antSelect("Prime numbers"); }); cy.intercept( @@ -204,43 +204,34 @@ describe("Taxonomy management with Plus features", () => { }); const testIdSingle = - "input-customFieldValues.id-custom-field-definition-starter-pokemon"; + "controlled-select-customFieldValues.id-custom-field-definition-starter-pokemon"; const testIdMulti = - "input-customFieldValues.id-custom-field-definition-pokemon-party"; + "controlled-select-customFieldValues.id-custom-field-definition-pokemon-party"; it("initializes form fields with values returned by the API", () => { cy.getByTestId("custom-fields-list"); - cy.getSelectValueContainer(testIdSingle).contains("Squirtle"); + cy.getByTestId(testIdSingle).contains("Squirtle"); ["Charmander", "Eevee", "Snorlax"].forEach((value) => { - cy.getSelectValueContainer(testIdMulti).contains(value); + cy.getByTestId(testIdMulti).contains(value); }); }); it("allows choosing and changing selections", () => { cy.getByTestId("custom-fields-list"); - cy.clearSingleValue(testIdSingle); - cy.selectOption(testIdSingle, "Snorlax"); - cy.getSelectValueContainer(testIdSingle).contains("Snorlax"); - cy.clearSingleValue(testIdSingle); - - cy.removeMultiValue(testIdMulti, "Eevee"); - cy.removeMultiValue(testIdMulti, "Snorlax"); - - // clicking directly on the select element as we usually do hits the - // "remove" on the Charmander tag, so force it to find the dropdown - // indicator instead - cy.getByTestId(testIdMulti) - .find(".custom-select__dropdown-indicator") - .click(); - cy.getByTestId(testIdMulti) - .find(".custom-select__menu-list") - .contains("Eevee") - .click(); + cy.getByTestId(testIdSingle).antClearSelect(); + cy.getByTestId(testIdSingle).antSelect("Snorlax"); + cy.getByTestId(testIdSingle).contains("Snorlax"); + cy.getByTestId(testIdSingle).antClearSelect(); + + cy.getByTestId(testIdMulti).antRemoveSelectTag("Eevee"); + cy.getByTestId(testIdMulti).antRemoveSelectTag("Snorlax"); + + cy.getByTestId(testIdMulti).antSelect("Eevee"); ["Charmander", "Eevee"].forEach((value) => { - cy.getSelectValueContainer(testIdMulti).contains(value); + cy.getByTestId(testIdMulti).contains(value); }); cy.intercept("POST", `/api/v1/plus/custom-metadata/custom-field/bulk`, { diff --git a/clients/admin-ui/cypress/e2e/taxonomy.cy.ts b/clients/admin-ui/cypress/e2e/taxonomy.cy.ts index a5d5aae968..40e433586d 100644 --- a/clients/admin-ui/cypress/e2e/taxonomy.cy.ts +++ b/clients/admin-ui/cypress/e2e/taxonomy.cy.ts @@ -220,9 +220,9 @@ describe("Taxonomy management page", () => { "Object", ]; rightValues.forEach((v) => { - cy.getByTestId("input-rights").should("contain", v); + cy.getByTestId("controlled-select-rights").should("contain", v); }); - cy.getByTestId("input-strategy").should("contain", "INCLUDE"); + cy.getByTestId("controlled-select-strategy").should("contain", "INCLUDE"); cy.getByTestId("input-automatic_decisions_or_profiling").within(() => { cy.getByTestId("option-true").should("have.attr", "data-checked"); // For some reason Cypress can accidentally click the dropdown selector above, @@ -256,8 +256,8 @@ describe("Taxonomy management page", () => { // check an entity that has no optional fields filled in cy.getByTestId("item-Anonymous User").trigger("mouseover"); cy.getByTestId("edit-btn").click(); - cy.getByTestId("input-rights").should("contain", "Select..."); - cy.getByTestId("input-strategy").should("not.exist"); + cy.getByTestId("controlled-select-rights").should("contain", "Select..."); + cy.getByTestId("controlled-select-strategy").should("not.exist"); }); it("Can trigger an error", () => { diff --git a/clients/admin-ui/cypress/support/ant-support.ts b/clients/admin-ui/cypress/support/ant-support.ts index f9f0569b62..9414ede492 100644 --- a/clients/admin-ui/cypress/support/ant-support.ts +++ b/clients/admin-ui/cypress/support/ant-support.ts @@ -16,14 +16,28 @@ declare global { * Clear all options from an Ant Design Select component */ antClearSelect: () => void; + /** + * Remove a tag (or selected option in mode="multiple") from an Ant Design Select component + */ + antRemoveSelectTag: (option: string) => void; + /** + * Ant Desitn Select component dropdown is visible + */ + antSelectDropdownVisible: () => void; } } } Cypress.Commands.add("getAntSelectOption", (option: string | number) => typeof option === "string" - ? cy.get(`.ant-select-item-option[title="${option}"]`) - : cy.get(`.ant-select-item-option`).eq(option), + ? cy.get( + `.ant-select-dropdown:not(.ant-select-dropdown-hidden) .ant-select-item-option[title="${option}"]`, + ) + : cy + .get( + `.ant-select-dropdown:not(.ant-select-dropdown-hidden) .ant-select-item-option`, + ) + .eq(option), ); Cypress.Commands.add( @@ -41,9 +55,14 @@ Cypress.Commands.add( throw new Error("Cannot select from a disabled Ant Select component"); } if (!classes.includes("ant-select-open")) { - cy.get(subject.selector).first().click(clickOptions); + if (classes.includes("ant-select-multiple")) { + cy.get(subject.selector).first().find("input").click(); + } else { + cy.get(subject.selector).first().click(clickOptions); + } } }); + cy.antSelectDropdownVisible(); cy.getAntSelectOption(option).should("be.visible").click(clickOptions); }, ); @@ -59,4 +78,23 @@ Cypress.Commands.add( }, ); +Cypress.Commands.add( + "antRemoveSelectTag", + { + prevSubject: "element", + }, + (subject, option) => { + cy.get(subject.selector) + .find(`.ant-select-selection-item[title="${option}"]`) + .find(".ant-select-selection-item-remove") + .click(); + }, +); + +Cypress.Commands.add("antSelectDropdownVisible", () => { + cy.get(".ant-select-dropdown:not(.ant-select-dropdown-hidden)").should( + "be.visible", + ); +}); + export {}; diff --git a/clients/admin-ui/cypress/support/commands.ts b/clients/admin-ui/cypress/support/commands.ts index a86243f456..fea018b9e4 100644 --- a/clients/admin-ui/cypress/support/commands.ts +++ b/clients/admin-ui/cypress/support/commands.ts @@ -36,14 +36,6 @@ Cypress.Commands.add("login", () => { const getSelectOptionList = (selectorId: string) => cy.getByTestId(selectorId).click().find(`.custom-select__menu-list`); -/** @deprecated */ -Cypress.Commands.add("getSelectValueContainer", (selectorId) => - cy.getByTestId(selectorId).find(`.custom-select__value-container`), -); -Cypress.Commands.add("getSelectContainer", (selectorId) => - cy.getByTestId(selectorId).find(`.ant-select`), -); - Cypress.Commands.add("selectOption", (selectorId, optionText) => { getSelectOptionList(selectorId).contains(optionText).click(); }); @@ -52,7 +44,7 @@ Cypress.Commands.add( "removeMultiValue", (selectorId: string, optionText: string) => cy - .getSelectValueContainer(selectorId) + .getByTestId(selectorId) .contains(optionText) .siblings(".custom-select__multi-value__remove") .click(), @@ -130,22 +122,9 @@ declare global { */ assumeRole(role: RoleRegistryEnum): void; /** - * @deprecated only use for legacy Chakra UI Custom Select components - * Get the container of a CustomSelect - * @example cy.getSelectValueContainer("input-allow_list_id") - */ - getSelectValueContainer( - selectorId: string, - ): Chainable>; - /** - * Get the container of an Ant Select - * @example cy.getSelectContainer("input-allow_list_id") - */ - getSelectContainer(selectorId: string): Chainable>; - /** - * Selects an option from a CustomSelect component + * @deprecated Selects an option from a CustomSelect component * - * @example cy.selectOption("input-allow_list_id", "Prime numbers"); + * @example cy.getByTestId("input-allow_list_id").antSelect("Prime numbers") */ selectOption( selectorId: string, diff --git a/clients/admin-ui/src/features/common/custom-fields/CustomFieldsList.tsx b/clients/admin-ui/src/features/common/custom-fields/CustomFieldsList.tsx index 394005a712..a51902f107 100644 --- a/clients/admin-ui/src/features/common/custom-fields/CustomFieldsList.tsx +++ b/clients/admin-ui/src/features/common/custom-fields/CustomFieldsList.tsx @@ -82,29 +82,21 @@ export const CustomFieldsList = ({ const { options } = allowList; return ( - - {({ - field, - }: { - field: FieldInputProps; - }) => ( - - )} - + ); })} diff --git a/clients/admin-ui/src/features/privacy-requests/SubmitPrivacyRequestForm.tsx b/clients/admin-ui/src/features/privacy-requests/SubmitPrivacyRequestForm.tsx index 9f053f63b9..6a57e70d44 100644 --- a/clients/admin-ui/src/features/privacy-requests/SubmitPrivacyRequestForm.tsx +++ b/clients/admin-ui/src/features/privacy-requests/SubmitPrivacyRequestForm.tsx @@ -111,10 +111,10 @@ const SubmitPrivacyRequestForm = ({ config?.actions, ); - const handleResetCustomFields = (e: any) => { + const handleResetCustomFields = (value: string) => { // when selecting a new request type, populate the Formik state with // labels and default values for the corresponding custom fields - const newAction = findActionFromPolicyKey(e.value, config?.actions); + const newAction = findActionFromPolicyKey(value, config?.actions); if (!newAction?.custom_privacy_request_fields) { setFieldValue(`custom_privacy_request_fields`, undefined); return; diff --git a/clients/admin-ui/src/features/system/VendorSelector.tsx b/clients/admin-ui/src/features/system/VendorSelector.tsx index 9058612c3c..91291d9f09 100644 --- a/clients/admin-ui/src/features/system/VendorSelector.tsx +++ b/clients/admin-ui/src/features/system/VendorSelector.tsx @@ -171,7 +171,9 @@ const VendorSelector = ({ }; // complete the autosuggest - const handleTabPressed = async (event: KeyboardEvent) => { + const handleTabPressed = async ( + event: KeyboardEvent, + ) => { if (suggestions.length > 0 && searchParam !== suggestions[0].label) { event.preventDefault(); setSearchParam(suggestions[0].label); @@ -196,7 +198,7 @@ const VendorSelector = ({ - + id="vendorName" showSearch @@ -217,13 +219,13 @@ const VendorSelector = ({ onSearch={setSearchParam} onClear={handleClear} onBlur={handleBlur} - onKeyDown={(e) => { + onInputKeyDown={(e) => { if (searchParam && e.key === "Tab") { handleTabPressed(e); } }} status={isInvalid ? "error" : undefined} - className="w-full" + data-testid="vendor-name-select" /> Date: Tue, 19 Nov 2024 09:59:07 -0700 Subject: [PATCH 15/25] various layout fixes --- .../ConsentManagementModal.tsx | 56 +++++++++++-------- .../features/system/SystemInformationForm.tsx | 6 ++ 2 files changed, 38 insertions(+), 24 deletions(-) diff --git a/clients/admin-ui/src/features/configure-consent/ConsentManagementModal.tsx b/clients/admin-ui/src/features/configure-consent/ConsentManagementModal.tsx index a3705c407a..ecf501db01 100644 --- a/clients/admin-ui/src/features/configure-consent/ConsentManagementModal.tsx +++ b/clients/admin-ui/src/features/configure-consent/ConsentManagementModal.tsx @@ -86,25 +86,29 @@ export const ConsentManagementModal = ({ Data Categories - - {systemPurposeSummary.purposes[ - purposeName - ].data_uses?.map((data_use) => ( - {data_use} - ))} - +
+ + {systemPurposeSummary.purposes[ + purposeName + ].data_uses?.map((data_use) => ( + {data_use} + ))} + +
Data Categories - - {systemPurposeSummary.purposes[ - purposeName - ].legal_bases?.map((legal_base) => ( - {legal_base} - ))} - +
+ + {systemPurposeSummary.purposes[ + purposeName + ].legal_bases?.map((legal_base) => ( + {legal_base} + ))} + +
@@ -115,19 +119,23 @@ export const ConsentManagementModal = ({
Features - - {systemPurposeSummary.features?.map((feature) => ( - {feature} - ))} - +
+ + {systemPurposeSummary.features?.map((feature) => ( + {feature} + ))} + +
Data Categories - - {systemPurposeSummary.data_categories?.map((category) => ( - {category} - ))} - +
+ + {systemPurposeSummary.data_categories?.map((category) => ( + {category} + ))} + +
) diff --git a/clients/admin-ui/src/features/system/SystemInformationForm.tsx b/clients/admin-ui/src/features/system/SystemInformationForm.tsx index e6f127f9f1..21c58324a8 100644 --- a/clients/admin-ui/src/features/system/SystemInformationForm.tsx +++ b/clients/admin-ui/src/features/system/SystemInformationForm.tsx @@ -433,6 +433,8 @@ const SystemInformationForm = ({ > Date: Tue, 19 Nov 2024 12:06:33 -0700 Subject: [PATCH 16/25] fix broken link palette color --- clients/admin-ui/src/theme/ant.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/clients/admin-ui/src/theme/ant.ts b/clients/admin-ui/src/theme/ant.ts index 742640d365..b5a678bf2d 100644 --- a/clients/admin-ui/src/theme/ant.ts +++ b/clients/admin-ui/src/theme/ant.ts @@ -19,7 +19,7 @@ export const antTheme: AntThemeConfig = { colorSuccess: palette.FIDESUI_SUCCESS, colorWarning: palette.FIDESUI_WARNING, colorError: palette.FIDESUI_ERROR, - colorLink: palette.LINK, + colorLink: palette.FIDESUI_LINK, colorBgBase: palette.FIDESUI_FULL_WHITE, borderRadius: 4, wireframe: true, From 781a7bbc7e76bc1cc28cc02184024ced3124ef63 Mon Sep 17 00:00:00 2001 From: Jason Gill Date: Tue, 19 Nov 2024 12:06:57 -0700 Subject: [PATCH 17/25] update badge display views --- .../ConsentManagementModal.tsx | 138 +++++++++--------- .../privacy-notices/PrivacyNoticeForm.tsx | 29 ++-- 2 files changed, 88 insertions(+), 79 deletions(-) diff --git a/clients/admin-ui/src/features/configure-consent/ConsentManagementModal.tsx b/clients/admin-ui/src/features/configure-consent/ConsentManagementModal.tsx index ecf501db01..600c7d2d55 100644 --- a/clients/admin-ui/src/features/configure-consent/ConsentManagementModal.tsx +++ b/clients/admin-ui/src/features/configure-consent/ConsentManagementModal.tsx @@ -24,6 +24,8 @@ import { import { useGetSystemPurposeSummaryQuery } from "~/features/plus/plus.slice"; +const { Text, Title } = Typography; + export const useConsentManagementModal = () => { const { isOpen, onOpen, onClose } = useDisclosure(); @@ -44,6 +46,21 @@ export const ConsentManagementModal = ({ const { data: systemPurposeSummary, isLoading } = useGetSystemPurposeSummaryQuery(fidesKey); + const listRender = (label: string, list: string[]) => ( + <> + {label} + {list?.length ? ( +
+ + {list?.map((item) => {item})} + +
+ ) : ( + no known {label.toLowerCase()} + )} + + ); + return ( ) : ( !!systemPurposeSummary && ( - <> + + Purposes {Object.entries(systemPurposeSummary.purposes || {}).length > - 0 && Purposes} - - {Object.entries(systemPurposeSummary.purposes).map( - ([purposeName], index: number) => ( - - {({ isExpanded }) => ( - <> - - - {purposeName} - - - - - - - Data Categories - -
- - {systemPurposeSummary.purposes[ - purposeName - ].data_uses?.map((data_use) => ( - {data_use} - ))} - -
-
- - - Data Categories - -
- - {systemPurposeSummary.purposes[ - purposeName - ].legal_bases?.map((legal_base) => ( - {legal_base} - ))} - -
-
-
- - )} -
- ), - )} -
+ 0 ? ( + + {Object.entries(systemPurposeSummary.purposes).map( + ([purposeName], index: number) => ( + + {({ isExpanded }) => ( + <> + + + {purposeName} + + + + + + {listRender( + "Data uses", + systemPurposeSummary.purposes[purposeName] + .data_uses, + )} + + + {listRender( + "Legal basis", + systemPurposeSummary.purposes[purposeName] + .legal_bases, + )} + + + + )} + + ), + )} + + ) : ( + no known purposes + )}
- Features -
- - {systemPurposeSummary.features?.map((feature) => ( - {feature} - ))} - -
+ {listRender("Features", systemPurposeSummary.features)}
- Data Categories -
- - {systemPurposeSummary.data_categories?.map((category) => ( - {category} - ))} - -
+ {listRender( + "Data categories", + systemPurposeSummary.data_categories, + )}
- +
) )} diff --git a/clients/admin-ui/src/features/privacy-notices/PrivacyNoticeForm.tsx b/clients/admin-ui/src/features/privacy-notices/PrivacyNoticeForm.tsx index 6067027014..c81a9e1360 100644 --- a/clients/admin-ui/src/features/privacy-notices/PrivacyNoticeForm.tsx +++ b/clients/admin-ui/src/features/privacy-notices/PrivacyNoticeForm.tsx @@ -3,6 +3,7 @@ import { AntButton as Button, AntSpace as Space, AntTag as Tag, + AntTypography as Typography, Box, Divider, Flex, @@ -12,6 +13,7 @@ import { VStack, } from "fidesui"; import { Form, Formik } from "formik"; +import NextLink from "next/link"; import { useRouter } from "next/router"; import { useMemo } from "react"; @@ -20,6 +22,7 @@ import FormSection from "~/features/common/form/FormSection"; import { CustomSwitch, CustomTextInput } from "~/features/common/form/inputs"; import { getErrorMessage, isErrorResult } from "~/features/common/helpers"; import { PRIVACY_NOTICES_ROUTE } from "~/features/common/nav/v2/routes"; +import * as routes from "~/features/common/nav/v2/routes"; import { PRIVACY_NOTICE_REGION_RECORD } from "~/features/common/privacy-notice-regions"; import QuestionTooltip from "~/features/common/QuestionTooltip"; import { errorToastParams, successToastParams } from "~/features/common/toast"; @@ -55,6 +58,8 @@ import { usePostPrivacyNoticeMutation, } from "./privacy-notices.slice"; +const { Text, Link } = Typography; + const PrivacyNoticeLocationDisplay = ({ regions, label, @@ -78,7 +83,15 @@ const PrivacyNoticeLocationDisplay = ({ {regions?.map((r) => ( {PRIVACY_NOTICE_REGION_RECORD[r]} ))} - {!regions?.length && No locations assigned} + {!regions?.length && ( + + No locations assigned. Navigate to the{" "} + + experiences view + {" "} + configure. + + )}
@@ -180,13 +193,17 @@ const PrivacyNoticeForm = ({ layout="stacked" /> + + {!isChildNotice && ( label="Child notices" @@ -211,12 +228,6 @@ const PrivacyNoticeForm = ({ baseTestId="children" /> )} - - Date: Tue, 19 Nov 2024 12:38:34 -0700 Subject: [PATCH 18/25] minor fix --- .../admin-ui/src/features/privacy-notices/PrivacyNoticeForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/clients/admin-ui/src/features/privacy-notices/PrivacyNoticeForm.tsx b/clients/admin-ui/src/features/privacy-notices/PrivacyNoticeForm.tsx index c81a9e1360..6b071bd0a9 100644 --- a/clients/admin-ui/src/features/privacy-notices/PrivacyNoticeForm.tsx +++ b/clients/admin-ui/src/features/privacy-notices/PrivacyNoticeForm.tsx @@ -58,7 +58,7 @@ import { usePostPrivacyNoticeMutation, } from "./privacy-notices.slice"; -const { Text, Link } = Typography; +const { Text } = Typography; const PrivacyNoticeLocationDisplay = ({ regions, From 5f982d68229786a59553d0bad7b25e6b520bc577 Mon Sep 17 00:00:00 2001 From: Jason Gill Date: Wed, 20 Nov 2024 14:52:34 -0700 Subject: [PATCH 19/25] fix Connection Type filter options --- .../datastore-connections/filters/ConnectionTypeFilter.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/clients/admin-ui/src/features/datastore-connections/filters/ConnectionTypeFilter.tsx b/clients/admin-ui/src/features/datastore-connections/filters/ConnectionTypeFilter.tsx index fd05c56eef..1646279446 100644 --- a/clients/admin-ui/src/features/datastore-connections/filters/ConnectionTypeFilter.tsx +++ b/clients/admin-ui/src/features/datastore-connections/filters/ConnectionTypeFilter.tsx @@ -29,7 +29,7 @@ const ConnectionTypeFilter = () => { }, [connectionOptions, connection_type]); const list = useMemo(() => loadList(), [loadList]); - const options = [...list].map(([key]) => ({ value: key })); + const options = [...list].map(([key]) => ({ value: key, label: key })); // Hooks const dispatch = useAppDispatch(); From 91fb96999438c3746a0406e6614cd147799ed431 Mon Sep 17 00:00:00 2001 From: Jason Gill Date: Wed, 20 Nov 2024 15:31:25 -0700 Subject: [PATCH 20/25] migrate connection type filter --- .../common/dropdown/MultiSelectDropdown.tsx | 152 ------------------ .../add-connection/ConnectionTypeFilter.tsx | 33 ++-- 2 files changed, 15 insertions(+), 170 deletions(-) delete mode 100644 clients/admin-ui/src/features/common/dropdown/MultiSelectDropdown.tsx diff --git a/clients/admin-ui/src/features/common/dropdown/MultiSelectDropdown.tsx b/clients/admin-ui/src/features/common/dropdown/MultiSelectDropdown.tsx deleted file mode 100644 index 64d4b9c66e..0000000000 --- a/clients/admin-ui/src/features/common/dropdown/MultiSelectDropdown.tsx +++ /dev/null @@ -1,152 +0,0 @@ -import { - AntButton as Button, - ArrowDownLineIcon, - Box, - HStack, - Menu, - MenuButton, - PlacementWithLogical, - Text, - Tooltip, -} from "fidesui"; -import React, { useState } from "react"; - -import MultiSelectDropdownList from "./MultiSelectDropdownList"; - -type MultiSelectDropdwonProps = { - /** - * Boolean to determine if the dropdown is to be immediately close on a user selection - */ - closeOnSelect?: boolean; - /** - * List of key/value pairs to be rendered as a checkbox list - */ - list: Map; - /** - * Parent callback event handler invoked when list of selection values have changed - */ - onChange: (values: string[]) => void; - /** - * List of key/value pairs which are marked for selection - */ - selectedList: Map; - /** - * Placeholder - */ - label: string; - /** - * Disable showing a tooltip - */ - tooltipDisabled?: boolean; - /** - * Position of the tooltip - */ - tooltipPlacement?: PlacementWithLogical; - /** - * Fixed Width of the textbox within the Menu Button component - */ - width?: string; -}; - -const MultiSelectDropdown = ({ - closeOnSelect = false, - list, - onChange, - selectedList, - label, - tooltipDisabled = false, - tooltipPlacement = "auto", - width, -}: MultiSelectDropdwonProps) => { - const defaultItems = new Map(list); - - // Hooks - const [isOpen, setIsOpen] = useState(false); - - // Listeners - const handleClose = () => { - setIsOpen(false); - }; - const handleOpen = () => { - setIsOpen(true); - }; - const handleSelection = (items: Map) => { - const temp = new Map([...items].filter(([, v]) => v === true)); - onChange([...temp.keys()]); - handleClose(); - }; - - const getMenuButtonText = () => { - if (!tooltipDisabled) { - return selectedList.size > 0 - ? [...selectedList.keys()].sort().join(", ") - : label; - } - return label; - }; - - return ( - - - {({ onClose }) => ( - <> - 0)} - placement={tooltipPlacement} - > - } - iconPosition="end" - className="hover:bg-none active:bg-none" - > - {!tooltipDisabled && ( - - {getMenuButtonText()} - - )} - {tooltipDisabled && ( - - {getMenuButtonText()} - {selectedList.size > 0 && ( - {selectedList.size} - )} - - )} - - - {isOpen && ( - { - handleSelection(items); - onClose(); - }} - /> - )} - - )} - - - ); -}; - -export default MultiSelectDropdown; diff --git a/clients/admin-ui/src/features/datastore-connections/add-connection/ConnectionTypeFilter.tsx b/clients/admin-ui/src/features/datastore-connections/add-connection/ConnectionTypeFilter.tsx index a31e283dd5..842bc1f7e7 100644 --- a/clients/admin-ui/src/features/datastore-connections/add-connection/ConnectionTypeFilter.tsx +++ b/clients/admin-ui/src/features/datastore-connections/add-connection/ConnectionTypeFilter.tsx @@ -1,13 +1,8 @@ -import SelectDropdown from "common/dropdown/SelectDropdown"; -import { - selectConnectionTypeFilters, - setSystemType, -} from "connection-type/connection-type.slice"; -import { Box } from "fidesui"; +import { setSystemType } from "connection-type/connection-type.slice"; import React, { useEffect, useRef } from "react"; import { useDispatch } from "react-redux"; -import { useAppSelector } from "~/app/hooks"; +import { FilterSelect } from "~/features/common/dropdown/FilterSelect"; import { CONNECTION_TYPE_FILTER_MAP, @@ -17,7 +12,6 @@ import { const ConnectionTypeFilter = () => { const dispatch = useDispatch(); const mounted = useRef(false); - const filters = useAppSelector(selectConnectionTypeFilters); const handleChange = (value?: string) => { dispatch(setSystemType(value || DEFAULT_CONNECTION_TYPE_FILTER)); @@ -31,17 +25,20 @@ const ConnectionTypeFilter = () => { }; }, [dispatch]); + const options = useRef( + [...CONNECTION_TYPE_FILTER_MAP].map(([key, value]) => ({ + value: value?.value, + label: key, + })), + ); + return ( - - - + ); }; From c19e94bb6cb94640a45aa6eadc5eabccfd6b8b6f Mon Sep 17 00:00:00 2001 From: Jason Gill Date: Wed, 20 Nov 2024 15:58:56 -0700 Subject: [PATCH 21/25] migrate remaining Selects --- .../dropdown/MultiSelectDropdownList.tsx | 109 --------------- .../common/dropdown/MultiSelectTags.tsx | 124 ------------------ .../RequestTableFilterModal.tsx | 26 ++-- .../features/privacy-requests/constants.ts | 14 ++ 4 files changed, 28 insertions(+), 245 deletions(-) delete mode 100644 clients/admin-ui/src/features/common/dropdown/MultiSelectDropdownList.tsx delete mode 100644 clients/admin-ui/src/features/common/dropdown/MultiSelectTags.tsx diff --git a/clients/admin-ui/src/features/common/dropdown/MultiSelectDropdownList.tsx b/clients/admin-ui/src/features/common/dropdown/MultiSelectDropdownList.tsx deleted file mode 100644 index 3cca7c65e3..0000000000 --- a/clients/admin-ui/src/features/common/dropdown/MultiSelectDropdownList.tsx +++ /dev/null @@ -1,109 +0,0 @@ -import { - AntButton as Button, - Box, - Checkbox, - CheckboxGroup, - Flex, - MenuItem, - MenuList, - Spacer, -} from "fidesui"; -import React, { useState } from "react"; - -type MultiSelectDropdownListProps = { - defaultValues?: string[]; - items: Map; - onSelection: (items: Map) => void; -}; - -/** - * @param defaultValues - List of default item values - * @param items - List of key/value pair items - * @param onSelection - Event handler invoked when user item selections are applied - */ -const MultiSelectDropdownList = ({ - defaultValues, - items, - onSelection, -}: MultiSelectDropdownListProps) => { - const [pendingItems, setPendingItems] = useState(items); - - // Listeners - const handleChange = (values: string[]) => { - // Copy items - const temp = new Map(pendingItems); - - // Uncheck all items - temp.forEach((_value, key) => { - temp.set(key, false); - }); - - // Check the selected items - values.forEach((v) => { - temp.set(v, true); - }); - - setPendingItems(temp); - }; - const handleClear = () => { - setPendingItems(items); - onSelection(new Map()); - }; - const handleDone = () => { - onSelection(pendingItems); - }; - - return ( - - - - - - - {/* MenuItems are not rendered unless Menu is open */} - - - {[...items].sort().map(([key]) => ( - { - if (e.key === " ") { - e.currentTarget.getElementsByTagName("input")[0].click(); - } - if (e.key === "Enter") { - handleDone(); - } - }} - > - e.stopPropagation()} - > - {key} - - - ))} - - - - ); -}; - -export default MultiSelectDropdownList; diff --git a/clients/admin-ui/src/features/common/dropdown/MultiSelectTags.tsx b/clients/admin-ui/src/features/common/dropdown/MultiSelectTags.tsx deleted file mode 100644 index 02377f7254..0000000000 --- a/clients/admin-ui/src/features/common/dropdown/MultiSelectTags.tsx +++ /dev/null @@ -1,124 +0,0 @@ -import { - AntButton as Button, - ArrowDownLineIcon, - Box, - Menu, - MenuButton, - MenuButtonProps, - Tag, - TagCloseButton, - TagLabel, -} from "fidesui"; -import { useMemo, useState } from "react"; - -import { createSelectedMap, getKeysFromMap } from "~/features/common/utils"; - -import MultiSelectDropdownList from "./MultiSelectDropdownList"; - -interface MultiSelectTagsProps - extends Omit { - closeOnSelect?: boolean; - options: Map; - value: T[] | undefined; - placeholder?: string; - onChange: (values: T[]) => void; -} - -/** - * Dropdown menu with a list of checkboxes for multiple selection that displays the selected values in a textbox as chips - * @param closeOnSelect - Boolean to determine if the dropdown is to be immediately close on a user selection - * @param options - Map of key/value pairs to be rendered as a checkbox list, where the value is the display text - * @param onChange - Parent callback event handler invoked when list of selection values have changed - * @param placeholder - Placeholder text to be displayed when no items are selected - */ -export const MultiSelectTags = ({ - closeOnSelect = false, - options, - value, - onChange, - placeholder = "Select one or more items", - ...props -}: MultiSelectTagsProps) => { - const [isOpen, setIsOpen] = useState(false); - const list = useMemo( - () => createSelectedMap(options, value), - [options, value], - ); - const selectedList = useMemo( - () => value?.map((selectedItem) => options.get(selectedItem)!), - [options, value], - ); - - const handleClose = () => { - setIsOpen(false); - }; - const handleOpen = () => { - setIsOpen(true); - }; - - const handleSelection = (items: Map) => { - const selectedLabels = getKeysFromMap(items, [true]); - onChange(getKeysFromMap(options, selectedLabels)); - handleClose(); - }; - - const handleRemoveItem = (item: T) => { - onChange(value!.filter((selectedItem) => selectedItem !== item)); - }; - - return ( - - {({ onClose }) => ( - <> - - {value?.length - ? value.map((selectedItem) => ( - - {options.get(selectedItem)} - handleRemoveItem(selectedItem)} - /> - - )) - : placeholder} - } - className="absolute right-0 top-0 border-none !bg-transparent" - /> - - {isOpen && ( - { - handleSelection(items as Map); - onClose(); - }} - /> - )} - - )} - - ); -}; diff --git a/clients/admin-ui/src/features/privacy-requests/RequestTableFilterModal.tsx b/clients/admin-ui/src/features/privacy-requests/RequestTableFilterModal.tsx index 03d4c1215d..e40ab9f114 100644 --- a/clients/admin-ui/src/features/privacy-requests/RequestTableFilterModal.tsx +++ b/clients/admin-ui/src/features/privacy-requests/RequestTableFilterModal.tsx @@ -16,13 +16,13 @@ import { Stack, } from "fidesui"; -import { MultiSelectTags } from "~/features/common/dropdown/MultiSelectTags"; import { - SubjectRequestActionTypeMap, - SubjectRequestStatusMap, + SubjectRequestActionTypeOptions, + SubjectRequestStatusOptions, } from "~/features/privacy-requests/constants"; import { useRequestFilters } from "~/features/privacy-requests/hooks/useRequestFilters"; -import { ActionType, PrivacyRequestStatus } from "~/types/api"; + +import { FilterSelect } from "../common/dropdown/FilterSelect"; interface RequestTableFilterModalProps extends Omit { onFilterChange: () => void; @@ -100,25 +100,27 @@ export const RequestTableFilterModal = ({ - + Status - - options={SubjectRequestStatusMap} + - + Request Type - - options={SubjectRequestActionTypeMap} + diff --git a/clients/admin-ui/src/features/privacy-requests/constants.ts b/clients/admin-ui/src/features/privacy-requests/constants.ts index 030e0e0bca..be88297f08 100644 --- a/clients/admin-ui/src/features/privacy-requests/constants.ts +++ b/clients/admin-ui/src/features/privacy-requests/constants.ts @@ -13,6 +13,13 @@ export const SubjectRequestStatusMap = new Map([ [PrivacyRequestStatus.REQUIRES_INPUT, "Requires input"], ]); +export const SubjectRequestStatusOptions = [...SubjectRequestStatusMap].map( + ([key, value]) => ({ + label: value, + value: key, + }), +); + export const SubjectRequestActionTypeMap = new Map([ [ActionType.ACCESS, "Access"], [ActionType.ERASURE, "Erasure"], @@ -20,6 +27,13 @@ export const SubjectRequestActionTypeMap = new Map([ [ActionType.UPDATE, "Update"], ]); +export const SubjectRequestActionTypeOptions = [ + ...SubjectRequestActionTypeMap, +].map(([key, value]) => ({ + label: value, + value: key, +})); + export const messagingProviders = { mailgun: "mailgun", twilio_email: "twilio_email", From 65951a397e2877b72e531bbcc7a562e50f999272 Mon Sep 17 00:00:00 2001 From: Jason Gill Date: Wed, 20 Nov 2024 16:06:03 -0700 Subject: [PATCH 22/25] Revert "Update CHANGELOG.md" This reverts commit c759c250ae6761fb4989775d58be780277ef442c. --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1646a56272..d133eeeae8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,7 +21,7 @@ The types of changes are: - Added namespace support for Snowflake [#5486](https://github.com/ethyca/fides/pull/5486) ### Developer Experience -- Migrated all instances of Chakra React Select component to use Ant's Select component [#5475](https://github.com/ethyca/fides/pull/5475) +- Migrated several instances of Chakra's Select component to use Ant's Select component [#5475](https://github.com/ethyca/fides/pull/5475) ### Fixed - Fixed deletion of ConnectionConfigs that have related MonitorConfigs [#5478](https://github.com/ethyca/fides/pull/5478) From 44674299f7a5ccd6a1cee724bcbab7484433e316 Mon Sep 17 00:00:00 2001 From: Jason Gill Date: Wed, 20 Nov 2024 16:07:36 -0700 Subject: [PATCH 23/25] Update CHANGELOG.md --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5eaece1756..5466021c68 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,8 @@ The types of changes are: ## [Unreleased](https://github.com/ethyca/fides/compare/2.50.0...main) +### Developer Experience +- Migrated remaining instances of Chakra's Select component to use Ant's Select component [#5502](https://github.com/ethyca/fides/pull/5502) From eec85dd01d200e4c650573393146432f7b7fe894 Mon Sep 17 00:00:00 2001 From: Jason Gill Date: Wed, 20 Nov 2024 16:32:34 -0700 Subject: [PATCH 24/25] fix Cypress test --- clients/admin-ui/cypress/e2e/connectors.cy.ts | 4 +--- .../add-connection/ConnectionTypeFilter.tsx | 1 + 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/clients/admin-ui/cypress/e2e/connectors.cy.ts b/clients/admin-ui/cypress/e2e/connectors.cy.ts index 10f4472f83..8c6d6d7c97 100644 --- a/clients/admin-ui/cypress/e2e/connectors.cy.ts +++ b/clients/admin-ui/cypress/e2e/connectors.cy.ts @@ -163,9 +163,7 @@ describe("Connectors", () => { it("allows the user to add an email connector", () => { cy.visit("/datastore-connection/new"); - cy.getByTestId("select-dropdown-btn").click(); - cy.getByTestId("select-dropdown-list").contains("Email connectors"); - cy.getByTestId("select-dropdown-btn").click(); + cy.getByTestId("connection-type-filter").antSelect("Email connectors"); cy.getByTestId("sovrn-item").click(); cy.url().should("contain", "/new?step=2"); diff --git a/clients/admin-ui/src/features/datastore-connections/add-connection/ConnectionTypeFilter.tsx b/clients/admin-ui/src/features/datastore-connections/add-connection/ConnectionTypeFilter.tsx index 842bc1f7e7..86d4336fd8 100644 --- a/clients/admin-ui/src/features/datastore-connections/add-connection/ConnectionTypeFilter.tsx +++ b/clients/admin-ui/src/features/datastore-connections/add-connection/ConnectionTypeFilter.tsx @@ -38,6 +38,7 @@ const ConnectionTypeFilter = () => { options={options.current} onChange={handleChange} defaultValue="" + data-testid="connection-type-filter" /> ); }; From 016b2f01f7df725ed74125e8d2582554a1e28277 Mon Sep 17 00:00:00 2001 From: Jason Gill Date: Thu, 21 Nov 2024 13:30:13 -0700 Subject: [PATCH 25/25] include `data-testid` where appropriate --- .../src/features/common/dropdown/FilterSelect.tsx | 1 + .../filters/ConnectionTypeFilter.tsx | 1 + .../filters/DisabledStatusFilter.tsx | 1 + .../datastore-connections/filters/SystemTypeFilter.tsx | 1 + .../filters/TestingStatusFilter.tsx | 1 + .../privacy-requests/RequestTableFilterModal.tsx | 10 ++++++---- clients/fidesui/src/hoc/CustomSelect.tsx | 1 + 7 files changed, 12 insertions(+), 4 deletions(-) diff --git a/clients/admin-ui/src/features/common/dropdown/FilterSelect.tsx b/clients/admin-ui/src/features/common/dropdown/FilterSelect.tsx index 4b9add148f..6313c79ee7 100644 --- a/clients/admin-ui/src/features/common/dropdown/FilterSelect.tsx +++ b/clients/admin-ui/src/features/common/dropdown/FilterSelect.tsx @@ -11,6 +11,7 @@ export const FilterSelect = ({ allowClear dropdownStyle={isMulti ? undefined : { width: "auto", minWidth: "200px" }} className="w-auto" + data-testid="filter-select" {...props} /> ); diff --git a/clients/admin-ui/src/features/datastore-connections/filters/ConnectionTypeFilter.tsx b/clients/admin-ui/src/features/datastore-connections/filters/ConnectionTypeFilter.tsx index 1646279446..6bb8611295 100644 --- a/clients/admin-ui/src/features/datastore-connections/filters/ConnectionTypeFilter.tsx +++ b/clients/admin-ui/src/features/datastore-connections/filters/ConnectionTypeFilter.tsx @@ -50,6 +50,7 @@ const ConnectionTypeFilter = () => { onChange={handleChange} defaultValue={connection_type?.length ? connection_type : []} className="w-60" + data-testid="connection-type-filter" /> ); }; diff --git a/clients/admin-ui/src/features/datastore-connections/filters/DisabledStatusFilter.tsx b/clients/admin-ui/src/features/datastore-connections/filters/DisabledStatusFilter.tsx index a4dd56adb2..88c7060d68 100644 --- a/clients/admin-ui/src/features/datastore-connections/filters/DisabledStatusFilter.tsx +++ b/clients/admin-ui/src/features/datastore-connections/filters/DisabledStatusFilter.tsx @@ -46,6 +46,7 @@ const DisabledStatusFilter = () => { options={options} onChange={handleChange} defaultValue={disabled_status?.toString() || undefined} + data-testid="disabled-status-filter" /> ); }; diff --git a/clients/admin-ui/src/features/datastore-connections/filters/SystemTypeFilter.tsx b/clients/admin-ui/src/features/datastore-connections/filters/SystemTypeFilter.tsx index ce864d36d6..fab48a23fb 100644 --- a/clients/admin-ui/src/features/datastore-connections/filters/SystemTypeFilter.tsx +++ b/clients/admin-ui/src/features/datastore-connections/filters/SystemTypeFilter.tsx @@ -37,6 +37,7 @@ const SystemTypeFilter = () => { options={options} onChange={handleChange} defaultValue={system_type?.toString() || undefined} + data-testid="system-type-filter" /> ); }; diff --git a/clients/admin-ui/src/features/datastore-connections/filters/TestingStatusFilter.tsx b/clients/admin-ui/src/features/datastore-connections/filters/TestingStatusFilter.tsx index ab45c0ddce..9fa7a88a8e 100644 --- a/clients/admin-ui/src/features/datastore-connections/filters/TestingStatusFilter.tsx +++ b/clients/admin-ui/src/features/datastore-connections/filters/TestingStatusFilter.tsx @@ -46,6 +46,7 @@ const TestingStatusFilter = () => { options={options} onChange={handleChange} defaultValue={test_status?.toString() || undefined} + data-testid="testing-status-filter" /> ); }; diff --git a/clients/admin-ui/src/features/privacy-requests/RequestTableFilterModal.tsx b/clients/admin-ui/src/features/privacy-requests/RequestTableFilterModal.tsx index e40ab9f114..06aeb9733a 100644 --- a/clients/admin-ui/src/features/privacy-requests/RequestTableFilterModal.tsx +++ b/clients/admin-ui/src/features/privacy-requests/RequestTableFilterModal.tsx @@ -100,27 +100,29 @@ export const RequestTableFilterModal = ({ - + Status - + Request Type diff --git a/clients/fidesui/src/hoc/CustomSelect.tsx b/clients/fidesui/src/hoc/CustomSelect.tsx index ad5a999965..87753f2494 100644 --- a/clients/fidesui/src/hoc/CustomSelect.tsx +++ b/clients/fidesui/src/hoc/CustomSelect.tsx @@ -44,6 +44,7 @@ const withCustomProps = (WrappedComponent: typeof Select) => { optionRender, className, suffixIcon, + "data-testid": `select${props.id ? `-${props.id}` : ""}`, ...props, }; return ;