From 10d7576842f397621df8c110798a0e786a58313d Mon Sep 17 00:00:00 2001 From: Sergei Maertens Date: Mon, 16 Oct 2023 17:47:05 +0200 Subject: [PATCH 1/3] :sparkles: [#2] Define type for file component, pt. 1 Mostly describes the base schema and how a single file upload looks like, i.e. what data is chucked over the wire when submitting the form. --- src/formio/components/file.ts | 97 +++++++++++++++++++++++++ src/formio/components/index.ts | 1 + src/formio/index.ts | 2 + test-d/formio/components/file.test-d.ts | 66 +++++++++++++++++ 4 files changed, 166 insertions(+) create mode 100644 src/formio/components/file.ts create mode 100644 test-d/formio/components/file.test-d.ts diff --git a/src/formio/components/file.ts b/src/formio/components/file.ts new file mode 100644 index 0000000..aea4d84 --- /dev/null +++ b/src/formio/components/file.ts @@ -0,0 +1,97 @@ +import {DisplayConfig, HasValidation, OFExtensions, StrictComponentSchema} from '../base'; + +type UnusedFileProperties = 'hideLabel' | 'placeholder' | 'disabled' | 'widget' | 'validate'; + +type Validator = 'required'; +type TranslatableKeys = 'label' | 'description' | 'tooltip'; + +/** + * Shape of a single file upload from Form.io to the backend. + */ +export interface FileUploadData { + data: { + /** + * Full backend URL of the uploaded file (API endpoint). + * + * The value appears to be identical to the root `url` key. + */ + url: string; + /** + * Does not seems to be set to a meaningful value. + */ + form: ''; + /** + * File name of uploaded file. + * + * The value is different from the root `name` key. + */ + name: string; + /** + * File size in bytes, (positive) integer value. + * + * The value appears to be identical to the root `size` key. + */ + size: number; + /** + * Formio base URL configuration option, set to the root of our own API. + */ + baseUrl: string; + /** + * Does not seems to be set to a meaningful value. + */ + project: ''; + }; + name: string; + originalName: string; + /** + * File size in bytes, (positive) integer value. + */ + size: number; + /** + * We only support file uploads to a backend URL. + */ + storage: 'url'; + /** + * MIME type, determined by the browser during upload. If the OS/browser doesn't know + * it, it seems to be an empty string (see https://github.com/open-formulieren/open-forms-sdk/ + * blob/27877938249cdd627294871b70291ab8dc66fd61/src/formio/components/FileField.js#L279) + */ + type: string; + /** + * Full backend URL of the uploaded file (API endpoint). + */ + url: string; +} + +/** + * @group Form.io components + * @category Base types + */ +export interface BaseFileComponentSchema + extends Omit, UnusedFileProperties | 'errors'>, + DisplayConfig, + OFExtensions, + HasValidation { + type: 'file'; + multiple?: boolean; +} + +export type SingleFileComponentSchema = BaseFileComponentSchema & { + multiple?: false; + defaultValue?: [] | [FileUploadData]; +}; + +export type MultipleFileComponentSchema = BaseFileComponentSchema & { + multiple: true; + defaultValue?: FileUploadData[]; +}; + +/** + * @group Form.io components + * @category Concrete types + * + * Note that while `defaultValue` is defined here, this is only to be able to type + * define the submission data type. A file upload component cannot actually have + * default values. + */ +export type FileComponentSchema = SingleFileComponentSchema | MultipleFileComponentSchema; diff --git a/src/formio/components/index.ts b/src/formio/components/index.ts index 605f02a..54aa360 100644 --- a/src/formio/components/index.ts +++ b/src/formio/components/index.ts @@ -7,6 +7,7 @@ export * from './time'; export * from './phonenumber'; export * from './postcode'; export * from './number'; +export * from './file'; // Layout components export * from './content'; diff --git a/src/formio/index.ts b/src/formio/index.ts index bf0a86a..a7e6e6a 100644 --- a/src/formio/index.ts +++ b/src/formio/index.ts @@ -3,6 +3,7 @@ import { DateComponentSchema, DateTimeComponentSchema, EmailComponentSchema, + FileComponentSchema, NumberComponentSchema, PhoneNumberComponentSchema, PostcodeComponentSchema, @@ -42,6 +43,7 @@ export type AnyComponentSchema = | TimeComponentSchema | PhoneNumberComponentSchema | PostcodeComponentSchema + | FileComponentSchema | NumberComponentSchema // layout | ContentComponentSchema; diff --git a/test-d/formio/components/file.test-d.ts b/test-d/formio/components/file.test-d.ts new file mode 100644 index 0000000..5ca05c7 --- /dev/null +++ b/test-d/formio/components/file.test-d.ts @@ -0,0 +1,66 @@ +import {expectAssignable, expectNotAssignable} from 'tsd'; + +import {FileComponentSchema, FileUploadData} from '../../../lib'; + +// Grabbed from test env file upload, URLs obfuscated. +const anUpload: FileUploadData = { + url: 'http://localhost:8000/api/v2/submissions/files/54cc40ed-f1c4-4206-ba76-76d376ba4c3a', + data: { + url: 'http://localhost:8000/api/v2/submissions/files/54cc40ed-f1c4-4206-ba76-76d376ba4c3a', + form: '', + name: 'maykin_logo.png', + size: 8725, + baseUrl: 'http://localhost:8000/api/v2/', + project: '', + }, + name: 'maykin_logo-e0568045-45f6-46d1-909a-8895c5ee061e.png', + size: 8725, + type: 'image/png', + storage: 'url', + originalName: 'maykin_logo.png', +}; + +// minimal file component schema +expectAssignable({ + id: 'yejak', + type: 'file', + key: 'someFile', + label: 'Attachment', +}); + +// Behaviour of single vs. multiple file uploads + +const explicitSingleUpload: FileComponentSchema = { + id: 'yejak', + type: 'file', + key: 'someFile', + label: 'Attachment', + multiple: false, +}; +type ExplicitSingleUploadValue = (typeof explicitSingleUpload)['defaultValue']; +expectAssignable([]); +expectAssignable([anUpload]); +expectNotAssignable([anUpload, anUpload]); + +const explicitMultipleUpload: FileComponentSchema = { + id: 'yejak', + type: 'file', + key: 'someFile', + label: 'Attachment', + multiple: true, +}; +type ExplicitMultipleUploadValue = (typeof explicitMultipleUpload)['defaultValue']; +expectAssignable([]); +expectAssignable([anUpload]); +expectAssignable([anUpload, anUpload]); + +const implicitSingleUpload: FileComponentSchema = { + id: 'yejak', + type: 'file', + key: 'someFile', + label: 'Attachment', +}; +type ImplicitSingleUploadValue = (typeof implicitSingleUpload)['defaultValue']; +expectAssignable([]); +expectAssignable([anUpload]); +expectNotAssignable([anUpload, anUpload]); From 594f1e862518f37cdd59e1568dd7a6ea9a4eb507 Mon Sep 17 00:00:00 2001 From: Sergei Maertens Date: Mon, 16 Oct 2023 19:09:16 +0200 Subject: [PATCH 2/3] :sparkles: [#2] File upload component, pt. 2 Incorporate the configuration for the file uploads in the type. Added annotations because many of these are dynamically set by the backend or the calculated as part of the form builder form. --- src/formio/components/file.ts | 42 +++++- test-d/formio/components/file.test-d.ts | 180 ++++++++++++++++++++++++ 2 files changed, 221 insertions(+), 1 deletion(-) diff --git a/src/formio/components/file.ts b/src/formio/components/file.ts index aea4d84..a070cac 100644 --- a/src/formio/components/file.ts +++ b/src/formio/components/file.ts @@ -63,6 +63,14 @@ export interface FileUploadData { url: string; } +export interface FileUploadConfiguration { + // vanilla Form.io + name: string; + type: string[]; + // custom, injected by the backend or calculated by the builder + allowedTypesLabels: string[]; +} + /** * @group Form.io components * @category Base types @@ -70,10 +78,42 @@ export interface FileUploadData { export interface BaseFileComponentSchema extends Omit, UnusedFileProperties | 'errors'>, DisplayConfig, - OFExtensions, + Omit, 'registration'>, HasValidation { type: 'file'; multiple?: boolean; + + // (possibly) more-constrained existing formio properties + storage: 'url'; + url: string; + file: FileUploadConfiguration; + filePattern: string; // can be empty string, which sort of acts like wildcard + fileMaxSize?: string; // strings like 10MB, 1GB... parsed by Form.io + + // custom open forms properties. + // TODO: this should all be merged in the openForms namespace, but that's a rather + // big refactor/cleanup :( + useConfigFiletypes?: boolean; + // backend gloms it with defaults, so it anticipates keys being absent. Note that this + // is also only used in the backend! + of?: { + image?: { + resize?: { + apply?: boolean; + // backend falls back to defaults if the keys are absent, but if they are + // provided, they must be ints + width?: number; + height?: number; + }; + }; + }; + maxNumberOfFiles?: number | null; // should maybe go in validate.maxNumberOfFiles? + registration?: { + informatieobjecttype?: string; + bronorganisatie?: string; + docVertrouwelijkheidaanduiding?: string; + titel?: string; + }; } export type SingleFileComponentSchema = BaseFileComponentSchema & { diff --git a/test-d/formio/components/file.test-d.ts b/test-d/formio/components/file.test-d.ts index 5ca05c7..5ea70e9 100644 --- a/test-d/formio/components/file.test-d.ts +++ b/test-d/formio/components/file.test-d.ts @@ -26,6 +26,14 @@ expectAssignable({ type: 'file', key: 'someFile', label: 'Attachment', + storage: 'url', + url: '', + file: { + name: '', + type: [], + allowedTypesLabels: [], + }, + filePattern: '*', }); // Behaviour of single vs. multiple file uploads @@ -35,6 +43,14 @@ const explicitSingleUpload: FileComponentSchema = { type: 'file', key: 'someFile', label: 'Attachment', + storage: 'url', + url: '', + file: { + name: '', + type: [], + allowedTypesLabels: [], + }, + filePattern: '*', multiple: false, }; type ExplicitSingleUploadValue = (typeof explicitSingleUpload)['defaultValue']; @@ -47,6 +63,14 @@ const explicitMultipleUpload: FileComponentSchema = { type: 'file', key: 'someFile', label: 'Attachment', + storage: 'url', + url: '', + file: { + name: '', + type: [], + allowedTypesLabels: [], + }, + filePattern: '*', multiple: true, }; type ExplicitMultipleUploadValue = (typeof explicitMultipleUpload)['defaultValue']; @@ -59,8 +83,164 @@ const implicitSingleUpload: FileComponentSchema = { type: 'file', key: 'someFile', label: 'Attachment', + storage: 'url', + url: '', + file: { + name: '', + type: [], + allowedTypesLabels: [], + }, + filePattern: '*', }; type ImplicitSingleUploadValue = (typeof implicitSingleUpload)['defaultValue']; expectAssignable([]); expectAssignable([anUpload]); expectNotAssignable([anUpload, anUpload]); + +// Form builder assignability checks + +// with additional, file-component specific properties +expectAssignable({ + id: 'yejak', + type: 'file', + key: 'someInput', + label: 'Some input', + // builder sets empty URL, backend dynamically makes this non-empty + storage: 'url', + url: '', + file: { + // vanilla + name: 'prefix_{{ fileName }}', + type: ['image/png', 'application/pdf'], + // custom + allowedTypesLabels: ['.png', '.pdf'], + }, + useConfigFiletypes: false, // custom option to dynamically set allowed file types + registration: { + informatieobjecttype: '', + bronorganisatie: '', + docVertrouwelijkheidaanduiding: '', + titel: '', + }, + openForms: { + translations: {}, + }, + of: { + image: { + resize: { + apply: true, + width: 2000, + height: 2000, + }, + }, + }, + filePattern: 'image/png,application/pdf', + fileMaxSize: '10MB', + maxNumberOfFiles: 3, // custom setting +}); + +// full, correct schema +expectAssignable({ + id: 'yejak', + type: 'file', + storage: 'url', + url: '', + // basic tab in builder form + key: 'someFile', + label: 'Attachment', + description: 'A description', + tooltip: 'A tooltip', + showInSummary: true, + showInEmail: false, + showInPDF: true, + multiple: false, + hidden: false, + clearOnHide: true, + isSensitiveData: true, + // advanced tab in builder form + conditional: { + show: undefined, + when: undefined, + eq: undefined, + }, + // validation tab in builder form + validate: { + required: false, + }, + translatedErrors: { + nl: { + required: 'Je moet een bestand toevoegen!!!', + }, + }, + errors: { + // translatedErrors is converted into errors by the backend + required: 'Je moet een bestand toevoegen!!!', + }, + // file tab in builder form + file: { + name: 'prefix_{{ fileName }}', + type: ['image/png', 'application/pdf'], + // custom + allowedTypesLabels: ['.png', '.pdf'], + }, + useConfigFiletypes: false, // custom option to dynamically set allowed file types + of: { + image: { + resize: { + apply: true, + width: 2000, + height: 2000, + }, + }, + }, + filePattern: 'image/png,application/pdf', + fileMaxSize: '10MB', + maxNumberOfFiles: 3, // custom setting + // registration tab in builder form + registration: { + informatieobjecttype: '', + bronorganisatie: '', + docVertrouwelijkheidaanduiding: 'openbaar', + titel: '', + }, + // translations tab in builder form + openForms: { + translations: { + nl: { + label: 'Bestand toevoegen', + }, + }, + }, +}); + +// different component type +expectNotAssignable({ + id: 'yejak', + type: 'content', + key: 'someFile', + label: 'Attachment', + storage: 'url', + url: '', + file: { + name: '', + type: [], + allowedTypesLabels: [], + }, + filePattern: '*', +}); + +// using unsupported properties +expectNotAssignable({ + id: 'yejak', + type: 'file', + key: 'someFile', + label: 'Attachment', + storage: 's3', // we only support url + url: '', + file: { + name: '', + type: [], + allowedTypesLabels: [], + }, + filePattern: '*', +}); From 195786527c2b8720d57e59ef1ea78325a9cca51a Mon Sep 17 00:00:00 2001 From: Sergei Maertens Date: Wed, 18 Oct 2023 17:20:42 +0200 Subject: [PATCH 3/3] :ok_hand: [#2] File upload component PR feedback --- src/formio/components/file.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/formio/components/file.ts b/src/formio/components/file.ts index a070cac..3a47968 100644 --- a/src/formio/components/file.ts +++ b/src/formio/components/file.ts @@ -17,7 +17,7 @@ export interface FileUploadData { */ url: string; /** - * Does not seems to be set to a meaningful value. + * Does not seem to be set to a meaningful value. */ form: ''; /** @@ -37,7 +37,7 @@ export interface FileUploadData { */ baseUrl: string; /** - * Does not seems to be set to a meaningful value. + * Does not seem to be set to a meaningful value. */ project: ''; }; @@ -130,8 +130,8 @@ export type MultipleFileComponentSchema = BaseFileComponentSchema & { * @group Form.io components * @category Concrete types * - * Note that while `defaultValue` is defined here, this is only to be able to type - * define the submission data type. A file upload component cannot actually have - * default values. + * Note that while `defaultValue` is defined here, this is only to be able to derive + * the submission data type. A file upload component cannot actually have default + * values. */ export type FileComponentSchema = SingleFileComponentSchema | MultipleFileComponentSchema;