{expression}
",
+ "description": "Selectboxes dummy option for itemsExpression",
+ "originalDefault": "Options from expression: {expression}
"
+ },
"pq7q6N": {
"defaultMessage": "Show in PDF",
"description": "Label for 'showInPDF' builder field",
@@ -764,6 +854,11 @@
"description": "Component edit form tab title for 'Registration' tab",
"originalDefault": "Registration"
},
+ "sU/n0S": {
+ "defaultMessage": "Manually fill in",
+ "description": "Data source option label for value 'manual'",
+ "originalDefault": "Manually fill in"
+ },
"sgmcmf": {
"defaultMessage": "Plugin attribute",
"description": "Label for 'prefill.attribute' builder field",
@@ -804,6 +899,16 @@
"description": "Tooltip for 'prefill.attribute' builder field",
"originalDefault": "Specify the attribute holding the pre-fill data."
},
+ "wOq8Pb": {
+ "defaultMessage": "Translation for option with value \"{value}\"",
+ "description": "Accessible label for option label translation field",
+ "originalDefault": "Translation for option with value \"{value}\""
+ },
+ "wRNK2B": {
+ "defaultMessage": "Value",
+ "description": "Option value table header/label",
+ "originalDefault": "Value"
+ },
"wZ99FU": {
"defaultMessage": "The regular expression pattern test that the field value must pass before the form can be submitted.",
"description": "Tooltip for 'validate.pattern' builder field",
@@ -839,6 +944,11 @@
"description": "Tooltip for 'Hidden' builder field",
"originalDefault": "Hide a field from the form."
},
+ "ySkT14": {
+ "defaultMessage": "How to specify the available options.",
+ "description": "Tooltip for 'openForms.dataSrc' builder field",
+ "originalDefault": "How to specify the available options."
+ },
"yujuQr": {
"defaultMessage": "Maximum height",
"description": "Label for 'of.image.resize.height' builder field",
diff --git a/i18n/messages/nl.json b/i18n/messages/nl.json
index a374af94..2869be3e 100644
--- a/i18n/messages/nl.json
+++ b/i18n/messages/nl.json
@@ -4,6 +4,11 @@
"description": "Component property 'Property Name' label",
"originalDefault": "Property Name"
},
+ "+9x9sg": {
+ "defaultMessage": "Gebruik variabele",
+ "description": "Data source option label for value 'variable'",
+ "originalDefault": "From variable"
+ },
"+XwAuT": {
"defaultMessage": "Verwijderen",
"description": "Remove component button",
@@ -65,7 +70,7 @@
"originalDefault": "Regular Expression Pattern"
},
"1pC7IP": {
- "defaultMessage": "Specifieer een template voor de naam van het/de ge\u00fcploade bestand(en). '{{ fileName }}'
bevat de oorspronkelijke bestandsnaam.",
+ "defaultMessage": "Specifieer een template voor de naam van het/de geüploade bestand(en). '{{ fileName }}'
bevat de oorspronkelijke bestandsnaam.",
"description": "Tooltip for 'file.name' builder field",
"originalDefault": "Specify template for name of uploaded file(s). '{{ fileName }}'
contains the original filename."
},
@@ -154,6 +159,11 @@
"description": "Character count",
"originalDefault": "{length} {length, plural, one {character} other {characters}}"
},
+ "5xNPpS": {
+ "defaultMessage": "Optielabel",
+ "description": "Accessible label for option label",
+ "originalDefault": "Option label"
+ },
"5y8Yt4": {
"defaultMessage": "Sla het bestand op in de Documenten API met dit documenttype. Indien leeg, dan worden algemene instellingen gebruikt.",
"description": "Tooltip for 'registration.informatieobjecttype' builder field",
@@ -234,11 +244,21 @@
"description": "Placeholder for 'validate.min' builder field",
"originalDefault": "Minimum value"
},
+ "BKe/DT": {
+ "defaultMessage": "De optiewaarde is een verplicht veld.",
+ "description": "Form builder option value required error",
+ "originalDefault": "The option value is a required field."
+ },
"BvRBef": {
"defaultMessage": "Decimalen",
"description": "Label for 'decimalLimit' builder field",
"originalDefault": "Decimal places"
},
+ "C48xJT": {
+ "defaultMessage": "Opties-expressie",
+ "description": "Label for 'openForms.itemsExpression' builder field",
+ "originalDefault": "Items expression"
+ },
"C64ptd": {
"defaultMessage": "Maximale lengte",
"description": "Placeholder for 'validate.maxLength' builder field",
@@ -265,6 +285,11 @@
"description": "Tooltip validation error translations panel",
"originalDefault": "Custom error messages for this component and their translations"
},
+ "DJWATl": {
+ "defaultMessage": "De mogelijke keuzeopties voor dit veld. De waarden worden in de formuliergegevens opgeslagen. De labels zijn de teksten die aan de gebruiker getoond worden.",
+ "description": "Tooltip for 'values' builder field",
+ "originalDefault": "The values that can be picked for this field. Values are the text that is submitted with the form data. Labels are the text next to radio buttons, checkboxes and options in dropdowns."
+ },
"Dm3S1P": {
"defaultMessage": "Placeholder",
"description": "Component property 'Placeholder' label",
@@ -311,6 +336,12 @@
"description": "Save component configuration button",
"originalDefault": "Save"
},
+ "FOlQaP": {
+ "defaultMessage": "Label",
+ "description": "Option label table header/label",
+ "isTranslated": true,
+ "originalDefault": "Label"
+ },
"Fc/emc": {
"defaultMessage": "Nog één toevoegen",
"description": "'Add another' button text for 'multiple' components",
@@ -331,6 +362,11 @@
"description": "Label for 'ReadOnly' builder field",
"originalDefault": "Read only"
},
+ "IOko1U": {
+ "defaultMessage": "Verwijderen",
+ "description": "Values table: accessible label to remove an option",
+ "originalDefault": "Remove"
+ },
"JDYF2q": {
"defaultMessage": "Gegevens opgevoerd in dit component worden geschoond volgens de privacy-instellingen.",
"description": "Tooltip for 'IsSensitiveData' builder field",
@@ -352,6 +388,11 @@
"description": "Label for 'Hidden' builder field",
"originalDefault": "Hidden"
},
+ "Kj8P8I": {
+ "defaultMessage": "Optiewaarde",
+ "description": "Accessible label for option value",
+ "originalDefault": "Option value"
+ },
"KrJ+rN": {
"defaultMessage": "Het maximaal aantal bestanden die mogen geüpload worden.",
"description": "Tooltip for 'maxNumberOfFiles' builder field",
@@ -403,6 +444,11 @@
"description": "Label identifier role main",
"originalDefault": "Main"
},
+ "RuAQwk": {
+ "defaultMessage": "Verwijderen",
+ "description": "Option add/remove table header/label",
+ "originalDefault": "Add/remove"
+ },
"RxmRzd": {
"defaultMessage": "Indien ingeschakeld, gebruik dan de algemene instellingen voor toegestane bestandstypen.",
"description": "Tooltip for 'useConfigFiletypes' builder field",
@@ -444,6 +490,16 @@
"description": "Tooltip for 'validate.plugins' builder field",
"originalDefault": "Select the plugin(s) to use for the validation functionality."
},
+ "UlVtQd": {
+ "defaultMessage": "Omhoog",
+ "description": "Options table: move option up",
+ "originalDefault": "Move up"
+ },
+ "UmLruQ": {
+ "defaultMessage": "Keuzeopties",
+ "description": "Label for 'openForms.dataSrc' builder field",
+ "originalDefault": "Data source"
+ },
"Un5b5T": {
"defaultMessage": "Minimale tijd",
"description": "Label for 'validate.minTime' builder field",
@@ -539,6 +595,11 @@
"description": "Label for 'maxNumberOfFiles' builder field",
"originalDefault": "Maximum number of files"
},
+ "arAMAF": {
+ "defaultMessage": "Het optielabel is een verplicht veld.",
+ "description": "Form builder option label required error",
+ "originalDefault": "The option label is a required field."
+ },
"asZV7t": {
"defaultMessage": "Straatnaam afleiden",
"description": "Label for 'deriveStreetName' builder field",
@@ -656,6 +717,11 @@
"description": "Label for 'registration.informatieobjecttype' builder field",
"originalDefault": "Information object type"
},
+ "fe3wyn": {
+ "defaultMessage": "Geef een JsonLogic expressie voor de lijst van mogelijke opties. Evaluatie van de expressie moet een lijst teruggeven, waarbij elk element een lijst (array) is van [waarde, label] combinaties.",
+ "description": "Description for the 'openForms.itemsExpression' builder field",
+ "originalDefault": "A JSON logic expression returning a variable (of array type) whose items should be used as the options for this component."
+ },
"h0B9Fr": {
"defaultMessage": "De eigenschapsnaam mag alleen alfanumerieke tekens, onderstrepingstekens, punten en streepjes bevatten en mag niet worden afgesloten met een streepje of punt.",
"description": "Form builder 'key' pattern validation error",
@@ -686,16 +752,31 @@
"description": "Label for 'ClearOnHide' builder field",
"originalDefault": "Clear on hide"
},
+ "j2vQH3": {
+ "defaultMessage": "Waarden",
+ "description": "Label for the 'values' builder field",
+ "originalDefault": "Values"
+ },
"jLg5l6": {
"defaultMessage": "Huisnummercomponent",
"description": "Label for 'deriveHouseNumber' builder field",
"originalDefault": "House number component"
},
+ "k1+ljn": {
+ "defaultMessage": "Omlaag",
+ "description": "Options table: move option down",
+ "originalDefault": "Move down"
+ },
"k9tAyW": {
"defaultMessage": "Foutmelding",
"description": "Label for translation message for validation error code",
"originalDefault": "Error message"
},
+ "kg/eh1": {
+ "defaultMessage": "Waardenvertalingen",
+ "description": "Values/options translations table header",
+ "originalDefault": "Choice/option translations"
+ },
"kkTfpu": {
"defaultMessage": "Beschrijving",
"description": "Component property 'Description' label",
@@ -758,11 +839,21 @@
"description": "Component translations 'suffix' property label",
"originalDefault": "Suffix (e.g. m²)"
},
+ "p7g2h+": {
+ "defaultMessage": "Nog één toevoegen",
+ "description": "Add another option button label",
+ "originalDefault": "Add another"
+ },
"pDJBaK": {
"defaultMessage": "Minimale waarde",
"description": "Label for 'validate.min' builder field",
"originalDefault": "Minimum value"
},
+ "paY2Oa": {
+ "defaultMessage": "Waarden via de expressie: {expression}
",
+ "description": "Selectboxes dummy option for itemsExpression",
+ "originalDefault": "Options from expression: {expression}
"
+ },
"pq7q6N": {
"defaultMessage": "Weergeven in PDF",
"description": "Label for 'showInPDF' builder field",
@@ -773,6 +864,11 @@
"description": "Component edit form tab title for 'Registration' tab",
"originalDefault": "Registration"
},
+ "sU/n0S": {
+ "defaultMessage": "Handmatig opvoeren",
+ "description": "Data source option label for value 'manual'",
+ "originalDefault": "Manually fill in"
+ },
"sgmcmf": {
"defaultMessage": "Pluginattribuut",
"description": "Label for 'prefill.attribute' builder field",
@@ -813,6 +909,16 @@
"description": "Tooltip for 'prefill.attribute' builder field",
"originalDefault": "Specify the attribute holding the pre-fill data."
},
+ "wOq8Pb": {
+ "defaultMessage": "Vertaling voor het label van de optie met waarde \"{value}\"",
+ "description": "Accessible label for option label translation field",
+ "originalDefault": "Translation for option with value \"{value}\""
+ },
+ "wRNK2B": {
+ "defaultMessage": "Waarde",
+ "description": "Option value table header/label",
+ "originalDefault": "Value"
+ },
"wZ99FU": {
"defaultMessage": "Het patroon van reguliere expressie waar de waarde aan moet voldoen voor het formulier kan verstuurd worden.",
"description": "Tooltip for 'validate.pattern' builder field",
@@ -848,6 +954,11 @@
"description": "Tooltip for 'Hidden' builder field",
"originalDefault": "Hide a field from the form."
},
+ "ySkT14": {
+ "defaultMessage": "Selecteer een databron voor de keuzeopties.",
+ "description": "Tooltip for 'openForms.dataSrc' builder field",
+ "originalDefault": "How to specify the available options."
+ },
"yujuQr": {
"defaultMessage": "Maximale hoogte",
"description": "Label for 'of.image.resize.height' builder field",
diff --git a/package-lock.json b/package-lock.json
index fcb13767..ce364a33 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -14444,9 +14444,9 @@
}
},
"node_modules/@testing-library/user-event": {
- "version": "14.4.3",
- "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.4.3.tgz",
- "integrity": "sha512-kCUc5MEwaEMakkO5x7aoD+DLi02ehmEM2QCGWvNqAS1dV/fAvORWEjnjsEIvml59M7Y5kCkWN6fCCyPOe8OL6Q==",
+ "version": "14.5.1",
+ "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.5.1.tgz",
+ "integrity": "sha512-UCcUKrUYGj7ClomOo2SpNVvx4/fkd/2BbIHDCle8A0ax+P3bU7yJwDBDrS6ZwdTMARWTGODX1hEsCcO+7beJjg==",
"dev": true,
"engines": {
"node": ">=12",
@@ -41192,9 +41192,9 @@
}
},
"@testing-library/user-event": {
- "version": "14.4.3",
- "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.4.3.tgz",
- "integrity": "sha512-kCUc5MEwaEMakkO5x7aoD+DLi02ehmEM2QCGWvNqAS1dV/fAvORWEjnjsEIvml59M7Y5kCkWN6fCCyPOe8OL6Q==",
+ "version": "14.5.1",
+ "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.5.1.tgz",
+ "integrity": "sha512-UCcUKrUYGj7ClomOo2SpNVvx4/fkd/2BbIHDCle8A0ax+P3bU7yJwDBDrS6ZwdTMARWTGODX1hEsCcO+7beJjg==",
"dev": true
},
"@trivago/prettier-plugin-sort-imports": {
diff --git a/src/components/ComponentConfiguration.stories.tsx b/src/components/ComponentConfiguration.stories.tsx
index 62f0531b..1ffe3f96 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,208 @@ 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);
+ const editForm = within(canvas.getByTestId('componentEditForm'));
+ const preview = within(canvas.getByTestId('componentPreview'));
+
+ await expect(canvas.getByLabelText('Label')).toHaveValue('A selectboxes field');
+ await waitFor(async () => {
+ await expect(canvas.getByLabelText('Property Name')).toHaveValue('aSelectboxesField');
+ });
+ await expect(canvas.getByLabelText('Description')).toHaveValue('');
+ await expect(canvas.getByLabelText('Tooltip')).toHaveValue('');
+ await expect(canvas.getByLabelText('Show in summary')).toBeChecked();
+ await expect(canvas.getByLabelText('Show in email')).not.toBeChecked();
+ await expect(canvas.getByLabelText('Show in PDF')).toBeChecked();
+
+ // ensure that changing fields in the edit form properly update the preview
+
+ await userEvent.clear(canvas.getByLabelText('Label'));
+ await userEvent.type(canvas.getByLabelText('Label'), 'Updated preview label');
+ expect(await preview.findByText('Updated preview label'));
+
+ const previewInput = preview.getByRole('checkbox');
+ await expect(previewInput).not.toBeChecked();
+
+ // Ensure that the manually entered key is kept instead of derived from the label,
+ // even when key/label components are not mounted.
+ const keyInput = canvas.getByLabelText('Property Name');
+ // fireEvent is deliberate, as userEvent.clear + userEvent.type briefly makes the field
+ // not have any value, which triggers the generate-key-from-label behaviour.
+ fireEvent.change(keyInput, {target: {value: 'customKey'}});
+ await userEvent.click(canvas.getByRole('tab', {name: 'Basic'}));
+ await userEvent.clear(canvas.getByLabelText('Label'));
+ await userEvent.type(canvas.getByLabelText('Label'), 'Other label', {delay: 50});
+ await expect(canvas.getByLabelText('Property Name')).toHaveDisplayValue('customKey');
+
+ await step('Set up manual options', async () => {
+ // enter some possible options
+ const firstOptionLabelInput = canvas.getByLabelText('Option label');
+ expect(firstOptionLabelInput).toHaveDisplayValue('');
+ await userEvent.type(firstOptionLabelInput, 'Option label 1');
+ const firstOptionValue = canvas.getByLabelText('Option value');
+ await waitFor(() => expect(firstOptionValue).toHaveDisplayValue('optionLabel1'));
+
+ // add a second option
+ await userEvent.click(canvas.getByRole('button', {name: 'Add another'}));
+ const optionLabels = canvas.queryAllByLabelText('Option label');
+ const optionValues = canvas.queryAllByLabelText('Option value');
+ expect(optionLabels).toHaveLength(2);
+ expect(optionValues).toHaveLength(2);
+ await userEvent.type(optionValues[1], 'manualValue');
+ await userEvent.type(optionLabels[1], 'Second option');
+
+ await userEvent.click(canvas.getByRole('button', {name: 'Save'}));
+ expect(args.onSubmit).toHaveBeenCalledWith({
+ id: 'wqimsadk',
+ type: 'selectboxes',
+ // basic tab
+ label: 'Other label',
+ key: 'customKey',
+ description: '',
+ tooltip: '',
+ showInSummary: true,
+ showInEmail: false,
+ showInPDF: true,
+ hidden: false,
+ clearOnHide: true,
+ isSensitiveData: false,
+ openForms: {
+ dataSrc: 'manual',
+ translations: {},
+ },
+ values: [
+ {
+ value: 'optionLabel1',
+ label: 'Option label 1',
+ },
+ {
+ value: 'manualValue',
+ label: 'Second option',
+ openForms: {translations: {}},
+ },
+ ],
+ defaultValue: {
+ optionLabel1: false,
+ manualValue: false,
+ },
+ // Advanced tab
+ conditional: {
+ show: undefined,
+ when: '',
+ eq: '',
+ },
+ // Validation tab
+ validate: {
+ required: false,
+ plugins: [],
+ },
+ translatedErrors: {
+ nl: {required: ''},
+ },
+ // registration tab
+ registration: {
+ attribute: '',
+ },
+ });
+ // @ts-expect-error
+ args.onSubmit.mockClear();
+ });
+
+ await step('Option labels are translatable', async () => {
+ await userEvent.click(canvas.getByRole('tab', {name: 'Translations'}));
+
+ // check that the option labels are in the translations table
+ expect(await editForm.findByText('Option label 1')).toBeVisible();
+ expect(await editForm.findByText('Second option')).toBeVisible();
+ });
+
+ await step('Set up itemsExpression for options', async () => {
+ await userEvent.click(canvas.getByRole('tab', {name: 'Basic'}));
+
+ canvas.getByLabelText('Data source').focus();
+ await userEvent.keyboard('[ArrowDown]');
+ await userEvent.click(await canvas.findByText('From variable'));
+ const itemsExpressionInput = canvas.getByLabelText('Items expression');
+ await userEvent.clear(itemsExpressionInput);
+ // { needs to be escaped: https://github.com/testing-library/user-event/issues/584
+ const expression = '{"var": "someVar"}'.replace(/[{[]/g, '$&$&');
+ await userEvent.type(itemsExpressionInput, expression);
+
+ await expect(editForm.queryByLabelText('Default value')).toBeNull();
+ await expect(preview.getByRole('checkbox', {name: /Options from expression:/})).toBeVisible();
+
+ await userEvent.click(canvas.getByRole('button', {name: 'Save'}));
+ expect(args.onSubmit).toHaveBeenCalledWith({
+ id: 'wqimsadk',
+ type: 'selectboxes',
+ // basic tab
+ label: 'Other label',
+ key: 'customKey',
+ description: '',
+ tooltip: '',
+ showInSummary: true,
+ showInEmail: false,
+ showInPDF: true,
+ hidden: false,
+ clearOnHide: true,
+ isSensitiveData: false,
+ openForms: {
+ dataSrc: 'variable',
+ itemsExpression: {var: 'someVar'},
+ translations: {},
+ },
+ defaultValue: {},
+ // Advanced tab
+ conditional: {
+ show: undefined,
+ when: '',
+ eq: '',
+ },
+ // Validation tab
+ validate: {
+ required: false,
+ plugins: [],
+ },
+ translatedErrors: {
+ nl: {required: ''},
+ },
+ // registration tab
+ registration: {
+ attribute: '',
+ },
+ });
+ // @ts-expect-error
+ args.onSubmit.mockClear();
+ });
+ },
+};
diff --git a/src/components/ComponentPreview.stories.tsx b/src/components/ComponentPreview.stories.tsx
index c197afc9..73728894 100644
--- a/src/components/ComponentPreview.stories.tsx
+++ b/src/components/ComponentPreview.stories.tsx
@@ -1,6 +1,6 @@
import {expect} from '@storybook/jest';
import {Meta, StoryFn, StoryObj} from '@storybook/react';
-import {userEvent, within} from '@storybook/testing-library';
+import {fireEvent, userEvent, within} from '@storybook/testing-library';
import ComponentPreview from './ComponentPreview';
@@ -615,3 +615,72 @@ export const File: Story = {
await canvas.findByText('A preview of the file Formio component');
},
};
+
+export const SelectBoxes: Story = {
+ name: 'Selectboxes manual values',
+ render: Template,
+
+ args: {
+ component: {
+ type: 'selectboxes',
+ id: 'selectboxes',
+ key: 'selectboxesPreview',
+ label: 'Selectboxes preview',
+ description: 'A preview of the selectboxes Formio component',
+ openForms: {
+ dataSrc: 'manual',
+ translations: {},
+ },
+ values: [
+ {
+ value: 'option1',
+ label: 'Option 1',
+ },
+ {
+ value: 'option2',
+ label: 'Option 2',
+ },
+ ],
+ },
+ },
+
+ play: async ({canvasElement, args}) => {
+ const canvas = within(canvasElement);
+
+ // check that the user-controlled content is visible
+ await canvas.findByText('Selectboxes preview');
+ await canvas.findByText('A preview of the selectboxes Formio component');
+
+ // check that the input name is set correctly
+ const firstOptionInput = canvas.getByLabelText