diff --git a/src/components/ComponentConfiguration.stories.tsx b/src/components/ComponentConfiguration.stories.tsx index 62f0531b..44f9a35d 100644 --- a/src/components/ComponentConfiguration.stories.tsx +++ b/src/components/ComponentConfiguration.stories.tsx @@ -777,6 +777,10 @@ export const FileUpload: Story = { component: { id: 'kiweljhr', storage: 'url', + webcam: false, + options: { + withCredentials: true, + }, url: '', type: 'file', key: 'file', @@ -900,3 +904,35 @@ export const FileUpload: Story = { }); }, }; + +export const SelectBoxes: Story = { + render: Template, + name: 'type: selectboxes', + + args: { + component: { + id: 'wqimsadk', + type: 'selectboxes', + key: 'selectboxes', + label: 'A selectboxes field', + openForms: { + dataSrc: 'manual', + translations: {}, + }, + values: [], + defaultValue: {}, + }, + + builderInfo: { + title: 'Select Boxes', + icon: 'plus-square', + group: 'basic', + weight: 60, + schema: {}, + }, + }, + + play: async ({canvasElement, step, args}) => { + const canvas = within(canvasElement); + }, +}; diff --git a/src/registry/selectboxes/edit-validation.ts b/src/registry/selectboxes/edit-validation.ts new file mode 100644 index 00000000..24405eb7 --- /dev/null +++ b/src/registry/selectboxes/edit-validation.ts @@ -0,0 +1,25 @@ +import {IntlShape} from 'react-intl'; +import {z} from 'zod'; + +import {buildCommonSchema, jsonSchema, optionSchema} from '@/registry/validation'; + +const manualValuesSchema = z.object({ + openForms: z.object({ + dataSrc: z.literal('manual'), + }), + values: optionSchema.array().min(1), +}); + +const variableValuesSchema = z.object({ + openForms: z.object({ + dataSrc: z.literal('variable'), + // TODO: wire up infernologic type checking + itemsExpression: jsonSchema, + }), +}); + +const valuesSchema = manualValuesSchema.or(variableValuesSchema); + +const schema = (intl: IntlShape) => buildCommonSchema(intl).and(valuesSchema); + +export default schema; diff --git a/src/registry/selectboxes/edit.tsx b/src/registry/selectboxes/edit.tsx new file mode 100644 index 00000000..3750d991 --- /dev/null +++ b/src/registry/selectboxes/edit.tsx @@ -0,0 +1,158 @@ +import {SelectboxesComponentSchema} from '@open-formulieren/types'; +import {useFormikContext} from 'formik'; +import {FormattedMessage, useIntl} from 'react-intl'; + +import { + BuilderTabs, + ClearOnHide, + Description, + Hidden, + IsSensitiveData, + Key, + Label, + PresentationConfig, + Registration, + SimpleConditional, + Tooltip, + Translations, + Validate, + ValuesTable, + useDeriveComponentKey, +} from '@/components/builder'; +import {LABELS} from '@/components/builder/messages'; +import {TabList, TabPanel, Tabs} from '@/components/formio'; +import {getErrorNames} from '@/utils/errors'; + +import {EditFormDefinition} from '../types'; + +/** + * Form to configure a Formio 'selectboxes' type component. + */ +const EditForm: EditFormDefinition = () => { + const intl = useIntl(); + const [isKeyManuallySetRef, generatedKey] = useDeriveComponentKey(); + const {values, errors} = useFormikContext(); + + const erroredFields = Object.keys(errors).length + ? getErrorNames(errors) + : []; + // TODO: pattern match instead of just string inclusion? + // TODO: move into more generically usuable utility when we implement other component + // types + const hasAnyError = (...fieldNames: string[]): boolean => { + if (!erroredFields.length) return false; + return fieldNames.some(name => erroredFields.includes(name)); + }; + + Validate.useManageValidatorsTranslations(['required']); + + return ( + + + + + + + + + + {/* Basic tab */} + + + + {/* Advanced tab */} + + + + + {/* Validation tab */} + + + + + + + {/* Registration tab */} + + + + + {/* Translations */} + + + propertyLabels={{ + label: intl.formatMessage(LABELS.label), + description: intl.formatMessage(LABELS.description), + tooltip: intl.formatMessage(LABELS.tooltip), + }} + /> + + + ); +}; + +EditForm.defaultValues = { + // basic tab + label: '', + key: '', + description: '', + tooltip: '', + showInSummary: true, + showInEmail: false, + showInPDF: true, + hidden: false, + clearOnHide: true, + isSensitiveData: false, + openForms: { + dataSrc: 'manual', + translations: {}, + }, + values: [{value: '', label: ''}], + // TODO: check that the initial values are set based on component.values + // TODO: at some point we can allow an itemsExpression for this too + defaultValue: {}, + // Advanced tab + conditional: { + show: undefined, + when: '', + eq: '', + }, + // Validation tab + validate: { + required: false, + plugins: [], + }, + translatedErrors: {}, + registration: { + attribute: '', + }, +}; + +export default EditForm; diff --git a/src/registry/selectboxes/index.ts b/src/registry/selectboxes/index.ts index 8178ec42..76237530 100644 --- a/src/registry/selectboxes/index.ts +++ b/src/registry/selectboxes/index.ts @@ -1,10 +1,11 @@ -// import EditForm from './edit'; -// import validationSchema from './edit-validation'; +import EditForm from './edit'; +import validationSchema from './edit-validation'; import Preview from './preview'; export default { - // edit: EditForm, - // editSchema: validationSchema, + edit: EditForm, + editSchema: validationSchema, preview: Preview, - // defaultValue: '', + // default empty value for Formik - this ignores any manually configured options! + defaultValue: {}, }; diff --git a/src/registry/selectboxes/preview.tsx b/src/registry/selectboxes/preview.tsx index fae2ecda..c84bd15b 100644 --- a/src/registry/selectboxes/preview.tsx +++ b/src/registry/selectboxes/preview.tsx @@ -7,7 +7,8 @@ import {CheckboxInput} from '@/components/formio/checkbox'; import {ComponentPreviewProps} from '../types'; -// FIXME: this union does not seem to be properly inferred :( +// A type guard is needed because TS cannot figure out it's a discriminated union +// when the discriminator is nested. const checkIsManualOptions = ( component: SelectboxesComponentSchema ): component is SelectboxesComponentSchema & {values: Option[]} => { diff --git a/src/registry/validation.ts b/src/registry/validation.ts index f49e6171..4704eb55 100644 --- a/src/registry/validation.ts +++ b/src/registry/validation.ts @@ -74,6 +74,31 @@ export const buildCommonSchema = (intl: IntlShape) => key: buildKeySchema(intl), }); +// From https://zod.dev/?id=json-type +const literalSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]); +type Literal = z.infer; +type Json = Literal | {[key: string]: Json} | Json[]; +export const jsonSchema: z.ZodType = z.lazy(() => + z.union([literalSchema, z.array(jsonSchema), z.record(jsonSchema)]) +); + +// Maps to @open-formulieren/types common.ts Option type. +const optionTranslationSchema = z.object({ + label: z.string(), +}); + +export const optionSchema = z.object({ + value: z.string(), + label: z.string(), + openForms: z + .object({ + // zod doesn't seem to be able to use our supportedLanguageCodes for z.object keys, + // they need to be defined statically. So, 'record' it is. + translations: z.record(optionTranslationSchema.optional()), + }) + .optional(), +}); + /* Helpers */ diff --git a/src/utils/errors.tsx b/src/utils/errors.tsx index c630cfec..634c2361 100644 --- a/src/utils/errors.tsx +++ b/src/utils/errors.tsx @@ -7,6 +7,11 @@ export function getErrorNames(errors: FormikErrors): s Object.entries(errors).forEach(([key, nested]) => { if (Array.isArray(nested)) { const nestedNames = nested.map((item, index) => { + // ArrayField and arrayHelpers does some questionable things, see Github issue: + // https://github.com/jaredpalmer/formik/issues/1811#issuecomment-552029935 + if (item === undefined) { + return []; + } if (typeof item === 'string') { if (item) { return [`${key}.${index}`];