diff options
Diffstat (limited to 'admin/src')
| -rw-r--r-- | admin/src/components/CheckboxListDefaultInput.tsx | 59 | ||||
| -rw-r--r-- | admin/src/components/CheckboxListEnumInput.tsx | 126 | ||||
| -rw-r--r-- | admin/src/components/CheckboxListInput.tsx | 8 | ||||
| -rw-r--r-- | admin/src/index.ts | 12 | ||||
| -rw-r--r-- | admin/src/utils/checkboxListValidator.ts | 74 |
5 files changed, 250 insertions, 29 deletions
diff --git a/admin/src/components/CheckboxListDefaultInput.tsx b/admin/src/components/CheckboxListDefaultInput.tsx index 465d0d3..ffd0551 100644 --- a/admin/src/components/CheckboxListDefaultInput.tsx +++ b/admin/src/components/CheckboxListDefaultInput.tsx @@ -1,6 +1,6 @@ import type { ReactNode } from 'react'; -import { Box, Checkbox, Field, Flex, Typography } from '@strapi/design-system'; +import { Box, Field, MultiSelect, MultiSelectOption, Typography } from '@strapi/design-system'; import { useIntl } from 'react-intl'; type CheckboxListDefaultInputProps = { @@ -23,6 +23,9 @@ type CheckboxListDefaultInputProps = { error?: string; modifiedData?: { enum?: string[]; + options?: { + enum?: string[]; + }; }; }; @@ -51,23 +54,26 @@ const CheckboxListDefaultInput = ({ modifiedData, }: CheckboxListDefaultInputProps) => { const { formatMessage } = useIntl(); - const enumValues = Array.isArray(modifiedData?.enum) ? modifiedData.enum : []; + const enumValues = Array.isArray(modifiedData?.options?.enum) + ? modifiedData.options.enum + : Array.isArray(modifiedData?.enum) + ? modifiedData.enum + : []; const selectedValues = normalizeValue(value); + const uniqueValues = Array.from( + new Set(enumValues.filter((option) => typeof option === 'string' && option.trim().length > 0)) + ); const label = intlLabel ? formatMessage(intlLabel, intlLabel.values ?? {}) : name; const hint = description ? formatMessage(description, description.values ?? {}) : undefined; - const handleToggle = (option: string, isChecked: boolean) => { - const nextValues = isChecked - ? Array.from(new Set([...selectedValues, option])) - : selectedValues.filter((item) => item !== option); - + const handleChange = (nextValues: string[] | undefined) => { onChange({ target: { name, - value: nextValues, + value: Array.isArray(nextValues) ? nextValues : [], }, }); }; @@ -75,21 +81,28 @@ const CheckboxListDefaultInput = ({ return ( <Field.Root name={name} hint={hint} error={error} required={required}> <Field.Label action={labelAction}>{label}</Field.Label> - {enumValues.length > 0 ? ( - <Flex direction="column" gap={2} paddingTop={1} alignItems="flex-start"> - {enumValues.map((option) => ( - <Checkbox - key={option} - checked={selectedValues.includes(option)} - disabled={disabled} - onCheckedChange={(checked: boolean | 'indeterminate') => - handleToggle(option, Boolean(checked)) - } - > - {option} - </Checkbox> - ))} - </Flex> + {uniqueValues.length > 0 ? ( + <Box paddingTop={1}> + <MultiSelect + aria-label={label} + disabled={disabled} + id={name} + name={name} + onChange={handleChange} + placeholder={formatMessage({ + id: 'checkbox-list.default.placeholder', + defaultMessage: 'Select default values', + })} + value={selectedValues} + withTags + > + {uniqueValues.map((option) => ( + <MultiSelectOption key={option} value={option}> + {option} + </MultiSelectOption> + ))} + </MultiSelect> + </Box> ) : ( <Box paddingTop={1}> <Typography variant="pi" textColor="neutral500"> diff --git a/admin/src/components/CheckboxListEnumInput.tsx b/admin/src/components/CheckboxListEnumInput.tsx new file mode 100644 index 0000000..0a1ac73 --- /dev/null +++ b/admin/src/components/CheckboxListEnumInput.tsx @@ -0,0 +1,126 @@ +import type { ChangeEvent, ReactNode } from 'react'; + +import { Field, Textarea } from '@strapi/design-system'; +import { useIntl } from 'react-intl'; + +type CheckboxListEnumInputProps = { + name: string; + value?: unknown; + onChange: (eventOrPath: { target: { name: string; value: string[] } }, value?: unknown) => void; + intlLabel: { + id: string; + defaultMessage: string; + values?: Record<string, string | number | boolean | null | undefined>; + }; + description?: { + id: string; + defaultMessage: string; + values?: Record<string, string | number | boolean | null | undefined>; + } | null; + labelAction?: ReactNode; + placeholder?: { + id: string; + defaultMessage: string; + values?: Record<string, string | number | boolean | null | undefined>; + } | null; + disabled?: boolean; + error?: string; + modifiedData?: { + enum?: string[]; + options?: { + enum?: string[]; + }; + }; +}; + +const normalizeEnum = (value: unknown): string[] => { + if (Array.isArray(value)) { + return value.filter((item): item is string => typeof item === 'string'); + } + + return []; +}; + +const CheckboxListEnumInput = ({ + description = null, + disabled = false, + error = '', + intlLabel, + labelAction, + name, + onChange, + placeholder = null, + value, + modifiedData, +}: CheckboxListEnumInputProps) => { + const { formatMessage } = useIntl(); + + const fallbackEnum = normalizeEnum(modifiedData?.enum); + const resolvedEnum = normalizeEnum(value).length > 0 ? normalizeEnum(value) : fallbackEnum; + + const errorMessage = error + ? formatMessage({ + id: error, + defaultMessage: error, + }) + : ''; + + const hint = description + ? formatMessage( + { + id: description.id, + defaultMessage: description.defaultMessage, + }, + description.values + ) + : ''; + + const label = formatMessage(intlLabel, intlLabel.values ?? {}); + const formattedPlaceholder = placeholder + ? formatMessage( + { + id: placeholder.id, + defaultMessage: placeholder.defaultMessage, + }, + placeholder.values + ) + : ''; + + const inputValue = resolvedEnum.join('\n'); + + const handleChange = (event: ChangeEvent<HTMLTextAreaElement>) => { + const arrayValue = event.target.value.split('\n'); + + onChange({ + target: { + name, + value: arrayValue, + }, + }); + + if (name !== 'enum') { + onChange({ + target: { + name: 'enum', + value: arrayValue, + }, + }); + } + }; + + return ( + <Field.Root error={errorMessage} hint={hint} name={name}> + <Field.Label action={labelAction}>{label}</Field.Label> + <Textarea + disabled={disabled} + onChange={handleChange} + placeholder={formattedPlaceholder} + value={inputValue} + /> + <Field.Error /> + <Field.Hint /> + </Field.Root> + ); +}; + +export { CheckboxListEnumInput }; diff --git a/admin/src/components/CheckboxListInput.tsx b/admin/src/components/CheckboxListInput.tsx index 51a0d22..07d2776 100644 --- a/admin/src/components/CheckboxListInput.tsx +++ b/admin/src/components/CheckboxListInput.tsx @@ -26,14 +26,14 @@ const getEnumValues = (attribute: CheckboxListInputProps['attribute']): string[] return []; } - if (Array.isArray(attribute.enum)) { - return attribute.enum; - } - if (Array.isArray(attribute.options?.enum)) { return attribute.options.enum; } + if (Array.isArray(attribute.enum)) { + return attribute.enum; + } + return []; }; diff --git a/admin/src/index.ts b/admin/src/index.ts index a1c0473..6cab121 100644 --- a/admin/src/index.ts +++ b/admin/src/index.ts @@ -1,7 +1,9 @@ import { EnumerationField } from '@strapi/icons/symbols'; +import { CheckboxListEnumInput } from './components/CheckboxListEnumInput'; import { Initializer } from './components/Initializer'; import { CheckboxListDefaultInput } from './components/CheckboxListDefaultInput'; import { PLUGIN_ID } from './pluginId'; +import { checkboxListOptionsValidator } from './utils/checkboxListValidator'; export default { register(app: any) { @@ -19,6 +21,10 @@ export default { id: 'checkbox-list-default', component: CheckboxListDefaultInput, }); + ctbPlugin.apis.forms.components.add({ + id: 'checkbox-list-enum', + component: CheckboxListEnumInput, + }); } app.customFields.register({ @@ -47,8 +53,8 @@ export default { sectionTitle: null, items: [ { - name: 'enum', - type: 'textarea-enum', + name: 'options.enum', + type: 'checkbox-list-enum', size: 6, intlLabel: { id: 'form.attribute.item.enumeration.rules', @@ -58,6 +64,7 @@ export default { id: 'form.attribute.item.enumeration.placeholder', defaultMessage: 'Ex:\nmorning\nnoon\nevening', }, + defaultValue: [], validations: { required: true, }, @@ -128,6 +135,7 @@ export default { ], }, ], + validator: checkboxListOptionsValidator, }, }); }, diff --git a/admin/src/utils/checkboxListValidator.ts b/admin/src/utils/checkboxListValidator.ts new file mode 100644 index 0000000..771531f --- /dev/null +++ b/admin/src/utils/checkboxListValidator.ts @@ -0,0 +1,74 @@ +import slugify from '@sindresorhus/slugify'; +import { translatedErrors } from '@strapi/admin/strapi-admin'; +import * as yup from 'yup'; + +const GRAPHQL_ENUM_REGEX = /^[_A-Za-z][_0-9A-Za-z]*$/; + +const toRegressedEnumValue = (value?: string) => { + if (!value) { + return ''; + } + + return slugify(value, { + decamelize: false, + lowercase: false, + separator: '_', + }); +}; + +const hasUniqueValues = (values: string[]) => { + const seen = new Set<string>(); + + for (const value of values) { + if (seen.has(value)) { + return false; + } + + seen.add(value); + } + + return true; +}; + +export const checkboxListOptionsValidator = () => ({ + enum: yup + .array() + .of(yup.string()) + .min(1, translatedErrors.min.id) + .test({ + name: 'areEnumValuesUnique', + message: 'content-type-builder.error.validation.enum-duplicate', + test(values) { + if (!values) { + return false; + } + + const normalizedValues = values.map(toRegressedEnumValue); + + return hasUniqueValues(normalizedValues); + }, + }) + .test({ + name: 'doesNotHaveEmptyValues', + message: 'content-type-builder.error.validation.enum-empty-string', + test(values) { + if (!values) { + return false; + } + + return !values.map(toRegressedEnumValue).some((value) => value === ''); + }, + }) + .test({ + name: 'doesMatchRegex', + message: 'content-type-builder.error.validation.enum-regex', + test(values) { + if (!values) { + return false; + } + + return values.map(toRegressedEnumValue).every((value) => GRAPHQL_ENUM_REGEX.test(value)); + }, + }), + enumName: yup.string().nullable(), +}); |
