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

Stack creatable multiselect #2620

Merged
merged 6 commits into from
Feb 21, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
287 changes: 151 additions & 136 deletions clients/admin-ui/src/features/common/form/inputs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,22 @@ const ErrorMessage = ({
);
};

export interface Option {
value: string;
label: string;
}
interface SelectProps {
label: string;
labelProps?: FormLabelProps;
tooltip?: string;
options: Option[];
isDisabled?: boolean;
isSearchable?: boolean;
isClearable?: boolean;
size?: Size;
isMulti?: boolean;
variant?: Variant;
}
const SelectInput = ({
options,
fieldName,
Expand All @@ -127,7 +143,7 @@ const SelectInput = ({
isMulti = false,
}: { fieldName: string; isMulti?: boolean } & Omit<SelectProps, "label">) => {
const [initialField] = useField(fieldName);
const field = { ...initialField, value: initialField.value ?? [] };
const field = { ...initialField, value: initialField.value ?? "" };
const selected = isMulti
? options.filter((o) => field.value.indexOf(o.value) >= 0)
: options.find((o) => o.value === field.value) || null;
Expand Down Expand Up @@ -197,6 +213,89 @@ const SelectInput = ({
);
};

interface CreatableSelectProps extends SelectProps {
/** Do not render the dropdown menu */
disableMenu?: boolean;
}
const CreatableSelectInput = ({
options,
fieldName,
size,
isSearchable,
isClearable,
isMulti,
disableMenu,
}: { fieldName: string } & Omit<CreatableSelectProps, "label">) => {
const [initialField] = useField(fieldName);
const value: string[] | string = initialField.value ?? [];
const field = { ...initialField, value };
const selected = Array.isArray(field.value)
? field.value.map((v) => ({ label: v, value: v }))
: { label: field.value, value: field.value };

const { setFieldValue, touched, setTouched } = useFormikContext();

const handleChangeMulti = (newValue: MultiValue<Option>) => {
setFieldValue(
field.name,
newValue.map((v) => v.value)
);
};
const handleChangeSingle = (newValue: SingleValue<Option>) => {
if (newValue) {
field.onChange(field.name)(newValue.value);
} else {
field.onChange(field.name)("");
}
};

const handleChange = (newValue: MultiValue<Option> | SingleValue<Option>) =>
isMulti
? handleChangeMulti(newValue as MultiValue<Option>)
: handleChangeSingle(newValue as SingleValue<Option>);

const components = disableMenu
? { Menu: () => null, DropdownIndicator: () => null }
: undefined;

return (
<CreatableSelect
options={options}
onBlur={(e) => {
setTouched({ ...touched, [field.name]: true });
field.onBlur(e);
}}
onChange={handleChange}
name={fieldName}
value={selected}
size={size}
classNamePrefix="custom-creatable-select"
chakraStyles={{
container: (provided) => ({ ...provided, mr: 2, flexGrow: 1 }),
dropdownIndicator: (provided) => ({
...provided,
background: "white",
}),
multiValue: (provided) => ({
...provided,
background: "primary.400",
color: "white",
}),
multiValueRemove: (provided) => ({
...provided,
display: "none",
visibility: "hidden",
}),
}}
components={components}
isSearchable={isSearchable}
isClearable={isClearable}
instanceId={`creatable-select-${fieldName}`}
isMulti={isMulti}
/>
);
};

export const CustomTextInput = ({
label,
tooltip,
Expand Down Expand Up @@ -261,21 +360,6 @@ export const CustomTextInput = ({
);
};

export interface Option {
value: string;
label: string;
}
interface SelectProps {
label: string;
labelProps?: FormLabelProps;
tooltip?: string;
options: Option[];
isDisabled?: boolean;
isSearchable?: boolean;
isClearable?: boolean;
size?: Size;
isMulti?: boolean;
}
export const CustomSelect = ({
label,
labelProps,
Expand All @@ -288,7 +372,7 @@ export const CustomSelect = ({
isMulti,
variant = "inline",
...props
}: SelectProps & StringField & { variant?: Variant }) => {
}: SelectProps & StringField) => {
const [field, meta] = useField(props);
const isInvalid = !!(meta.touched && meta.error);
if (variant === "inline") {
Expand Down Expand Up @@ -351,140 +435,71 @@ export const CustomSelect = ({
);
};

export const CustomCreatableSingleSelect = ({
export const CustomCreatableSelect = ({
label,
isSearchable,
options,
...props
}: SelectProps & StringField) => {
const [initialField, meta] = useField(props);
const field = { ...initialField, value: initialField.value ?? "" };
const isInvalid = !!(meta.touched && meta.error);
const selected = { label: field.value, value: field.value };

const { touched, setTouched } = useFormikContext();

return (
<FormControl isInvalid={isInvalid}>
<Grid templateColumns="1fr 3fr">
<Label htmlFor={props.id || props.name}>{label}</Label>

<Box data-testid={`input-${field.name}`}>
<CreatableSelect
options={options}
onBlur={(e) => {
setTouched({ ...touched, [field.name]: true });
field.onBlur(e);
}}
onChange={(newValue) => {
if (newValue) {
field.onChange(props.name)(newValue.value);
} else {
field.onChange(props.name)("");
}
}}
name={props.name}
value={selected}
chakraStyles={{
dropdownIndicator: (provided) => ({
...provided,
background: "white",
}),
multiValue: (provided) => ({
...provided,
background: "primary.400",
color: "white",
}),
multiValueRemove: (provided) => ({
...provided,
display: "none",
visibility: "hidden",
}),
}}
/>
</Box>
</Grid>
<ErrorMessage
isInvalid={isInvalid}
message={meta.error}
fieldName={field.name}
/>
</FormControl>
);
};

export const CustomCreatableMultiSelect = ({
label,
isSearchable,
isClearable,
isSearchable = true,
options,
size = "sm",
tooltip,
variant = "inline",
...props
}: SelectProps & StringArrayField) => {
}: CreatableSelectProps & StringArrayField) => {
const [initialField, meta] = useField(props);
const field = { ...initialField, value: initialField.value ?? [] };
const isInvalid = !!(meta.touched && meta.error);
const selected = field.value.map((v) => ({ label: v, value: v }));
const { setFieldValue, touched, setTouched } = useFormikContext();

if (variant === "inline") {
return (
<FormControl isInvalid={isInvalid}>
<Grid templateColumns="1fr 3fr">
<Label htmlFor={props.id || props.name}>{label}</Label>
<Box
display="flex"
alignItems="center"
data-testid={`input-${field.name}`}
>
<CreatableSelectInput
fieldName={field.name}
options={options}
size={size}
isSearchable={isSearchable}
{...props}
/>
{tooltip ? <QuestionTooltip label={tooltip} /> : null}
</Box>
</Grid>
<ErrorMessage
isInvalid={isInvalid}
message={meta.error}
fieldName={field.name}
/>
</FormControl>
);
}
return (
<FormControl isInvalid={isInvalid}>
<Grid templateColumns="1fr 3fr">
<Label htmlFor={props.id || props.name}>{label}</Label>
<Box
display="flex"
alignItems="center"
data-testid={`input-${field.name}`}
>
<CreatableSelect
data-testid={`input-${field.name}`}
name={props.name}
chakraStyles={{
container: (provided) => ({ ...provided, mr: 2, flexGrow: 1 }),
dropdownIndicator: (provided) => ({
...provided,
background: "white",
}),
multiValue: (provided) => ({
...provided,
background: "primary.400",
color: "white",
}),
multiValueRemove: (provided) => ({
...provided,
display: "none",
visibility: "hidden",
}),
}}
components={{
Menu: () => null,
DropdownIndicator: () => null,
}}
isClearable={isClearable}
isMulti
<VStack alignItems="start">
<Flex alignItems="center">
<Label htmlFor={props.id || props.name} fontSize="sm" my={0} mr={1}>
{label}
</Label>
{tooltip ? <QuestionTooltip label={tooltip} /> : null}
</Flex>
<Box width="100%">
<CreatableSelectInput
fieldName={field.name}
options={options}
value={selected}
onBlur={(e) => {
setTouched({ ...touched, [field.name]: true });
field.onBlur(e);
}}
onChange={(newValue) => {
setFieldValue(
field.name,
newValue.map((v) => v.value)
);
}}
size={size}
isSearchable={isSearchable}
{...props}
/>
{tooltip ? <QuestionTooltip label={tooltip} /> : null}
</Box>
</Grid>
<ErrorMessage
isInvalid={isInvalid}
message={meta.error}
fieldName={field.name}
/>
<ErrorMessage
isInvalid={isInvalid}
message={meta.error}
fieldName={field.name}
/>
</VStack>
</FormControl>
);
};
Expand Down
6 changes: 4 additions & 2 deletions clients/admin-ui/src/features/system/DescribeSystemStep.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
useCustomFields,
} from "~/features/common/custom-fields";
import {
CustomCreatableMultiSelect,
CustomCreatableSelect,
CustomSelect,
CustomTextInput,
} from "~/features/common/form/inputs";
Expand Down Expand Up @@ -178,7 +178,7 @@ const DescribeSystemStep = ({
name="system_type"
tooltip="Describe the type of system being modeled, examples include: Service, Application, Third Party, etc"
/>
<CustomCreatableMultiSelect
<CustomCreatableSelect
id="tags"
name="tags"
label="System Tags"
Expand All @@ -191,6 +191,8 @@ const DescribeSystemStep = ({
: []
}
tooltip="Provide one or more tags to group the system. Tags are important as they allow you to filter and group systems for reporting and later review. Tags provide tremendous value as you scale - imagine you have thousands of systems, you’re going to thank us later for tagging!"
disableMenu
isMulti
/>
<CustomSelect
label="System dependencies"
Expand Down
6 changes: 4 additions & 2 deletions clients/admin-ui/src/features/taxonomy/hooks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {

import { YesNoOptions } from "../common/constants";
import {
CustomCreatableMultiSelect,
CustomCreatableSelect,
CustomRadioGroup,
CustomSelect,
CustomTextInput,
Expand Down Expand Up @@ -291,11 +291,13 @@ export const useDataUse = (): TaxonomyHookData<DataUse> => {
options={specialCategories}
isClearable
/>
<CustomCreatableMultiSelect
<CustomCreatableSelect
name="recipients"
label={labels.recipient}
options={[]}
size="sm"
disableMenu
isMulti
/>
<CustomRadioGroup
name="legitimate_interest"
Expand Down