diff --git a/e2e/visual/no-theme.spec.js-snapshots/no-theme---editor-1-firefox-linux.png b/e2e/visual/no-theme.spec.js-snapshots/no-theme---editor-1-firefox-linux.png index 2d5797ac0..a6c43bef2 100644 Binary files a/e2e/visual/no-theme.spec.js-snapshots/no-theme---editor-1-firefox-linux.png and b/e2e/visual/no-theme.spec.js-snapshots/no-theme---editor-1-firefox-linux.png differ diff --git a/e2e/visual/theming.spec.js-snapshots/theming---editor-1-firefox-linux.png b/e2e/visual/theming.spec.js-snapshots/theming---editor-1-firefox-linux.png index 89e953888..9fdd9d956 100644 Binary files a/e2e/visual/theming.spec.js-snapshots/theming---editor-1-firefox-linux.png and b/e2e/visual/theming.spec.js-snapshots/theming---editor-1-firefox-linux.png differ diff --git a/packages/form-js-editor/src/features/properties-panel/Util.js b/packages/form-js-editor/src/features/properties-panel/Util.js index 388d5c72c..46e00da4f 100644 --- a/packages/form-js-editor/src/features/properties-panel/Util.js +++ b/packages/form-js-editor/src/features/properties-panel/Util.js @@ -71,6 +71,7 @@ export const INPUTS = [ 'taglist', 'textfield', 'textarea', + 'filepicker', ]; export const OPTIONS_INPUTS = ['checklist', 'radio', 'select', 'taglist']; diff --git a/packages/form-js-editor/src/features/properties-panel/entries/AcceptEntry.js b/packages/form-js-editor/src/features/properties-panel/entries/AcceptEntry.js new file mode 100644 index 000000000..3ef481ee1 --- /dev/null +++ b/packages/form-js-editor/src/features/properties-panel/entries/AcceptEntry.js @@ -0,0 +1,65 @@ +import { get } from 'min-dash'; + +import { useService, useVariables } from '../hooks'; + +import { FeelTemplatingEntry, isFeelEntryEdited } from '@bpmn-io/properties-panel'; + +export function AcceptEntry(props) { + const { editField, field } = props; + + const entries = []; + + entries.push({ + id: 'accept', + component: Accept, + editField: editField, + field: field, + isEdited: isFeelEntryEdited, + isDefaultVisible: (field) => field.type === 'filepicker', + }); + + return entries; +} + +function Accept(props) { + const { editField, field, id } = props; + + const debounce = useService('debounce'); + + const variables = useVariables().map((name) => ({ name })); + + const path = ['accept']; + + const getValue = () => { + return get(field, path, ''); + }; + + const setValue = (value) => { + return editField(field, path, value); + }; + + return FeelTemplatingEntry({ + debounce, + element: field, + getValue, + id, + label: 'Supported file formats', + singleLine: true, + setValue, + variables, + description, + }); +} + +// helpers ////////// + +const description = ( + <> + A comma-separated list of{' '} + + file type specifiers + + +); diff --git a/packages/form-js-editor/src/features/properties-panel/entries/DefaultValueEntry.js b/packages/form-js-editor/src/features/properties-panel/entries/DefaultValueEntry.js index a5db2312f..2e519e88f 100644 --- a/packages/form-js-editor/src/features/properties-panel/entries/DefaultValueEntry.js +++ b/packages/form-js-editor/src/features/properties-panel/entries/DefaultValueEntry.js @@ -36,7 +36,7 @@ export function DefaultValueEntry(props) { }; } - const defaulValueBase = { + const defaultValueBase = { editField, field, id: 'defaultValue', @@ -44,21 +44,21 @@ export function DefaultValueEntry(props) { }; entries.push({ - ...defaulValueBase, + ...defaultValueBase, component: DefaultValueCheckbox, isEdited: isSelectEntryEdited, isDefaultVisible: isDefaultVisible((field) => field.type === 'checkbox'), }); entries.push({ - ...defaulValueBase, + ...defaultValueBase, component: DefaultValueNumber, isEdited: isTextFieldEntryEdited, isDefaultVisible: isDefaultVisible((field) => field.type === 'number'), }); entries.push({ - ...defaulValueBase, + ...defaultValueBase, component: DefaultValueSingleSelect, isEdited: isSelectEntryEdited, isDefaultVisible: isDefaultVisible((field) => field.type === 'radio' || field.type === 'select'), @@ -67,14 +67,14 @@ export function DefaultValueEntry(props) { // todo(Skaiir): implement a multiselect equivalent (cf. https://github.com/bpmn-io/form-js/issues/265) entries.push({ - ...defaulValueBase, + ...defaultValueBase, component: DefaultValueTextfield, isEdited: isTextFieldEntryEdited, isDefaultVisible: isDefaultVisible((field) => field.type === 'textfield'), }); entries.push({ - ...defaulValueBase, + ...defaultValueBase, component: DefaultValueTextarea, isEdited: isTextAreaEntryEdited, isDefaultVisible: isDefaultVisible((field) => field.type === 'textarea'), diff --git a/packages/form-js-editor/src/features/properties-panel/entries/DescriptionEntry.js b/packages/form-js-editor/src/features/properties-panel/entries/DescriptionEntry.js index c5b0f1909..efeb1bd61 100644 --- a/packages/form-js-editor/src/features/properties-panel/entries/DescriptionEntry.js +++ b/packages/form-js-editor/src/features/properties-panel/entries/DescriptionEntry.js @@ -17,7 +17,7 @@ export function DescriptionEntry(props) { editField: editField, field: field, isEdited: isFeelEntryEdited, - isDefaultVisible: (field) => INPUTS.includes(field.type), + isDefaultVisible: (field) => field.type !== 'filepicker' && INPUTS.includes(field.type), }); return entries; diff --git a/packages/form-js-editor/src/features/properties-panel/entries/MultipleEntry.js b/packages/form-js-editor/src/features/properties-panel/entries/MultipleEntry.js new file mode 100644 index 000000000..f1ccc4b71 --- /dev/null +++ b/packages/form-js-editor/src/features/properties-panel/entries/MultipleEntry.js @@ -0,0 +1,52 @@ +import { get } from 'min-dash'; + +import { useService, useVariables } from '../hooks'; + +import { FeelToggleSwitchEntry, isFeelEntryEdited } from '@bpmn-io/properties-panel'; + +export function MultipleEntry(props) { + const { editField, field } = props; + + const entries = []; + + entries.push({ + id: 'multiple', + component: Multiple, + editField: editField, + field: field, + isEdited: isFeelEntryEdited, + isDefaultVisible: (field) => field.type === 'filepicker', + }); + + return entries; +} + +function Multiple(props) { + const { editField, field, id } = props; + + const debounce = useService('debounce'); + + const variables = useVariables().map((name) => ({ name })); + + const path = ['multiple']; + + const getValue = () => { + return get(field, path, ''); + }; + + const setValue = (value) => { + return editField(field, path, value); + }; + + return FeelToggleSwitchEntry({ + debounce, + element: field, + feel: 'optional', + getValue, + id, + label: 'Upload multiple files', + inline: true, + setValue, + variables, + }); +} diff --git a/packages/form-js-editor/src/features/properties-panel/entries/index.js b/packages/form-js-editor/src/features/properties-panel/entries/index.js index 452144a1c..3636c9970 100644 --- a/packages/form-js-editor/src/features/properties-panel/entries/index.js +++ b/packages/form-js-editor/src/features/properties-panel/entries/index.js @@ -40,3 +40,5 @@ export { HeadersSourceSelectEntry } from './HeadersSourceSelectEntry'; export { ColumnsExpressionEntry } from './ColumnsExpressionEntry'; export { StaticColumnsSourceEntry } from './StaticColumnsSourceEntry'; export { VersionTagEntry } from './VersionTagEntry'; +export { AcceptEntry } from './AcceptEntry'; +export { MultipleEntry } from './MultipleEntry'; diff --git a/packages/form-js-editor/src/features/properties-panel/groups/GeneralGroup.js b/packages/form-js-editor/src/features/properties-panel/groups/GeneralGroup.js index 66c1a73fd..a8cd25ccd 100644 --- a/packages/form-js-editor/src/features/properties-panel/groups/GeneralGroup.js +++ b/packages/form-js-editor/src/features/properties-panel/groups/GeneralGroup.js @@ -24,6 +24,8 @@ import { PaginationEntry, RowCountEntry, VersionTagEntry, + AcceptEntry, + MultipleEntry, } from '../entries'; export function GeneralGroup(field, editField, getService) { @@ -48,6 +50,8 @@ export function GeneralGroup(field, editField, getService) { ...ImageSourceEntry({ field, editField }), ...AltTextEntry({ field, editField }), ...SelectEntries({ field, editField }), + ...AcceptEntry({ field, editField }), + ...MultipleEntry({ field, editField }), ...DisabledEntry({ field, editField }), ...ReadonlyEntry({ field, editField }), ...TableDataSourceEntry({ field, editField }), diff --git a/packages/form-js-editor/test/spec/features/properties-panel/PropertiesPanel.spec.js b/packages/form-js-editor/test/spec/features/properties-panel/PropertiesPanel.spec.js index 9b86a5dc5..98e19aeeb 100644 --- a/packages/form-js-editor/test/spec/features/properties-panel/PropertiesPanel.spec.js +++ b/packages/form-js-editor/test/spec/features/properties-panel/PropertiesPanel.spec.js @@ -3416,6 +3416,26 @@ describe('properties panel', function () { }); }); }); + + describe('filepicker', function () { + it('entries', function () { + // given + const field = schema.components.find(({ key }) => key === 'files'); + + bootstrapPropertiesPanel({ + container, + field, + }); + + // then + expectPanelStructure(container, { + General: ['Field label', 'Key', 'Supported file formats', 'Upload multiple files', 'Disabled', 'Read only'], + Condition: [], + Validation: ['Required'], + 'Custom properties': [], + }); + }); + }); }); describe('custom properties', function () { diff --git a/packages/form-js-editor/test/spec/features/properties-panel/groups/GeneralGroup.spec.js b/packages/form-js-editor/test/spec/features/properties-panel/groups/GeneralGroup.spec.js index 4e843e273..8e02868fb 100644 --- a/packages/form-js-editor/test/spec/features/properties-panel/groups/GeneralGroup.spec.js +++ b/packages/form-js-editor/test/spec/features/properties-panel/groups/GeneralGroup.spec.js @@ -349,7 +349,7 @@ describe('GeneralGroup', function () { it('should render for INPUTS', function () { // given - for (const type of INPUTS) { + for (const type of INPUTS.filter((type) => type !== 'filepicker')) { const field = { type }; // when @@ -580,11 +580,14 @@ describe('GeneralGroup', function () { }); describe('for all other INPUTS', () => { - const otherInputTypes = INPUTS.filter((i) => !OPTIONS_INPUTS.includes(i)); + const nonDefaultValueInputs = new Set(['datetime', 'filepicker']); + const defaultValueInputs = INPUTS.filter( + (input) => !OPTIONS_INPUTS.includes(input) && !nonDefaultValueInputs.has(input), + ); it('should render', function () { // given - for (const type of otherInputTypes) { + for (const type of defaultValueInputs) { const field = { type }; // when @@ -593,8 +596,22 @@ describe('GeneralGroup', function () { // then const defaultValueEntry = findEntry('defaultValue', container); - if (type === 'datetime') expect(defaultValueEntry).to.not.exist; - else expect(defaultValueEntry).to.exist; + expect(defaultValueEntry).to.exist; + } + }); + + it('should not render', function () { + // given + for (const type of nonDefaultValueInputs) { + const field = { type }; + + // when + const { container } = renderGeneralGroup({ field }); + + // then + const defaultValueEntry = findEntry('defaultValue', container); + + expect(defaultValueEntry).to.not.exist; } }); }); diff --git a/packages/form-js-editor/test/spec/form.json b/packages/form-js-editor/test/spec/form.json index 375b4fafc..f8ac1c932 100644 --- a/packages/form-js-editor/test/spec/form.json +++ b/packages/form-js-editor/test/spec/form.json @@ -238,6 +238,14 @@ "alt": "The bpmn.io logo", "type": "image" }, + { + "label": "Image files", + "type": "filepicker", + "id": "files", + "key": "files", + "multiple": true, + "accept": ".jpg,.png" + }, { "id": "Spacer_1", "type": "spacer", diff --git a/packages/form-js-viewer/assets/form-js-base.css b/packages/form-js-viewer/assets/form-js-base.css index 9ca6b91ae..40791b4b8 100644 --- a/packages/form-js-viewer/assets/form-js-base.css +++ b/packages/form-js-viewer/assets/form-js-base.css @@ -461,6 +461,7 @@ .fjs-container .fjs-input[type='tel'], .fjs-container .fjs-input[type='number'], .fjs-container .fjs-button[type='submit'], +.fjs-container .fjs-button[type='button'], .fjs-container .fjs-button[type='reset'], .fjs-container .fjs-textarea, .fjs-container .fjs-select { @@ -636,7 +637,8 @@ margin: 6px 10px 6px 4px; } -.fjs-container .fjs-button[type='submit'] { +.fjs-container .fjs-button[type='submit'], +.fjs-container .fjs-button[type='button'] { color: var(--cds-text-inverse, var(--color-white)); background-color: var(--color-accent); border-color: var(--color-accent); @@ -649,12 +651,14 @@ } .fjs-container .fjs-button[type='submit'], +.fjs-container .fjs-button[type='button'], .fjs-container .fjs-button[type='reset'] { min-width: 100px; width: auto; } -.fjs-container .fjs-button[type='submit'] { +.fjs-container .fjs-button[type='submit'], +.fjs-container .fjs-button[type='button'] { font-weight: 600; } @@ -665,6 +669,7 @@ .fjs-container .fjs-input[type='tel']:focus, .fjs-container .fjs-input[type='number']:focus, .fjs-container .fjs-button[type='submit']:focus, +.fjs-container .fjs-button[type='button']:focus, .fjs-container .fjs-button[type='reset']:focus, .fjs-container .fjs-textarea:focus, .fjs-container .fjs-select:focus { @@ -681,7 +686,8 @@ outline: none; } -.fjs-container .fjs-button[type='submit']:focus { +.fjs-container .fjs-button[type='submit']:focus, +.fjs-container .fjs-button[type='button']:focus { border-color: var(--color-accent); } @@ -724,6 +730,7 @@ } .fjs-container .fjs-button[type='submit']:disabled, +.fjs-container .fjs-button[type='button']:disabled, .fjs-container .fjs-button[type='reset']:disabled { color: var(--cds-text-on-color-disabled, var(--color-text-light)); background-color: var(--color-background-disabled); @@ -731,6 +738,7 @@ } .fjs-container .fjs-button[type='submit']:read-only, +.fjs-container .fjs-button[type='button']:read-only, .fjs-container .fjs-button[type='reset']:read-only { color: var(--text-light); background-color: var(--color-background-readonly); @@ -1278,3 +1286,14 @@ .fjs-container .flatpickr-calendar { width: 326px; } + +.fjs-hidden { + display: none; +} + +.fjs-container .fjs-filepicker-container { + display: flex; + flex-direction: row; + align-items: center; + gap: 8px; +} diff --git a/packages/form-js-viewer/src/render/components/form-fields/FilePicker.js b/packages/form-js-viewer/src/render/components/form-fields/FilePicker.js new file mode 100644 index 000000000..cb644d3cd --- /dev/null +++ b/packages/form-js-viewer/src/render/components/form-fields/FilePicker.js @@ -0,0 +1,132 @@ +import { formFieldClasses } from '../Util'; +import { Label } from '../Label'; +import { Errors } from '../Errors'; +import { useEffect, useRef, useState } from 'preact/hooks'; +import { useService, useSingleLineTemplateEvaluation } from '../../hooks'; + +const type = 'filepicker'; + +/** + * @typedef Props + * @property {(props: { value: string }) => void} onChange + * @property {string} domId + * @property {string[]} errors + * @property {boolean} disabled + * @property {boolean} readonly + * @property {boolean} required + * @property {Object} field + * @property {string} field.id + * @property {string} [field.label] + * @property {string} [field.accept] + * @property {boolean} [field.multiple] + * + * @param {Props} props + * @returns {import("preact").JSX.Element} + */ +export function FilePicker(props) { + /** @type {import("preact/hooks").Ref} */ + const fileInputRef = useRef(null); + /** @type {[File[],import("preact/hooks").StateUpdater]} */ + const [selectedFiles, setSelectedFiles] = useState([]); + const eventBus = useService('eventBus'); + const { field, onChange, domId, errors = [], disabled, readonly, required } = props; + const { label, multiple = '', accept = '', id } = field; + const evaluatedAccept = useSingleLineTemplateEvaluation(accept); + const evaluatedMultiple = + useSingleLineTemplateEvaluation(typeof multiple === 'string' ? multiple : multiple.toString()) === 'true'; + const errorMessageId = `${domId}-error-message`; + + useEffect(() => { + const reset = () => { + setSelectedFiles([]); + onChange({ + value: null, + }); + }; + + eventBus.on('import.done', reset); + eventBus.on('reset', reset); + + return () => { + eventBus.off('import.done', reset); + eventBus.off('reset', reset); + }; + }, [eventBus, onChange]); + + return ( +
+
+ ); +} + +FilePicker.config = { + type: 'filepicker', + keyed: true, + label: 'File picker', + group: 'basic-input', + emptyValue: null, + sanitizeValue: ({ value }) => { + return value; + }, + create: (options = {}) => ({ ...options }), +}; + +// helper ////////// + +/** + * @param {File[]} files + * @returns {string} + */ +function getSelectedFilesLabel(files) { + if (files.length === 0) { + return 'No files selected'; + } + + if (files.length === 1) { + return files[0].name; + } + + return `${files.length} files selected`; +} diff --git a/packages/form-js-viewer/src/render/components/icons/FilePicker.svg b/packages/form-js-viewer/src/render/components/icons/FilePicker.svg new file mode 100644 index 000000000..7cd733f13 --- /dev/null +++ b/packages/form-js-viewer/src/render/components/icons/FilePicker.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/form-js-viewer/src/render/components/icons/index.js b/packages/form-js-viewer/src/render/components/icons/index.js index e1729d27f..7896d8201 100644 --- a/packages/form-js-viewer/src/render/components/icons/index.js +++ b/packages/form-js-viewer/src/render/components/icons/index.js @@ -20,6 +20,7 @@ import IFrameIcon from './IFrame.svg'; import ImageIcon from './Image.svg'; import GroupIcon from './Group.svg'; import TableIcon from './Table.svg'; +import FilePickerIcon from './FilePicker.svg'; export const iconsByType = (type) => { return { @@ -44,6 +45,7 @@ export const iconsByType = (type) => { textfield: TextfieldIcon, textarea: TextareaIcon, table: TableIcon, + filepicker: FilePickerIcon, default: FormIcon, }[type]; }; diff --git a/packages/form-js-viewer/src/render/components/index.js b/packages/form-js-viewer/src/render/components/index.js index 0256d2dab..6446b5abe 100644 --- a/packages/form-js-viewer/src/render/components/index.js +++ b/packages/form-js-viewer/src/render/components/index.js @@ -19,6 +19,7 @@ import { ExpressionField } from './form-fields/ExpressionField'; import { Textfield } from './form-fields/Textfield'; import { Textarea } from './form-fields/Textarea'; import { Table } from './form-fields/Table'; +import { FilePicker } from './form-fields/FilePicker'; import { Label } from './Label'; import { Description } from './Description'; @@ -52,6 +53,7 @@ export { Textfield, Textarea, Table, + FilePicker, }; export const formFields = [ @@ -61,6 +63,7 @@ export const formFields = [ Numberfield, Datetime, ExpressionField, + FilePicker, /* Selection */ Checkbox, diff --git a/packages/form-js-viewer/test/spec/render/components/form-fields/FilePicker.spec.js b/packages/form-js-viewer/test/spec/render/components/form-fields/FilePicker.spec.js new file mode 100644 index 000000000..dc2ad2b62 --- /dev/null +++ b/packages/form-js-viewer/test/spec/render/components/form-fields/FilePicker.spec.js @@ -0,0 +1,214 @@ +import { fireEvent, render, screen } from '@testing-library/preact/pure'; + +import { FilePicker } from '../../../../../src/render/components/form-fields/FilePicker'; + +import { createFormContainer, expectNoViolations } from '../../../../TestHelper'; + +import { MockFormContext } from '../helper'; + +let container; + +describe('FilePicker', function () { + beforeEach(function () { + container = createFormContainer(); + }); + + afterEach(function () { + container.remove(); + }); + + it('should render', function () { + // when + createFilePicker({ + field: { + ...defaultField, + label: 'My files', + }, + }); + + // then + + expect(screen.getByLabelText('My files')).to.exist; + expect(screen.getByRole('button', { name: 'Browse' })).to.exist; + expect(screen.getByText('No files selected')).to.exist; + }); + + it('should render errors', function () { + // when + createFilePicker({ + errors: ['Something went wrong'], + }); + + // then + expect(screen.getByText('Something went wrong')).to.exist; + }); + + it('should change the label with single file selected', function () { + // given + const file = new File([''], 'test.png', { type: 'image/png' }); + const { container } = createFilePicker(); + + // when + + fireEvent.change(container.querySelector('input[type="file"]'), { + target: { + files: [file], + }, + }); + + // then + + expect(screen.getByText('test.png')).to.exist; + }); + + it('should change the label with multiple files selected', function () { + // given + const file = new File([''], 'test1.png', { type: 'image/png' }); + const { container } = createFilePicker(); + + // when + + fireEvent.change(container.querySelector('input[type="file"]'), { + target: { + files: [file, file], + }, + }); + + // then + + expect(screen.getByText('2 files selected')).to.exist; + }); + + it('should accept multiple files and limit the file types', function () { + // when + const { container } = createFilePicker({ + field: { + ...defaultField, + accept: 'image/*', + multiple: true, + }, + }); + + // then + + expect(screen.getByRole('button', { name: 'Browse' })).to.exist; + expect(screen.getByText('No files selected')).to.exist; + expect(container.querySelector('input[type="file"]')).to.have.property('accept', 'image/*'); + expect(container.querySelector('input[type="file"]')).to.have.property('multiple'); + }); + + it('should accept multiple files and limit the file types (expression)', function () { + // when + const { container } = createFilePicker({ + initialData: { + mime: 'image/svg', + acceptMultiple: true, + }, + field: { + ...defaultField, + accept: '=mime', + multiple: '=acceptMultiple', + }, + }); + + // then + + expect(screen.getByRole('button', { name: 'Browse' })).to.exist; + expect(screen.getByText('No files selected')).to.exist; + expect(container.querySelector('input[type="file"]')).to.have.property('accept', 'image/svg'); + expect(container.querySelector('input[type="file"]')).to.have.property('multiple'); + }); + + it('#create', function () { + // assume + const { config } = FilePicker; + + // when + const field = config.create(); + + // then + expect(field).to.eql({}); + + // but when + const customField = config.create({ + custom: true, + }); + + // then + expect(customField).to.contain({ + custom: true, + }); + }); + + describe('a11y', function () { + it('should have no violations', async function () { + // given + this.timeout(10000); + + const { container } = createFilePicker(); + + // then + await expectNoViolations(container); + }); + + it('should have no violations for readonly', async function () { + // given + this.timeout(10000); + + const { container } = createFilePicker({ + value: true, + readonly: true, + }); + + // then + await expectNoViolations(container); + }); + + it('should have no violations for errors', async function () { + // given + this.timeout(10000); + + const { container } = createFilePicker({ + value: true, + errors: ['Something went wrong'], + }); + + // then + await expectNoViolations(container); + }); + }); +}); + +// helper ////////// + +const defaultField = { + id: 'Filepicker_1', + type: 'filepicker', +}; + +function createFilePicker({ services, ...restOptions } = {}) { + const options = { + domId: 'test-filepicker', + field: defaultField, + onChange: () => {}, + ...restOptions, + }; + + return render( + + + , + { + container: options.container || container.querySelector('.fjs-form'), + }, + ); +} diff --git a/packages/form-json-schema/src/defs/component.json b/packages/form-json-schema/src/defs/component.json index faadabc9c..31e157b2f 100644 --- a/packages/form-json-schema/src/defs/component.json +++ b/packages/form-json-schema/src/defs/component.json @@ -262,6 +262,16 @@ "$id": "#/component/content", "description": "The content of a custom component.", "type": "string" + }, + "accept": { + "$id": "#/component/accept", + "description": "Define the accepted file types.", + "type": "string" + }, + "multiple": { + "$id": "#/component/multiple", + "description": "Allow multiple files to be selected.", + "type": ["boolean", "string"] } }, "required": ["type"] diff --git a/packages/form-json-schema/src/defs/field-types/inputs.json b/packages/form-json-schema/src/defs/field-types/inputs.json index 14d871981..4fbb5f1f3 100644 --- a/packages/form-json-schema/src/defs/field-types/inputs.json +++ b/packages/form-json-schema/src/defs/field-types/inputs.json @@ -11,7 +11,8 @@ "taglist", "textfield", "textarea", - "expression" + "expression", + "filepicker" ] } }, diff --git a/packages/form-json-schema/src/defs/rules/rules-allowed-properties.json b/packages/form-json-schema/src/defs/rules/rules-allowed-properties.json index f64233bfa..576276d57 100644 --- a/packages/form-json-schema/src/defs/rules/rules-allowed-properties.json +++ b/packages/form-json-schema/src/defs/rules/rules-allowed-properties.json @@ -397,6 +397,39 @@ "content": false } } + }, + { + "if": { + "not": { + "properties": { + "type": { + "const": "filepicker" + } + }, + "required": ["type"] + } + }, + "then": { + "properties": { + "accept": false, + "multiple": false + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "filepicker" + } + }, + "required": ["type"] + }, + "then": { + "properties": { + "description": false + } + } } ] } diff --git a/packages/form-json-schema/src/defs/type.json b/packages/form-json-schema/src/defs/type.json index 77d840553..e7587befc 100644 --- a/packages/form-json-schema/src/defs/type.json +++ b/packages/form-json-schema/src/defs/type.json @@ -22,6 +22,7 @@ "separator", "table", "iframe", - "expression" + "expression", + "filepicker" ] } diff --git a/packages/form-json-schema/test/fixtures/accept-not-allowed.js b/packages/form-json-schema/test/fixtures/accept-not-allowed.js new file mode 100644 index 000000000..f9e6423be --- /dev/null +++ b/packages/form-json-schema/test/fixtures/accept-not-allowed.js @@ -0,0 +1,27 @@ +export const form = { + type: 'default', + components: [ + { + type: 'textfield', + key: 'textfield_g35o3e', + accept: '.png,.jpg', + }, + ], +}; + +export const errors = [ + { + instancePath: '/components/0/accept', + keyword: 'false schema', + message: 'boolean schema is false', + params: {}, + schemaPath: '#/properties/components/items/allOf/1/allOf/20/then/properties/accept/false schema', + }, + { + instancePath: '/components/0', + schemaPath: '#/properties/components/items/allOf/1/allOf/20/if', + keyword: 'if', + params: { failingKeyword: 'then' }, + message: 'must match "then" schema', + }, +]; diff --git a/packages/form-json-schema/test/fixtures/filepicker.js b/packages/form-json-schema/test/fixtures/filepicker.js new file mode 100644 index 000000000..44d3893d7 --- /dev/null +++ b/packages/form-json-schema/test/fixtures/filepicker.js @@ -0,0 +1,13 @@ +export const form = { + type: 'default', + components: [ + { + type: 'filepicker', + key: 'filepicker', + accept: '.png,.jpg', + multiple: true, + }, + ], +}; + +export const errors = null; diff --git a/packages/form-json-schema/test/fixtures/multiple-not-allowed.js b/packages/form-json-schema/test/fixtures/multiple-not-allowed.js new file mode 100644 index 000000000..634dab41f --- /dev/null +++ b/packages/form-json-schema/test/fixtures/multiple-not-allowed.js @@ -0,0 +1,27 @@ +export const form = { + type: 'default', + components: [ + { + type: 'textfield', + key: 'textfield_g35o3e', + multiple: true, + }, + ], +}; + +export const errors = [ + { + instancePath: '/components/0/multiple', + keyword: 'false schema', + message: 'boolean schema is false', + params: {}, + schemaPath: '#/properties/components/items/allOf/1/allOf/20/then/properties/multiple/false schema', + }, + { + instancePath: '/components/0', + schemaPath: '#/properties/components/items/allOf/1/allOf/20/if', + keyword: 'if', + params: { failingKeyword: 'then' }, + message: 'must match "then" schema', + }, +]; diff --git a/packages/form-json-schema/test/spec/validation.spec.js b/packages/form-json-schema/test/spec/validation.spec.js index 6e0fe7f08..5b9d2766e 100644 --- a/packages/form-json-schema/test/spec/validation.spec.js +++ b/packages/form-json-schema/test/spec/validation.spec.js @@ -174,6 +174,12 @@ describe('validation', function () { testForm('dataSource-not-allowed'); testForm('columns-columnsExpression-exclusive'); + + testForm('filepicker'); + + testForm('accept-not-allowed'); + + testForm('multiple-not-allowed'); }); describe('rules - default', function () {