Skip to content

Commit

Permalink
✨ [#52] Add base for selectboxes edit form
Browse files Browse the repository at this point in the history
* Added zod validation schema
* Added base form layout (without options table & dynamic toggle)
* Wired up edit form to storybook
  • Loading branch information
sergei-maertens committed Nov 15, 2023
1 parent 0a7285d commit f99e436
Show file tree
Hide file tree
Showing 7 changed files with 257 additions and 6 deletions.
36 changes: 36 additions & 0 deletions src/components/ComponentConfiguration.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -777,6 +777,10 @@ export const FileUpload: Story = {
component: {
id: 'kiweljhr',
storage: 'url',
webcam: false,
options: {
withCredentials: true,
},
url: '',
type: 'file',
key: 'file',
Expand Down Expand Up @@ -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);
},
};
25 changes: 25 additions & 0 deletions src/registry/selectboxes/edit-validation.ts
Original file line number Diff line number Diff line change
@@ -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;
158 changes: 158 additions & 0 deletions src/registry/selectboxes/edit.tsx
Original file line number Diff line number Diff line change
@@ -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<SelectboxesComponentSchema> = () => {
const intl = useIntl();
const [isKeyManuallySetRef, generatedKey] = useDeriveComponentKey();
const {values, errors} = useFormikContext<SelectboxesComponentSchema>();

const erroredFields = Object.keys(errors).length
? getErrorNames<SelectboxesComponentSchema>(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<SelectboxesComponentSchema>(['required']);

return (
<Tabs>
<TabList>
<BuilderTabs.Basic
hasErrors={hasAnyError(
'label',
'key',
'description',
'tooltip',
'showInSummary',
'showInEmail',
'showInPDF',
'hidden',
'clearOnHide',
'isSensitiveData',
'openForms.dataSrc',
'openForms.itemsExpression',
'values',
'defaultValue'
)}
/>
<BuilderTabs.Advanced hasErrors={hasAnyError('conditional')} />
<BuilderTabs.Validation hasErrors={hasAnyError('validate')} />
<BuilderTabs.Registration hasErrors={hasAnyError('registration')} />
<BuilderTabs.Translations hasErrors={hasAnyError('openForms.translations')} />
</TabList>

{/* Basic tab */}
<TabPanel>
<Label />
<Key isManuallySetRef={isKeyManuallySetRef} generatedValue={generatedKey} />
<Description />
<Tooltip />
<PresentationConfig />
<Hidden />
<ClearOnHide />
<IsSensitiveData />
<ValuesTable<SelectboxesComponentSchema> name="values" />
{/*<DefaultValue multiple={!!values.multiple} />*/}
</TabPanel>

{/* Advanced tab */}
<TabPanel>
<SimpleConditional />
</TabPanel>

{/* Validation tab */}
<TabPanel>
<Validate.Required />
<Validate.ValidatorPluginSelect />
<Validate.ValidationErrorTranslations />
</TabPanel>

{/* Registration tab */}
<TabPanel>
<Registration.RegistrationAttributeSelect />
</TabPanel>

{/* Translations */}
<TabPanel>
<Translations.ComponentTranslations<SelectboxesComponentSchema>
propertyLabels={{
label: intl.formatMessage(LABELS.label),
description: intl.formatMessage(LABELS.description),
tooltip: intl.formatMessage(LABELS.tooltip),
}}
/>
</TabPanel>
</Tabs>
);
};

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;
11 changes: 6 additions & 5 deletions src/registry/selectboxes/index.ts
Original file line number Diff line number Diff line change
@@ -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: {},
};
3 changes: 2 additions & 1 deletion src/registry/selectboxes/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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[]} => {
Expand Down
25 changes: 25 additions & 0 deletions src/registry/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof literalSchema>;
type Json = Literal | {[key: string]: Json} | Json[];
export const jsonSchema: z.ZodType<Json> = 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
*/
Expand Down
5 changes: 5 additions & 0 deletions src/utils/errors.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ export function getErrorNames<Values = unknown>(errors: FormikErrors<Values>): 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}`];
Expand Down

0 comments on commit f99e436

Please sign in to comment.