From e52293b2abe052eef8d669b4cd3383a8159fd428 Mon Sep 17 00:00:00 2001 From: SilviaAmAm Date: Thu, 21 Mar 2024 11:15:01 +0100 Subject: [PATCH 1/2] :construction: [#4019] Handle special case for files The variable for file components has type 'array', but if only one file can be uploaded we want to be able to map it to attributes of type 'string' in the JSON schema. If multiple files can be uploaded, then we map it to 'array' attributes --- .../ObjectsApiVariableConfigurationEditor.js | 6 +++--- .../form_design/registrations/objectsapi/utils.js | 14 +++++++++++++- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/openforms/js/components/admin/form_design/registrations/objectsapi/ObjectsApiVariableConfigurationEditor.js b/src/openforms/js/components/admin/form_design/registrations/objectsapi/ObjectsApiVariableConfigurationEditor.js index 069a718f21..629027d90c 100644 --- a/src/openforms/js/components/admin/form_design/registrations/objectsapi/ObjectsApiVariableConfigurationEditor.js +++ b/src/openforms/js/components/admin/form_design/registrations/objectsapi/ObjectsApiVariableConfigurationEditor.js @@ -5,7 +5,7 @@ import React, {useContext} from 'react'; import {FormattedMessage} from 'react-intl'; import {useAsync, useToggle} from 'react-use'; -import {APIContext} from 'components/admin/form_design/Context'; +import {APIContext, FormContext} from 'components/admin/form_design/Context'; import {REGISTRATION_OBJECTS_TARGET_PATHS} from 'components/admin/form_design/constants'; import Field from 'components/admin/forms/Field'; import Fieldset from 'components/admin/forms/Fieldset'; @@ -34,7 +34,7 @@ import {asJsonSchema} from './utils'; */ const ObjectsApiVariableConfigurationEditor = ({variable}) => { const {csrftoken} = useContext(APIContext); - + const {components} = useContext(FormContext); const [jsonSchemaVisible, toggleJsonSchemaVisible] = useToggle(false); const {values: backendOptions, getFieldProps, setFieldValue} = useFormikContext(); @@ -73,7 +73,7 @@ const ObjectsApiVariableConfigurationEditor = ({variable}) => { const response = await post(REGISTRATION_OBJECTS_TARGET_PATHS, csrftoken, { objecttypeUrl: objecttype, objecttypeVersion, - variableJsonSchema: asJsonSchema(variable), + variableJsonSchema: asJsonSchema(variable, components), }); return response.data; diff --git a/src/openforms/js/components/admin/form_design/registrations/objectsapi/utils.js b/src/openforms/js/components/admin/form_design/registrations/objectsapi/utils.js index ed23097a14..1271f52538 100644 --- a/src/openforms/js/components/admin/form_design/registrations/objectsapi/utils.js +++ b/src/openforms/js/components/admin/form_design/registrations/objectsapi/utils.js @@ -48,9 +48,21 @@ const FORMAT_TYPE_MAP = { /** * Return a JSON Schema definition matching the provided variable. * @param {Object} variable - The current variable + * @param {Object} components - The components available in the form. The key is the component key, the value is the + * component definition. * @returns {Object} - The JSON Schema */ -const asJsonSchema = variable => { +const asJsonSchema = (variable, components) => { + // Figure out if the component is a file component (special case) + const componentDefinition = components[variable.key]; + if (componentDefinition && componentDefinition.type === 'file') { + // If it is, and it has multiple == True, then type is array + if (componentDefinition.multiple) + return {type: 'array', items: {type: 'string', format: 'uri'}}; + // Otherwise it's string (URL of the document) + return {type: 'string', format: 'uri'}; + } + if (VARIABLE_TYPE_MAP.hasOwnProperty(variable.dataType)) return {type: VARIABLE_TYPE_MAP[variable.dataType]}; return { From a5a4d5dc70252fa217088389f0c2ceb1e17f6e28 Mon Sep 17 00:00:00 2001 From: SilviaAmAm Date: Thu, 21 Mar 2024 14:54:38 +0100 Subject: [PATCH 2/2] :white_check_mark: [#4019] Add stories with test for mapping files --- .../registrations/objectsapi/mocks.js | 12 +- .../variables/VariablesEditor.stories.js | 212 +++++++++++++++--- 2 files changed, 196 insertions(+), 28 deletions(-) diff --git a/src/openforms/js/components/admin/form_design/registrations/objectsapi/mocks.js b/src/openforms/js/components/admin/form_design/registrations/objectsapi/mocks.js index 603f8690cd..b0c1cc80ce 100644 --- a/src/openforms/js/components/admin/form_design/registrations/objectsapi/mocks.js +++ b/src/openforms/js/components/admin/form_design/registrations/objectsapi/mocks.js @@ -21,6 +21,12 @@ export const mockObjecttypesError = () => }); export const mockTargetPathsPost = paths => - rest.post(`${BASE_URL}/api/v2/registration/plugins/objects-api/target-paths`, (req, res, ctx) => { - return res(ctx.json(paths)); - }); + rest.post( + `${BASE_URL}/api/v2/registration/plugins/objects-api/target-paths`, + async (req, res, ctx) => { + const requestBody = await req.json(); + const variableJsonSchemaType = requestBody.variableJsonSchema.type; + + return res(ctx.json(paths[variableJsonSchemaType])); + } + ); diff --git a/src/openforms/js/components/admin/form_design/variables/VariablesEditor.stories.js b/src/openforms/js/components/admin/form_design/variables/VariablesEditor.stories.js index 8edc70c5f4..3da2e49126 100644 --- a/src/openforms/js/components/admin/form_design/variables/VariablesEditor.stories.js +++ b/src/openforms/js/components/admin/form_design/variables/VariablesEditor.stories.js @@ -1,5 +1,6 @@ import {expect} from '@storybook/jest'; -import {userEvent, within} from '@storybook/testing-library'; +import {userEvent, waitFor, within} from '@storybook/testing-library'; +import {screen} from '@storybook/testing-library'; import {BACKEND_OPTIONS_FORMS} from 'components/admin/form_design/registrations'; import {mockTargetPathsPost} from 'components/admin/form_design/registrations/objectsapi/mocks'; @@ -34,6 +35,36 @@ export default { serviceFetchConfiguration: undefined, initialValue: '', }, + { + form: 'http://localhost:8000/api/v2/forms/36612390', + formDefinition: 'http://localhost:8000/api/v2/form-definitions/6de1ea5a', + name: 'Single File', + key: 'aSingleFile', + source: 'component', + prefillPlugin: '', + prefillAttribute: '', + prefillIdentifierRole: 'main', + dataType: 'array', + dataFormat: undefined, + isSensitiveData: false, + serviceFetchConfiguration: undefined, + initialValue: [], + }, + { + form: 'http://localhost:8000/api/v2/forms/36612390', + formDefinition: 'http://localhost:8000/api/v2/form-definitions/6de1ea5a', + name: 'Multiple File', + key: 'aMultipleFile', + source: 'component', + prefillPlugin: '', + prefillAttribute: '', + prefillIdentifierRole: 'main', + dataType: 'array', + dataFormat: undefined, + isSensitiveData: false, + serviceFetchConfiguration: undefined, + initialValue: [], + }, { form: 'http://localhost:8000/api/v2/forms/36612390', formDefinition: undefined, @@ -67,6 +98,22 @@ export default { initialValue: '2024-02-27T16:44:22.170405Z', }, ], + availableComponents: { + aSingleFile: { + type: 'file', + multiple: false, + key: 'aSingleFile', + }, + aMultipleFile: { + type: 'file', + multiple: true, + key: 'aMultipleFile', + }, + formioComponent: { + key: 'formioComponent', + type: 'textfield', + }, + }, }, argTypes: { onChange: {action: true}, @@ -181,18 +228,22 @@ export const WithObjectsAPIRegistrationBackends = { parameters: { msw: { handlers: [ - mockTargetPathsPost([ - { - targetPath: ['path', 'to.the', 'target'], - isRequired: true, - jsonSchema: {type: 'string'}, - }, - { - targetPath: ['other', 'path'], - isRequired: false, - jsonSchema: {type: 'object', properties: {a: {type: 'string'}}, required: ['a']}, - }, - ]), + mockTargetPathsPost({ + string: [ + { + targetPath: ['path', 'to.the', 'target'], + isRequired: true, + jsonSchema: {type: 'string'}, + }, + ], + object: [ + { + targetPath: ['other', 'path'], + isRequired: false, + jsonSchema: {type: 'object', properties: {a: {type: 'string'}}, required: ['a']}, + }, + ], + }), ], }, }, @@ -211,6 +262,113 @@ export const WithObjectsAPIRegistrationBackends = { }, }; +export const FilesMappingAndObjectAPIRegistration = { + args: { + registrationBackends: [ + { + backend: 'objects_api', + key: 'objects_api_1', + name: 'Example Objects API reg.', + options: { + version: 2, + objecttype: + 'https://objecttypen.nl/api/v1/objecttypes/2c77babf-a967-4057-9969-0200320d23f1', + objecttypeVersion: 2, + variablesMapping: [ + { + variableKey: 'formioComponent', + targetPath: ['path', 'to.the', 'target'], + }, + { + variableKey: 'userDefined', + targetPath: ['other', 'path'], + }, + ], + }, + }, + ], + registrationPluginsVariables: [ + { + pluginIdentifier: 'objects_api', + pluginVerboseName: 'Objects API registration', + }, + ], + onFieldChange: data => { + console.log(data); + }, + }, + parameters: { + msw: { + handlers: [ + mockTargetPathsPost({ + string: [ + { + targetPath: ['path', 'to.the', 'target'], + isRequired: true, + jsonSchema: {type: 'string'}, + }, + { + targetPath: ['path', 'to', 'uri'], + isRequired: true, + jsonSchema: { + type: 'string', + format: 'uri', + }, + }, + ], + object: [ + { + targetPath: ['other', 'path'], + isRequired: false, + jsonSchema: {type: 'object', properties: {a: {type: 'string'}}, required: ['a']}, + }, + ], + array: [ + { + targetPath: ['path', 'to', 'array'], + isRequired: true, + jsonSchema: {type: 'array'}, + }, + ], + }), + ], + }, + }, + play: async ({canvasElement}) => { + const canvas = within(canvasElement); + + const editIcons = canvas.getAllByTitle('Registratie-instellingen bewerken'); + + // The second icon is for the single file upload component variable + userEvent.click(editIcons[1]); + + const targetSchemaDropdown = await screen.findByRole('combobox'); + + await expect(targetSchemaDropdown).toBeInTheDocument(); + + // Only the targets of type string should appear + await expect( + await screen.findByRole('option', {name: 'path > to.the > target (required)'}) + ).toBeVisible(); + await expect( + await screen.findByRole('option', {name: 'path > to > uri (required)'}) + ).toBeVisible(); + + const saveButton = screen.getByRole('button', {name: 'Opslaan'}); + userEvent.click(saveButton); + + // The third icon is for the multiple file upload component variable + userEvent.click(editIcons[2]); + + const dropdown = await screen.findByRole('combobox'); + + await expect(dropdown).toBeInTheDocument(); + await expect( + await screen.findByRole('option', {name: 'path > to > array (required)'}) + ).toBeVisible(); + }, +}; + export const WithObjectsAPIAndTestRegistrationBackends = { args: { registrationBackends: [ @@ -293,18 +451,22 @@ export const WithObjectsAPIAndTestRegistrationBackends = { parameters: { msw: { handlers: [ - mockTargetPathsPost([ - { - targetPath: ['path', 'to.the', 'target'], - isRequired: true, - jsonSchema: {type: 'string'}, - }, - { - targetPath: ['other', 'path'], - isRequired: false, - jsonSchema: {type: 'object', properties: {a: {type: 'string'}}, required: ['a']}, - }, - ]), + mockTargetPathsPost({ + string: [ + { + targetPath: ['path', 'to.the', 'target'], + isRequired: true, + jsonSchema: {type: 'string'}, + }, + ], + object: [ + { + targetPath: ['other', 'path'], + isRequired: false, + jsonSchema: {type: 'object', properties: {a: {type: 'string'}}, required: ['a']}, + }, + ], + }), ], }, },