Skip to content

Commit

Permalink
Stack creatable multiselect (#2620)
Browse files Browse the repository at this point in the history
  • Loading branch information
allisonking authored Feb 21, 2023
1 parent 53e13d5 commit 871ccf6
Show file tree
Hide file tree
Showing 3 changed files with 168 additions and 143 deletions.
299 changes: 160 additions & 139 deletions clients/admin-ui/src/features/common/form/inputs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,24 @@ 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;
isRequired?: boolean;
size?: Size;
isMulti?: boolean;
variant?: Variant;
menuPosition?: MenuPosition;
}
const SelectInput = ({
options,
fieldName,
Expand All @@ -131,7 +149,7 @@ const SelectInput = ({
menuPosition = "absolute",
}: { 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 @@ -203,6 +221,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 @@ -268,23 +369,6 @@ export const CustomTextInput = ({
);
};

export interface Option {
value: string;
label: string;
}
interface SelectProps {
label: string;
labelProps?: FormLabelProps;
tooltip?: string;
options: Option[];
isDisabled?: boolean;
isRequired?: boolean;
isSearchable?: boolean;
isClearable?: boolean;
size?: Size;
isMulti?: boolean;
menuPosition?: MenuPosition;
}
export const CustomSelect = ({
label,
labelProps,
Expand All @@ -298,7 +382,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 @@ -338,7 +422,13 @@ export const CustomSelect = ({
<FormControl isInvalid={isInvalid} isRequired={isRequired}>
<VStack alignItems="start">
<Flex alignItems="center">
<Label htmlFor={props.id || props.name} my={0} {...labelProps}>
<Label
htmlFor={props.id || props.name}
fontSize="sm"
my={0}
mr={1}
{...labelProps}
>
{label}
</Label>
{tooltip ? <QuestionTooltip label={tooltip} /> : null}
Expand All @@ -365,140 +455,71 @@ export const CustomSelect = ({
);
};

export const CustomCreatableSingleSelect = ({
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 = ({
export const CustomCreatableSelect = ({
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
Loading

0 comments on commit 871ccf6

Please sign in to comment.