From 90994964fc466246f434c2fd6b1589f4c23e8ee9 Mon Sep 17 00:00:00 2001 From: darius-lesch <134329447+darius-lesch@users.noreply.github.com> Date: Fri, 2 Oct 2026 11:45:42 +0000 Subject: [PATCH 01/17] feat(vue-vuetify): add MultiEnumSelectRenderer for multiselect rendering Currently, arrays of enums are strictly bound to EnumArrayRenderer which forces a checkbox list view. For large data sets, a dropdown selection box is more manageable. This adds a native MultiEnumSelectRenderer using `` to provide dropdown multi-select. It is opted-in via setting `options: { format: 'multiselect' }` within the UI Schema. Added tests covering layout rendering, bindings, snapshots, and event firing. --- .../complex/MultiEnumSelectRenderer.entry.ts | 44 ++++++ .../src/complex/MultiEnumSelectRenderer.vue | 96 ++++++++++++ packages/vue-vuetify/src/complex/index.ts | 4 + .../complex/MultiEnumSelectRenderer.spec.ts | 114 ++++++++++++++ .../MultiEnumSelectRenderer.spec.ts.snap | 145 ++++++++++++++++++ 5 files changed, 403 insertions(+) create mode 100644 packages/vue-vuetify/src/complex/MultiEnumSelectRenderer.entry.ts create mode 100644 packages/vue-vuetify/src/complex/MultiEnumSelectRenderer.vue create mode 100644 packages/vue-vuetify/tests/unit/complex/MultiEnumSelectRenderer.spec.ts create mode 100644 packages/vue-vuetify/tests/unit/complex/__snapshots__/MultiEnumSelectRenderer.spec.ts.snap diff --git a/packages/vue-vuetify/src/complex/MultiEnumSelectRenderer.entry.ts b/packages/vue-vuetify/src/complex/MultiEnumSelectRenderer.entry.ts new file mode 100644 index 0000000000..a42b64917d --- /dev/null +++ b/packages/vue-vuetify/src/complex/MultiEnumSelectRenderer.entry.ts @@ -0,0 +1,44 @@ +import { + and, + hasType, + optionIs, + rankWith, + schemaMatches, + schemaSubPathMatches, + uiTypeIs, + type JsonFormsRendererRegistryEntry, + type JsonSchema, +} from '@jsonforms/core'; +import controlRenderer from './MultiEnumSelectRenderer.vue'; + +const hasOneOfItems = (schema: JsonSchema): boolean => + schema.oneOf !== undefined && + schema.oneOf.length > 0 && + (schema.oneOf as JsonSchema[]).every((entry: JsonSchema) => { + return entry.const !== undefined; + }); + +const hasEnumItems = (schema: JsonSchema): boolean => + schema.type === 'string' && schema.enum !== undefined; + +export const entry: JsonFormsRendererRegistryEntry = { + renderer: controlRenderer, + tester: rankWith( + 10, + and( + uiTypeIs('Control'), + optionIs('format', 'multiselect'), + and( + schemaMatches( + (schema) => + hasType(schema, 'array') && + !Array.isArray(schema.items) && + schema.uniqueItems === true, + ), + schemaSubPathMatches('items', (schema) => { + return hasOneOfItems(schema) || hasEnumItems(schema); + }), + ), + ), + ), +}; diff --git a/packages/vue-vuetify/src/complex/MultiEnumSelectRenderer.vue b/packages/vue-vuetify/src/complex/MultiEnumSelectRenderer.vue new file mode 100644 index 0000000000..d06bef85ad --- /dev/null +++ b/packages/vue-vuetify/src/complex/MultiEnumSelectRenderer.vue @@ -0,0 +1,96 @@ + + + diff --git a/packages/vue-vuetify/src/complex/index.ts b/packages/vue-vuetify/src/complex/index.ts index adeb842825..f186ec1b01 100644 --- a/packages/vue-vuetify/src/complex/index.ts +++ b/packages/vue-vuetify/src/complex/index.ts @@ -2,6 +2,7 @@ export { default as AllOfRenderer } from './AllOfRenderer.vue'; export { default as AnyOfRenderer } from './AnyOfRenderer.vue'; export { default as ArrayControlRenderer } from './ArrayControlRenderer.vue'; export { default as EnumArrayRenderer } from './EnumArrayRenderer.vue'; +export { default as MultiEnumSelectRenderer } from './MultiEnumSelectRenderer.vue'; export { default as ObjectRenderer } from './ObjectRenderer.vue'; export { default as OneOfRenderer } from './OneOfRenderer.vue'; export { default as OneOfTabRenderer } from './OneOfTabRenderer.vue'; @@ -11,6 +12,7 @@ import { entry as allOfRendererEntry } from './AllOfRenderer.entry'; import { entry as anyOfRendererEntry } from './AnyOfRenderer.entry'; import { entry as arrayControlRendererEntry } from './ArrayControlRenderer.entry'; import { entry as enumArrayRendererEntry } from './EnumArrayRenderer.entry'; +import { entry as multiEnumSelectRendererEntry } from './MultiEnumSelectRenderer.entry'; import { entry as objectRendererEntry } from './ObjectRenderer.entry'; import { entry as oneOfRendererEntry } from './OneOfRenderer.entry'; import { entry as oneOfTabRendererEntry } from './OneOfTabRenderer.entry'; @@ -21,6 +23,7 @@ export const complexRenderers = [ anyOfRendererEntry, arrayControlRendererEntry, enumArrayRendererEntry, + multiEnumSelectRendererEntry, objectRendererEntry, oneOfRendererEntry, oneOfTabRendererEntry, @@ -32,6 +35,7 @@ export { anyOfRendererEntry, arrayControlRendererEntry, enumArrayRendererEntry, + multiEnumSelectRendererEntry, objectRendererEntry, oneOfRendererEntry, oneOfTabRendererEntry, diff --git a/packages/vue-vuetify/tests/unit/complex/MultiEnumSelectRenderer.spec.ts b/packages/vue-vuetify/tests/unit/complex/MultiEnumSelectRenderer.spec.ts new file mode 100644 index 0000000000..71ff9a3200 --- /dev/null +++ b/packages/vue-vuetify/tests/unit/complex/MultiEnumSelectRenderer.spec.ts @@ -0,0 +1,114 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { clearAllIds } from '@jsonforms/core'; +import MultiEnumSelectRenderer from '../../../src/complex/MultiEnumSelectRenderer.vue'; +import { entry as multiEnumSelectRendererEntry } from '../../../src/complex/MultiEnumSelectRenderer.entry'; +import { mountJsonForms } from '../util'; + +describe('MultiEnumSelectRenderer.vue', () => { + const renderers = [multiEnumSelectRendererEntry]; + + describe('with enum schema', () => { + const schema = { + type: 'array', + title: 'My Multi Select', + uniqueItems: true, + items: { + type: 'string', + enum: ['a', 'b', 'c'], + }, + }; + const uischema = { + type: 'Control', + scope: '#', + options: { + format: 'multiselect', + }, + }; + + let wrapper: ReturnType; + + beforeEach(() => { + clearAllIds(); + wrapper = mountJsonForms(['a'], schema, renderers, uischema); + }); + + it('check if child MultiEnumSelectRenderer exists', () => { + expect(wrapper.getComponent(MultiEnumSelectRenderer)).toBeTruthy(); + }); + + it('renders a select input', () => { + expect(wrapper.find('input[type="text"]').exists()).toBe(true); + }); + + it('renders title as label', () => { + expect(wrapper.find('label').text()).toEqual('My Multi Select'); + }); + + it('emits change when an item is selected', async () => { + const select = wrapper.findComponent({ name: 'VSelect' }); + await select.vm.$emit('update:modelValue', ['a', 'b']); + + const component = wrapper.getComponent(MultiEnumSelectRenderer); + expect(component.vm.control.data).toEqual(['a', 'b']); + }); + + it('emits change when an item is deselected', async () => { + const select = wrapper.findComponent({ name: 'VSelect' }); + await select.vm.$emit('update:modelValue', []); + + const component = wrapper.getComponent(MultiEnumSelectRenderer); + expect(component.vm.control.data).toEqual([]); + }); + + it('emits change when cleared', async () => { + const select = wrapper.findComponent({ name: 'VSelect' }); + await select.vm.$emit('update:modelValue', null); + + const component = wrapper.getComponent(MultiEnumSelectRenderer); + // Because `determineClearValue([])` is used by jsonforms natively + // clearing sets the value to undefined to remove it from the data object cleanly + expect(component.vm.control.data).toEqual(undefined); + }); + + it('should render component and match snapshot', () => { + expect(wrapper.html()).toMatchSnapshot(); + }); + }); + + describe('with oneOf schema', () => { + const schema = { + type: 'array', + title: 'My OneOf Multi Select', + uniqueItems: true, + items: { + oneOf: [ + { const: 'a', title: 'Option A' }, + { const: 'b', title: 'Option B' }, + { const: 'c', title: 'Option C' }, + ], + }, + }; + const uischema = { + type: 'Control', + scope: '#', + options: { + format: 'multiselect', + }, + }; + + let wrapper: ReturnType; + + beforeEach(() => { + clearAllIds(); + wrapper = mountJsonForms(['b'], schema, renderers, uischema); + }); + + it('check if child MultiEnumSelectRenderer exists', () => { + expect(wrapper.getComponent(MultiEnumSelectRenderer)).toBeTruthy(); + }); + + it('should render component and match snapshot', () => { + expect(wrapper.html()).toMatchSnapshot(); + }); + }); +}); diff --git a/packages/vue-vuetify/tests/unit/complex/__snapshots__/MultiEnumSelectRenderer.spec.ts.snap b/packages/vue-vuetify/tests/unit/complex/__snapshots__/MultiEnumSelectRenderer.spec.ts.snap new file mode 100644 index 0000000000..97a66f163d --- /dev/null +++ b/packages/vue-vuetify/tests/unit/complex/__snapshots__/MultiEnumSelectRenderer.spec.ts.snap @@ -0,0 +1,145 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`MultiEnumSelectRenderer.vue > with enum schema > should render component and match snapshot 1`] = ` +"
+
+
+
+ +
+ +
+ + +
+
+
+
" +`; + +exports[`MultiEnumSelectRenderer.vue > with oneOf schema > should render component and match snapshot 1`] = ` +"
+
+
+
+ +
+ +
+ + +
+
+
+
" +`; From 2f82463bc9f54bf3434dc7032e4425a203f2179c Mon Sep 17 00:00:00 2001 From: darius-lesch <134329447+darius-lesch@users.noreply.github.com> Date: Fri, 2 Oct 2026 12:47:40 +0000 Subject: [PATCH 02/17] feat: add multi-select example to the dev playground Added a new dedicated example `multi-select` in the `@jsonforms/examples` package to showcase the `MultiEnumSelectRenderer` functionality. Registered the example and successfully verified its appearance in the playground. --- .../examples/src/examples/multi-select.ts | 43 +++++++++++++++++++ packages/examples/src/index.ts | 1 + 2 files changed, 44 insertions(+) create mode 100644 packages/examples/src/examples/multi-select.ts diff --git a/packages/examples/src/examples/multi-select.ts b/packages/examples/src/examples/multi-select.ts new file mode 100644 index 0000000000..8159d39cc7 --- /dev/null +++ b/packages/examples/src/examples/multi-select.ts @@ -0,0 +1,43 @@ +import { registerExamples } from '../register'; + +const schema = { + type: 'object', + properties: { + fruits: { + type: 'array', + title: 'Select Fruits (Dropdown)', + uniqueItems: true, + items: { + type: 'string', + enum: ['Apple', 'Banana', 'Cherry', 'Date', 'Elderberry'], + }, + }, + }, +}; + +const uischema = { + type: 'VerticalLayout', + elements: [ + { + type: 'Control', + scope: '#/properties/fruits', + options: { + format: 'multiselect', + }, + }, + ], +}; + +const data = { + fruits: ['Banana', 'Cherry'], +}; + +registerExamples([ + { + name: 'multi-select', + label: 'multi-select', + data, + schema, + uischema, + }, +]); \ No newline at end of file diff --git a/packages/examples/src/index.ts b/packages/examples/src/index.ts index cf75ae737f..c4a9a58bd0 100644 --- a/packages/examples/src/index.ts +++ b/packages/examples/src/index.ts @@ -154,3 +154,4 @@ export { text, validationNestedSameName, }; +import './examples/multi-select'; From 42b1b169f8c1b0ac104390136714d71494d33665 Mon Sep 17 00:00:00 2001 From: Darius Lesch Date: Fri, 2 Oct 2026 15:11:58 +0200 Subject: [PATCH 03/17] Resolved linting and harmonized imports --- packages/examples/src/examples/multi-select.ts | 3 ++- packages/examples/src/index.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/examples/src/examples/multi-select.ts b/packages/examples/src/examples/multi-select.ts index 8159d39cc7..d8f273d7fd 100644 --- a/packages/examples/src/examples/multi-select.ts +++ b/packages/examples/src/examples/multi-select.ts @@ -40,4 +40,5 @@ registerExamples([ schema, uischema, }, -]); \ No newline at end of file +]); + diff --git a/packages/examples/src/index.ts b/packages/examples/src/index.ts index c4a9a58bd0..122fc0370d 100644 --- a/packages/examples/src/index.ts +++ b/packages/examples/src/index.ts @@ -48,6 +48,7 @@ import * as defaultExample from './examples/default'; import * as enumExample from './examples/enum'; import * as multiEnum from './examples/enum-multi'; import * as multiEnumWithLabelAndDesc from './examples/enum-multi-with-label-and-desc'; +import * as multiSelect from './examples/multi-select'; import * as enumI18n from './examples/enumI18n'; import * as enumInArray from './examples/enumInArray'; import * as generateSchema from './examples/generate'; @@ -131,6 +132,7 @@ export { mixedObject, multiEnum, multiEnumWithLabelAndDesc, + multiSelect, nestedArray, nestedCategorization, numbers, @@ -154,4 +156,3 @@ export { text, validationNestedSameName, }; -import './examples/multi-select'; From f457e129062649af52ef8266e71c3e8ec069995d Mon Sep 17 00:00:00 2001 From: Darius Lesch Date: Tue, 6 Oct 2026 21:33:53 +0200 Subject: [PATCH 04/17] fix: resolved namespace error --- packages/examples/src/examples/multi-select.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/examples/src/examples/multi-select.ts b/packages/examples/src/examples/multi-select.ts index d8f273d7fd..eca2489c3b 100644 --- a/packages/examples/src/examples/multi-select.ts +++ b/packages/examples/src/examples/multi-select.ts @@ -1,6 +1,6 @@ import { registerExamples } from '../register'; -const schema = { +export const schema = { type: 'object', properties: { fruits: { @@ -15,7 +15,7 @@ const schema = { }, }; -const uischema = { +export const uischema = { type: 'VerticalLayout', elements: [ { @@ -28,7 +28,7 @@ const uischema = { ], }; -const data = { +export const data = { fruits: ['Banana', 'Cherry'], }; @@ -41,4 +41,3 @@ registerExamples([ uischema, }, ]); - From 7ef53a0c8c78a1a17f9f0afa5a79c77261a5b94b Mon Sep 17 00:00:00 2001 From: darius-lesch <134329447+darius-lesch@users.noreply.github.com> Date: Wed, 7 Oct 2026 20:32:29 +0000 Subject: [PATCH 05/17] feat(core, vue-vuetify): support atomic array updates for multi-enum controls --- packages/core/src/mappers/renderer.ts | 4 ++ .../src/complex/MultiEnumSelectRenderer.vue | 38 +++++++------------ .../complex/MultiEnumSelectRenderer.spec.ts | 2 +- 3 files changed, 18 insertions(+), 26 deletions(-) diff --git a/packages/core/src/mappers/renderer.ts b/packages/core/src/mappers/renderer.ts index d80392c85f..036677c30d 100644 --- a/packages/core/src/mappers/renderer.ts +++ b/packages/core/src/mappers/renderer.ts @@ -992,6 +992,7 @@ export const mapDispatchToArrayControlProps = ( export interface DispatchPropsOfMultiEnumControl { addItem: (path: string, value: any) => void; removeItem?: (path: string, toDelete: any) => void; + handleChange?: (path: string, value: any) => void; } export const mapDispatchToMultiEnumProps = ( @@ -1017,6 +1018,9 @@ export const mapDispatchToMultiEnumProps = ( }) ); }, + handleChange: (path: string, value: any) => { + dispatch(update(path, () => value)); + }, }); /** diff --git a/packages/vue-vuetify/src/complex/MultiEnumSelectRenderer.vue b/packages/vue-vuetify/src/complex/MultiEnumSelectRenderer.vue index d06bef85ad..51701c89e9 100644 --- a/packages/vue-vuetify/src/complex/MultiEnumSelectRenderer.vue +++ b/packages/vue-vuetify/src/complex/MultiEnumSelectRenderer.vue @@ -20,7 +20,7 @@ :error-messages="control.errors" :clearable="clearable" :model-value="control.data" - :items="selectOptions" + :items="control.options" item-title="label" item-value="value" multiple @@ -45,12 +45,12 @@ import { type ControlElement } from '@jsonforms/core'; import { rendererProps, - useJsonFormsControl, + useJsonFormsMultiEnumControl, type RendererProps, } from '@jsonforms/vue'; import { defineComponent } from 'vue'; import { VSelect } from 'vuetify/components'; -import { determineClearValue, useVuetifyControl } from '../util'; +import { useVuetifyControl } from '../util'; import { default as ControlWrapper } from '../controls/ControlWrapper.vue'; import { DisabledIconFocus } from '../controls/directives'; @@ -67,29 +67,17 @@ const controlRenderer = defineComponent({ ...rendererProps(), }, setup(props: RendererProps) { - const clearValue = determineClearValue([]); - return useVuetifyControl(useJsonFormsControl(props), (value) => - value === null ? clearValue : value || [], - ); - }, - computed: { - selectOptions() { - const itemsSchema = this.control.schema.items as any; - if (itemsSchema.oneOf) { - return itemsSchema.oneOf.map((item: any) => ({ - label: item.title || item.const, - value: item.const, - })); - } - if (itemsSchema.enum) { - return itemsSchema.enum.map((val: any) => ({ - label: val, - value: val, - })); + const control = useJsonFormsMultiEnumControl(props); + const vuetifyControl = useVuetifyControl(control); + + return { + ...vuetifyControl, + onChange: (value: any[] | null) => { + const sanitizedValue = value === null || value.length === 0 ? undefined : value; + control.handleChange?.(control.control.value.path, sanitizedValue); } - return []; - } - } + }; + }, }); export default controlRenderer; diff --git a/packages/vue-vuetify/tests/unit/complex/MultiEnumSelectRenderer.spec.ts b/packages/vue-vuetify/tests/unit/complex/MultiEnumSelectRenderer.spec.ts index 71ff9a3200..cf9c6e6e6d 100644 --- a/packages/vue-vuetify/tests/unit/complex/MultiEnumSelectRenderer.spec.ts +++ b/packages/vue-vuetify/tests/unit/complex/MultiEnumSelectRenderer.spec.ts @@ -57,7 +57,7 @@ describe('MultiEnumSelectRenderer.vue', () => { await select.vm.$emit('update:modelValue', []); const component = wrapper.getComponent(MultiEnumSelectRenderer); - expect(component.vm.control.data).toEqual([]); + expect(component.vm.control.data).toBeUndefined(); }); it('emits change when cleared', async () => { From c9ae9d263651cd3cd1c3d325bb565c1be85c2711 Mon Sep 17 00:00:00 2001 From: darius-lesch <134329447+darius-lesch@users.noreply.github.com> Date: Wed, 7 Oct 2026 21:01:55 +0000 Subject: [PATCH 06/17] refactor(vue-vuetify): extract shared predicate for multi-enum testers Extracted the array/uniqueItems logic currently duplicated in both MultiEnumSelectRenderer.entry.ts and EnumArrayRenderer.entry.ts into a shared `isMultiEnumControl` predicate in `util/tester.ts`. This refactor also replaces the local `hasOneOfItems` and `hasEnumItems` implementations with `@jsonforms/core`'s native `isOneOfEnumSchema` and `isEnumSchema` utilities while retaining the core conditions. --- .../src/complex/EnumArrayRenderer.entry.ts | 27 ++----------------- .../complex/MultiEnumSelectRenderer.entry.ts | 27 ++----------------- packages/vue-vuetify/src/util/index.ts | 1 + packages/vue-vuetify/src/util/tester.ts | 26 ++++++++++++++++++ 4 files changed, 31 insertions(+), 50 deletions(-) create mode 100644 packages/vue-vuetify/src/util/tester.ts diff --git a/packages/vue-vuetify/src/complex/EnumArrayRenderer.entry.ts b/packages/vue-vuetify/src/complex/EnumArrayRenderer.entry.ts index 721b040178..7130e7f7ee 100644 --- a/packages/vue-vuetify/src/complex/EnumArrayRenderer.entry.ts +++ b/packages/vue-vuetify/src/complex/EnumArrayRenderer.entry.ts @@ -1,14 +1,11 @@ import { and, - hasType, rankWith, - schemaMatches, - schemaSubPathMatches, uiTypeIs, type JsonFormsRendererRegistryEntry, - type JsonSchema, } from '@jsonforms/core'; import controlRenderer from './EnumArrayRenderer.vue'; +import { isMultiEnumControl } from '../util/tester'; export const entry: JsonFormsRendererRegistryEntry = { renderer: controlRenderer, @@ -16,27 +13,7 @@ export const entry: JsonFormsRendererRegistryEntry = { 5, and( uiTypeIs('Control'), - and( - schemaMatches( - (schema) => - hasType(schema, 'array') && - !Array.isArray(schema.items) && - schema.uniqueItems === true, - ), - schemaSubPathMatches('items', (schema) => { - return hasOneOfItems(schema) || hasEnumItems(schema); - }), - ), + isMultiEnumControl ), ), }; - -const hasOneOfItems = (schema: JsonSchema): boolean => - schema.oneOf !== undefined && - schema.oneOf.length > 0 && - (schema.oneOf as JsonSchema[]).every((entry: JsonSchema) => { - return entry.const !== undefined; - }); - -const hasEnumItems = (schema: JsonSchema): boolean => - schema.type === 'string' && schema.enum !== undefined; diff --git a/packages/vue-vuetify/src/complex/MultiEnumSelectRenderer.entry.ts b/packages/vue-vuetify/src/complex/MultiEnumSelectRenderer.entry.ts index a42b64917d..7e1be49625 100644 --- a/packages/vue-vuetify/src/complex/MultiEnumSelectRenderer.entry.ts +++ b/packages/vue-vuetify/src/complex/MultiEnumSelectRenderer.entry.ts @@ -1,25 +1,12 @@ import { and, - hasType, optionIs, rankWith, - schemaMatches, - schemaSubPathMatches, uiTypeIs, type JsonFormsRendererRegistryEntry, - type JsonSchema, } from '@jsonforms/core'; import controlRenderer from './MultiEnumSelectRenderer.vue'; - -const hasOneOfItems = (schema: JsonSchema): boolean => - schema.oneOf !== undefined && - schema.oneOf.length > 0 && - (schema.oneOf as JsonSchema[]).every((entry: JsonSchema) => { - return entry.const !== undefined; - }); - -const hasEnumItems = (schema: JsonSchema): boolean => - schema.type === 'string' && schema.enum !== undefined; +import { isMultiEnumControl } from '../util/tester'; export const entry: JsonFormsRendererRegistryEntry = { renderer: controlRenderer, @@ -28,17 +15,7 @@ export const entry: JsonFormsRendererRegistryEntry = { and( uiTypeIs('Control'), optionIs('format', 'multiselect'), - and( - schemaMatches( - (schema) => - hasType(schema, 'array') && - !Array.isArray(schema.items) && - schema.uniqueItems === true, - ), - schemaSubPathMatches('items', (schema) => { - return hasOneOfItems(schema) || hasEnumItems(schema); - }), - ), + isMultiEnumControl ), ), }; diff --git a/packages/vue-vuetify/src/util/index.ts b/packages/vue-vuetify/src/util/index.ts index e3c6d25f7b..6651966769 100644 --- a/packages/vue-vuetify/src/util/index.ts +++ b/packages/vue-vuetify/src/util/index.ts @@ -3,3 +3,4 @@ export * from './datejs'; export * from './inject'; export * from './options'; export * from './validator'; +export * from './tester'; diff --git a/packages/vue-vuetify/src/util/tester.ts b/packages/vue-vuetify/src/util/tester.ts new file mode 100644 index 0000000000..6b195726bf --- /dev/null +++ b/packages/vue-vuetify/src/util/tester.ts @@ -0,0 +1,26 @@ +import { + and, + hasType, + isEnumSchema, + isOneOfEnumSchema, + schemaMatches, + schemaSubPathMatches, + type JsonSchema, +} from '@jsonforms/core'; + +export const isMultiEnumControl = and( + schemaMatches( + (schema) => + hasType(schema, 'array') && + !Array.isArray(schema.items) && + schema.uniqueItems === true, + ), + schemaSubPathMatches('items', (schema) => { + return ( + (isOneOfEnumSchema(schema) && + schema.oneOf !== undefined && + (schema.oneOf as JsonSchema[]).length > 0) || + (schema.type === 'string' && isEnumSchema(schema)) + ); + }), +); From 0c76e8aa9482a7417062357f4de0e3061432f1f3 Mon Sep 17 00:00:00 2001 From: darius-lesch <134329447+darius-lesch@users.noreply.github.com> Date: Wed, 7 Oct 2026 21:21:52 +0000 Subject: [PATCH 07/17] chore(examples): consolidate multi-select into enum-multi and clean spec comments --- packages/examples/src/examples/enum-multi.ts | 22 +++++++++- .../examples/src/examples/multi-select.ts | 43 ------------------- packages/examples/src/index.ts | 3 +- .../complex/MultiEnumSelectRenderer.spec.ts | 2 - 4 files changed, 22 insertions(+), 48 deletions(-) delete mode 100644 packages/examples/src/examples/multi-select.ts diff --git a/packages/examples/src/examples/enum-multi.ts b/packages/examples/src/examples/enum-multi.ts index 5a7b235b4e..0765ff7aeb 100644 --- a/packages/examples/src/examples/enum-multi.ts +++ b/packages/examples/src/examples/enum-multi.ts @@ -46,6 +46,15 @@ export const schema = { enum: ['foo', 'bar', 'foobar'], }, }, + multiSelect: { + type: 'array', + title: 'Select Fruits', + uniqueItems: true, + items: { + type: 'string', + enum: ['Apple', 'Banana', 'Cherry', 'Date', 'Elderberry'], + }, + }, }, }; @@ -60,10 +69,21 @@ export const uischema = { type: 'Control', scope: '#/properties/multiEnum', }, + { + type: 'Control', + scope: '#/properties/multiSelect', + options: { + format: 'multiselect', + }, + }, ], }; -export const data = { oneOfMultiEnum: ['foo'], multiEnum: ['bar'] }; +export const data = { + oneOfMultiEnum: ['foo'], + multiEnum: ['bar'], + multiSelect: ['Banana', 'Cherry'], +}; registerExamples([ { diff --git a/packages/examples/src/examples/multi-select.ts b/packages/examples/src/examples/multi-select.ts deleted file mode 100644 index eca2489c3b..0000000000 --- a/packages/examples/src/examples/multi-select.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { registerExamples } from '../register'; - -export const schema = { - type: 'object', - properties: { - fruits: { - type: 'array', - title: 'Select Fruits (Dropdown)', - uniqueItems: true, - items: { - type: 'string', - enum: ['Apple', 'Banana', 'Cherry', 'Date', 'Elderberry'], - }, - }, - }, -}; - -export const uischema = { - type: 'VerticalLayout', - elements: [ - { - type: 'Control', - scope: '#/properties/fruits', - options: { - format: 'multiselect', - }, - }, - ], -}; - -export const data = { - fruits: ['Banana', 'Cherry'], -}; - -registerExamples([ - { - name: 'multi-select', - label: 'multi-select', - data, - schema, - uischema, - }, -]); diff --git a/packages/examples/src/index.ts b/packages/examples/src/index.ts index 122fc0370d..08be01580f 100644 --- a/packages/examples/src/index.ts +++ b/packages/examples/src/index.ts @@ -48,7 +48,7 @@ import * as defaultExample from './examples/default'; import * as enumExample from './examples/enum'; import * as multiEnum from './examples/enum-multi'; import * as multiEnumWithLabelAndDesc from './examples/enum-multi-with-label-and-desc'; -import * as multiSelect from './examples/multi-select'; + import * as enumI18n from './examples/enumI18n'; import * as enumInArray from './examples/enumInArray'; import * as generateSchema from './examples/generate'; @@ -132,7 +132,6 @@ export { mixedObject, multiEnum, multiEnumWithLabelAndDesc, - multiSelect, nestedArray, nestedCategorization, numbers, diff --git a/packages/vue-vuetify/tests/unit/complex/MultiEnumSelectRenderer.spec.ts b/packages/vue-vuetify/tests/unit/complex/MultiEnumSelectRenderer.spec.ts index cf9c6e6e6d..fab56042f0 100644 --- a/packages/vue-vuetify/tests/unit/complex/MultiEnumSelectRenderer.spec.ts +++ b/packages/vue-vuetify/tests/unit/complex/MultiEnumSelectRenderer.spec.ts @@ -65,8 +65,6 @@ describe('MultiEnumSelectRenderer.vue', () => { await select.vm.$emit('update:modelValue', null); const component = wrapper.getComponent(MultiEnumSelectRenderer); - // Because `determineClearValue([])` is used by jsonforms natively - // clearing sets the value to undefined to remove it from the data object cleanly expect(component.vm.control.data).toEqual(undefined); }); From e7a017137d4190053ffd8e5bd96c0ed6688a5b44 Mon Sep 17 00:00:00 2001 From: Darius Lesch Date: Wed, 7 Oct 2026 23:41:42 +0200 Subject: [PATCH 08/17] fix: prettier formatting fix applied --- .../vue-vuetify/src/complex/EnumArrayRenderer.entry.ts | 8 +------- .../src/complex/MultiEnumSelectRenderer.entry.ts | 2 +- .../vue-vuetify/src/complex/MultiEnumSelectRenderer.vue | 5 +++-- packages/vue-vuetify/src/util/tester.ts | 4 ++-- 4 files changed, 7 insertions(+), 12 deletions(-) diff --git a/packages/vue-vuetify/src/complex/EnumArrayRenderer.entry.ts b/packages/vue-vuetify/src/complex/EnumArrayRenderer.entry.ts index 7130e7f7ee..05798828dc 100644 --- a/packages/vue-vuetify/src/complex/EnumArrayRenderer.entry.ts +++ b/packages/vue-vuetify/src/complex/EnumArrayRenderer.entry.ts @@ -9,11 +9,5 @@ import { isMultiEnumControl } from '../util/tester'; export const entry: JsonFormsRendererRegistryEntry = { renderer: controlRenderer, - tester: rankWith( - 5, - and( - uiTypeIs('Control'), - isMultiEnumControl - ), - ), + tester: rankWith(5, and(uiTypeIs('Control'), isMultiEnumControl)), }; diff --git a/packages/vue-vuetify/src/complex/MultiEnumSelectRenderer.entry.ts b/packages/vue-vuetify/src/complex/MultiEnumSelectRenderer.entry.ts index 7e1be49625..aa06f0d7df 100644 --- a/packages/vue-vuetify/src/complex/MultiEnumSelectRenderer.entry.ts +++ b/packages/vue-vuetify/src/complex/MultiEnumSelectRenderer.entry.ts @@ -16,6 +16,6 @@ export const entry: JsonFormsRendererRegistryEntry = { uiTypeIs('Control'), optionIs('format', 'multiselect'), isMultiEnumControl - ), + ) ), }; diff --git a/packages/vue-vuetify/src/complex/MultiEnumSelectRenderer.vue b/packages/vue-vuetify/src/complex/MultiEnumSelectRenderer.vue index 51701c89e9..4d8b3383da 100644 --- a/packages/vue-vuetify/src/complex/MultiEnumSelectRenderer.vue +++ b/packages/vue-vuetify/src/complex/MultiEnumSelectRenderer.vue @@ -73,9 +73,10 @@ const controlRenderer = defineComponent({ return { ...vuetifyControl, onChange: (value: any[] | null) => { - const sanitizedValue = value === null || value.length === 0 ? undefined : value; + const sanitizedValue = + value === null || value.length === 0 ? undefined : value; control.handleChange?.(control.control.value.path, sanitizedValue); - } + }, }; }, }); diff --git a/packages/vue-vuetify/src/util/tester.ts b/packages/vue-vuetify/src/util/tester.ts index 6b195726bf..38c1efbdee 100644 --- a/packages/vue-vuetify/src/util/tester.ts +++ b/packages/vue-vuetify/src/util/tester.ts @@ -13,7 +13,7 @@ export const isMultiEnumControl = and( (schema) => hasType(schema, 'array') && !Array.isArray(schema.items) && - schema.uniqueItems === true, + schema.uniqueItems === true ), schemaSubPathMatches('items', (schema) => { return ( @@ -22,5 +22,5 @@ export const isMultiEnumControl = and( (schema.oneOf as JsonSchema[]).length > 0) || (schema.type === 'string' && isEnumSchema(schema)) ); - }), + }) ); From ce954975c3719ea621731cacf4f72f3bd4438c18 Mon Sep 17 00:00:00 2001 From: Darius Lesch Date: Thu, 8 Oct 2026 15:21:31 +0200 Subject: [PATCH 09/17] fix: removed leftover blank line --- packages/examples/src/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/examples/src/index.ts b/packages/examples/src/index.ts index 08be01580f..cf75ae737f 100644 --- a/packages/examples/src/index.ts +++ b/packages/examples/src/index.ts @@ -48,7 +48,6 @@ import * as defaultExample from './examples/default'; import * as enumExample from './examples/enum'; import * as multiEnum from './examples/enum-multi'; import * as multiEnumWithLabelAndDesc from './examples/enum-multi-with-label-and-desc'; - import * as enumI18n from './examples/enumI18n'; import * as enumInArray from './examples/enumInArray'; import * as generateSchema from './examples/generate'; From 3631b4f704864f87da66f72b173860fed5a02f3d Mon Sep 17 00:00:00 2001 From: darius-lesch <134329447+darius-lesch@users.noreply.github.com> Date: Thu, 8 Oct 2026 14:04:06 +0000 Subject: [PATCH 10/17] refactor(vue-vuetify): utilize `adaptValue` in `MultiEnumSelectRenderer` Replaces the manual `onChange` event wrapper in `MultiEnumSelectRenderer.vue` by leveraging the native `adaptValue` argument of `useVuetifyControl`. Restores `determineClearValue([])` to safely evaluate and assign clear values when the selected array is null or empty, ensuring dynamic property states remain consistent. --- .../src/complex/MultiEnumSelectRenderer.vue | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/packages/vue-vuetify/src/complex/MultiEnumSelectRenderer.vue b/packages/vue-vuetify/src/complex/MultiEnumSelectRenderer.vue index 4d8b3383da..4a10cabe59 100644 --- a/packages/vue-vuetify/src/complex/MultiEnumSelectRenderer.vue +++ b/packages/vue-vuetify/src/complex/MultiEnumSelectRenderer.vue @@ -50,7 +50,7 @@ import { } from '@jsonforms/vue'; import { defineComponent } from 'vue'; import { VSelect } from 'vuetify/components'; -import { useVuetifyControl } from '../util'; +import { determineClearValue, useVuetifyControl } from '../util'; import { default as ControlWrapper } from '../controls/ControlWrapper.vue'; import { DisabledIconFocus } from '../controls/directives'; @@ -67,17 +67,10 @@ const controlRenderer = defineComponent({ ...rendererProps(), }, setup(props: RendererProps) { - const control = useJsonFormsMultiEnumControl(props); - const vuetifyControl = useVuetifyControl(control); - - return { - ...vuetifyControl, - onChange: (value: any[] | null) => { - const sanitizedValue = - value === null || value.length === 0 ? undefined : value; - control.handleChange?.(control.control.value.path, sanitizedValue); - }, - }; + const clearValue = determineClearValue([]); + return useVuetifyControl(useJsonFormsMultiEnumControl(props), (value) => + value === null || value.length === 0 ? clearValue : value, + ); }, }); From 1bf9f57517b29d3b7cb67e5c5e895317a2674fed Mon Sep 17 00:00:00 2001 From: darius-lesch <134329447+darius-lesch@users.noreply.github.com> Date: Thu, 8 Oct 2026 14:09:05 +0000 Subject: [PATCH 11/17] refactor(vue-vuetify): standardize `isMultiEnumControl` API Encapsulated the `uiTypeIs('Control')` check natively inside the `isMultiEnumControl` tester utility to match the public API consistency of other core core testers. Removed redundant wrapper checks and orphaned imports from consuming entry files. --- .../vue-vuetify/src/complex/EnumArrayRenderer.entry.ts | 9 ++------- .../src/complex/MultiEnumSelectRenderer.entry.ts | 7 +------ packages/vue-vuetify/src/util/tester.ts | 6 ++++-- 3 files changed, 7 insertions(+), 15 deletions(-) diff --git a/packages/vue-vuetify/src/complex/EnumArrayRenderer.entry.ts b/packages/vue-vuetify/src/complex/EnumArrayRenderer.entry.ts index 05798828dc..891688e126 100644 --- a/packages/vue-vuetify/src/complex/EnumArrayRenderer.entry.ts +++ b/packages/vue-vuetify/src/complex/EnumArrayRenderer.entry.ts @@ -1,13 +1,8 @@ -import { - and, - rankWith, - uiTypeIs, - type JsonFormsRendererRegistryEntry, -} from '@jsonforms/core'; +import { rankWith, type JsonFormsRendererRegistryEntry } from '@jsonforms/core'; import controlRenderer from './EnumArrayRenderer.vue'; import { isMultiEnumControl } from '../util/tester'; export const entry: JsonFormsRendererRegistryEntry = { renderer: controlRenderer, - tester: rankWith(5, and(uiTypeIs('Control'), isMultiEnumControl)), + tester: rankWith(5, isMultiEnumControl), }; diff --git a/packages/vue-vuetify/src/complex/MultiEnumSelectRenderer.entry.ts b/packages/vue-vuetify/src/complex/MultiEnumSelectRenderer.entry.ts index aa06f0d7df..bd3d58939f 100644 --- a/packages/vue-vuetify/src/complex/MultiEnumSelectRenderer.entry.ts +++ b/packages/vue-vuetify/src/complex/MultiEnumSelectRenderer.entry.ts @@ -2,7 +2,6 @@ import { and, optionIs, rankWith, - uiTypeIs, type JsonFormsRendererRegistryEntry, } from '@jsonforms/core'; import controlRenderer from './MultiEnumSelectRenderer.vue'; @@ -12,10 +11,6 @@ export const entry: JsonFormsRendererRegistryEntry = { renderer: controlRenderer, tester: rankWith( 10, - and( - uiTypeIs('Control'), - optionIs('format', 'multiselect'), - isMultiEnumControl - ) + and(optionIs('format', 'multiselect'), isMultiEnumControl), ), }; diff --git a/packages/vue-vuetify/src/util/tester.ts b/packages/vue-vuetify/src/util/tester.ts index 38c1efbdee..691773a4af 100644 --- a/packages/vue-vuetify/src/util/tester.ts +++ b/packages/vue-vuetify/src/util/tester.ts @@ -5,15 +5,17 @@ import { isOneOfEnumSchema, schemaMatches, schemaSubPathMatches, + uiTypeIs, type JsonSchema, } from '@jsonforms/core'; export const isMultiEnumControl = and( + uiTypeIs('Control'), schemaMatches( (schema) => hasType(schema, 'array') && !Array.isArray(schema.items) && - schema.uniqueItems === true + schema.uniqueItems === true, ), schemaSubPathMatches('items', (schema) => { return ( @@ -22,5 +24,5 @@ export const isMultiEnumControl = and( (schema.oneOf as JsonSchema[]).length > 0) || (schema.type === 'string' && isEnumSchema(schema)) ); - }) + }), ); From 3b1351314f34b096fc8aeefe1a5274c9c83d0ca6 Mon Sep 17 00:00:00 2001 From: darius-lesch <134329447+darius-lesch@users.noreply.github.com> Date: Thu, 8 Oct 2026 14:11:32 +0000 Subject: [PATCH 12/17] chore(core): DRY up mapDispatchToMultiEnumProps handleChange implementation Refactors `mapDispatchToMultiEnumProps` to use object spread with `mapDispatchToControlProps` in order to reuse the standard `handleChange` dispatcher, removing duplicated code. Also adds an appropriate unit test to ensure `handleChange` mutations apply correctly for multi-enum core setups. --- packages/core/src/mappers/renderer.ts | 163 +++++++++--------- packages/core/test/mappers/renderer.test.ts | 175 ++++++++++++-------- 2 files changed, 183 insertions(+), 155 deletions(-) diff --git a/packages/core/src/mappers/renderer.ts b/packages/core/src/mappers/renderer.ts index 036677c30d..1f604f6ccc 100644 --- a/packages/core/src/mappers/renderer.ts +++ b/packages/core/src/mappers/renderer.ts @@ -107,20 +107,20 @@ export const moveDown = (array: any[], toMove: number) => { const isRequired = ( schema: JsonSchema, schemaPath: string, - rootSchema: JsonSchema + rootSchema: JsonSchema, ): boolean => { const pathSegments = schemaPath.split('/'); const lastSegment = decode(pathSegments[pathSegments.length - 1]); // Skip "properties", "items" etc. to resolve the parent const nextHigherSchemaSegments = pathSegments.slice( 0, - pathSegments.length - 2 + pathSegments.length - 2, ); const nextHigherSchemaPath = nextHigherSchemaSegments.join('/'); const nextHigherSchema = Resolve.schema( schema, nextHigherSchemaPath, - rootSchema + rootSchema, ); return ( @@ -142,7 +142,7 @@ const isRequired = ( export const computeLabel = ( label: string | undefined, required: boolean, - hideRequiredAsterisk: boolean + hideRequiredAsterisk: boolean, ): string => { return `${label ?? ''}${required && !hideRequiredAsterisk ? '*' : ''}`; }; @@ -156,7 +156,7 @@ export const computeLabel = ( */ export const showAsRequired = ( required: boolean, - hideRequiredAsterisk: boolean + hideRequiredAsterisk: boolean, ): boolean => { return required && !hideRequiredAsterisk; }; @@ -168,7 +168,7 @@ export const showAsRequired = ( */ export const createDefaultValue = ( schema: JsonSchema, - rootSchema: JsonSchema + rootSchema: JsonSchema, ) => { const defaultValue = doCreateDefaultValue(schema, rootSchema); @@ -183,7 +183,7 @@ export const createDefaultValue = ( */ export const doCreateDefaultValue = ( schema: JsonSchema, - rootSchema: JsonSchema + rootSchema: JsonSchema, ) => { const resolvedSchema = typeof schema.$ref === 'string' @@ -223,7 +223,7 @@ export const doCreateDefaultValue = ( if (schema[combinator] && Array.isArray(schema[combinator])) { const combinatorDefault = createDefaultValueForCombinatorSchema( schema[combinator], - rootSchema + rootSchema, ); if (combinatorDefault !== undefined) { return combinatorDefault; @@ -237,7 +237,7 @@ export const doCreateDefaultValue = ( const createDefaultValueForCombinatorSchema = ( combinatorSchemas: JsonSchema[], - rootSchema: JsonSchema + rootSchema: JsonSchema, ): any => { if (combinatorSchemas.length > 0) { for (const combinatorSchema of combinatorSchemas) { @@ -304,7 +304,7 @@ export const isDescriptionHidden = ( visible: boolean, description: string | undefined, isFocused: boolean, - showUnfocusedDescription: boolean + showUnfocusedDescription: boolean, ): boolean => { return ( description === undefined || @@ -325,7 +325,7 @@ export interface EnumOption { export const enumToEnumOptionMapper = ( e: any, t?: Translator, - i18nKey?: string + i18nKey?: string, ): EnumOption => { let label = typeof e === 'string' ? e : JSON.stringify(e); if (t) { @@ -341,7 +341,7 @@ export const enumToEnumOptionMapper = ( export const oneOfToEnumOptionMapper = ( e: any, t?: Translator, - fallbackI18nKey?: string + fallbackI18nKey?: string, ): EnumOption => { let label = e.title ?? @@ -548,8 +548,7 @@ export interface DispatchPropsOfControl { * Props of a Control. */ export interface ControlProps - extends StatePropsOfControl, - DispatchPropsOfControl {} + extends StatePropsOfControl, DispatchPropsOfControl {} /** * State props of a layout; @@ -587,7 +586,7 @@ export interface ControlState { */ export const mapStateToControlProps = ( state: JsonFormsState, - ownProps: OwnPropsOfControl + ownProps: OwnPropsOfControl, ): StatePropsOfControl => { const { uischema } = ownProps; const rootData = getData(state); @@ -607,7 +606,7 @@ export const mapStateToControlProps = ( const resolvedSchema = Resolve.schema( ownProps.schema || rootSchema, controlElement.scope, - rootSchema + rootSchema, ); const errors = getErrorAt(path, resolvedSchema)(state); @@ -622,7 +621,7 @@ export const mapStateToControlProps = ( uischema, resolvedSchema || rootSchema, rootData, - config + config, ); const readonly: boolean = isInherentlyReadonly( state, @@ -630,7 +629,7 @@ export const mapStateToControlProps = ( uischema, resolvedSchema || rootSchema, rootData, - config + config, ); const schema = resolvedSchema ?? rootSchema; @@ -646,7 +645,7 @@ export const mapStateToControlProps = ( const i18nDescription = t( getI18nKey(schema, uischema, path, 'description'), description, - { schema, uischema, path, errors } + { schema, uischema, path, errors }, ); const i18nErrorMessage = getCombinedErrorMessage( errors, @@ -654,7 +653,7 @@ export const mapStateToControlProps = ( t, schema, uischema, - path + path, ); return { @@ -685,7 +684,7 @@ export const mapStateToControlProps = ( * @returns {DispatchPropsOfControl} dispatch props for a control */ export const mapDispatchToControlProps = ( - dispatch: Dispatch + dispatch: Dispatch, ): DispatchPropsOfControl => ({ handleChange(path, value) { dispatch(update(path, () => value)); @@ -700,7 +699,7 @@ export const mapDispatchToControlProps = ( */ export const mapStateToEnumControlProps = ( state: JsonFormsState, - ownProps: OwnPropsOfControl & OwnPropsOfEnum + ownProps: OwnPropsOfControl & OwnPropsOfEnum, ): StatePropsOfControl & OwnPropsOfEnum => { const props: StatePropsOfControl = mapStateToControlProps(state, ownProps); const options: EnumOption[] = @@ -709,14 +708,14 @@ export const mapStateToEnumControlProps = ( enumToEnumOptionMapper( e, getTranslator()(state), - getI18nKeyPrefix(props.schema, props.uischema, props.path) - ) + getI18nKeyPrefix(props.schema, props.uischema, props.path), + ), ) || (props.schema.const && [ enumToEnumOptionMapper( props.schema.const, getTranslator()(state), - getI18nKeyPrefix(props.schema, props.uischema, props.path) + getI18nKeyPrefix(props.schema, props.uischema, props.path), ), ]); return { @@ -733,7 +732,7 @@ export const mapStateToEnumControlProps = ( */ export const mapStateToOneOfEnumControlProps = ( state: JsonFormsState, - ownProps: OwnPropsOfControl & OwnPropsOfEnum + ownProps: OwnPropsOfControl & OwnPropsOfEnum, ): StatePropsOfControl & OwnPropsOfEnum => { const props: StatePropsOfControl = mapStateToControlProps(state, ownProps); const options: EnumOption[] = @@ -742,8 +741,8 @@ export const mapStateToOneOfEnumControlProps = ( oneOfToEnumOptionMapper( oneOfSubSchema, getTranslator()(state), - getI18nKeyPrefix(props.schema, props.uischema, props.path) - ) + getI18nKeyPrefix(props.schema, props.uischema, props.path), + ), ); return { ...props, @@ -759,7 +758,7 @@ export const mapStateToOneOfEnumControlProps = ( */ export const mapStateToMultiEnumControlProps = ( state: JsonFormsState, - ownProps: OwnPropsOfControl & OwnPropsOfEnum + ownProps: OwnPropsOfControl & OwnPropsOfEnum, ): StatePropsOfControl & OwnPropsOfEnum => { const props: StatePropsOfControl = mapStateToControlProps(state, ownProps); let items = props.schema.items as JsonSchema; @@ -774,15 +773,15 @@ export const mapStateToMultiEnumControlProps = ( oneOfToEnumOptionMapper( oneOfSubSchema, state.jsonforms.i18n?.translate, - getI18nKeyPrefix(props.schema, props.uischema, props.path) - ) + getI18nKeyPrefix(props.schema, props.uischema, props.path), + ), )) || items?.enum?.map((e) => enumToEnumOptionMapper( e, state.jsonforms.i18n?.translate, - getI18nKeyPrefix(props.schema, props.uischema, props.path) - ) + getI18nKeyPrefix(props.schema, props.uischema, props.path), + ), ); return { ...props, @@ -798,7 +797,7 @@ export const mapStateToMultiEnumControlProps = ( */ export const mapStateToMasterListItemProps = ( state: JsonFormsState, - ownProps: OwnPropsOfMasterListItem + ownProps: OwnPropsOfMasterListItem, ): StatePropsOfMasterItem => { const { schema, path, uischema, childLabelProp, index } = ownProps; const childPath = composePaths(path, `${index}`); @@ -809,7 +808,7 @@ export const mapStateToMasterListItemProps = ( schema, getSchema(state), state.jsonforms.i18n.translate, - uischema + uischema, ); return { @@ -854,7 +853,7 @@ export interface StatePropsOfMasterItem extends OwnPropsOfMasterListItem { */ export const mapStateToControlWithDetailProps = ( state: JsonFormsState, - ownProps: OwnPropsOfControl + ownProps: OwnPropsOfControl, ): StatePropsOfControlWithDetail => { const { ...props } = mapStateToControlProps(state, ownProps); @@ -865,14 +864,12 @@ export const mapStateToControlWithDetailProps = ( }; export interface ControlWithDetailProps - extends StatePropsOfControlWithDetail, - DispatchPropsOfControl {} + extends StatePropsOfControlWithDetail, DispatchPropsOfControl {} /** * State-based props of a table control. */ -export interface StatePropsOfArrayControl - extends StatePropsOfControlWithDetail { +export interface StatePropsOfArrayControl extends StatePropsOfControlWithDetail { arraySchema: JsonSchema; childErrors?: ErrorObject[]; } @@ -886,7 +883,7 @@ export interface StatePropsOfArrayControl */ export const mapStateToArrayControlProps = ( state: JsonFormsState, - ownProps: OwnPropsOfControl + ownProps: OwnPropsOfControl, ): StatePropsOfArrayControl => { const { path, schema, uischema, label, ...props } = mapStateToControlWithDetailProps(state, ownProps); @@ -924,7 +921,7 @@ export interface DispatchPropsOfArrayControl { * @returns {DispatchPropsOfArrayControl} dispatch props of an array control */ export const mapDispatchToArrayControlProps = ( - dispatch: Dispatch + dispatch: Dispatch, ): DispatchPropsOfArrayControl => ({ addItem: (path: string, value: any) => () => { dispatch( @@ -938,8 +935,8 @@ export const mapDispatchToArrayControlProps = ( array.push(value); return array; }, - { type: 'ADD', values: [value] } as UpdateArrayContext - ) + { type: 'ADD', values: [value] } as UpdateArrayContext, + ), ); }, removeItems: (path: string, toDelete: number[]) => () => { @@ -953,8 +950,8 @@ export const mapDispatchToArrayControlProps = ( .forEach((s) => array.splice(s, 1)); return array; }, - { type: 'REMOVE', indices: toDelete } as UpdateArrayContext - ) + { type: 'REMOVE', indices: toDelete } as UpdateArrayContext, + ), ); }, moveUp: (path, toMove: number) => () => { @@ -968,8 +965,8 @@ export const mapDispatchToArrayControlProps = ( { type: 'MOVE', moves: [{ from: toMove, to: toMove - 1 }], - } as UpdateArrayContext - ) + } as UpdateArrayContext, + ), ); }, moveDown: (path, toMove: number) => () => { @@ -983,8 +980,8 @@ export const mapDispatchToArrayControlProps = ( { type: 'MOVE', moves: [{ from: toMove, to: toMove + 1 }], - } as UpdateArrayContext - ) + } as UpdateArrayContext, + ), ); }, }); @@ -996,8 +993,9 @@ export interface DispatchPropsOfMultiEnumControl { } export const mapDispatchToMultiEnumProps = ( - dispatch: Dispatch + dispatch: Dispatch, ): DispatchPropsOfMultiEnumControl => ({ + ...mapDispatchToControlProps(dispatch as any), addItem: (path: string, value: any) => { dispatch( update(path, (data) => { @@ -1006,7 +1004,7 @@ export const mapDispatchToMultiEnumProps = ( } data.push(value); return data; - }) + }), ); }, removeItem: (path: string, toDelete: any) => { @@ -1015,20 +1013,16 @@ export const mapDispatchToMultiEnumProps = ( const indexInData = data.indexOf(toDelete); data.splice(indexInData, 1); return data; - }) + }), ); }, - handleChange: (path: string, value: any) => { - dispatch(update(path, () => value)); - }, }); /** * Props of an array control. */ export interface ArrayControlProps - extends StatePropsOfArrayControl, - DispatchPropsOfArrayControl {} + extends StatePropsOfArrayControl, DispatchPropsOfArrayControl {} export const layoutDefaultProps: { visible: boolean; @@ -1060,7 +1054,7 @@ const getDirection = (uischema: UISchemaElement) => { */ export const mapStateToLayoutProps = ( state: JsonFormsState, - ownProps: OwnPropsOfLayout + ownProps: OwnPropsOfLayout, ): LayoutProps => { const rootData = getData(state); const { uischema } = ownProps; @@ -1071,7 +1065,7 @@ export const mapStateToLayoutProps = ( rootData, ownProps.path, getAjv(state), - getConfig(state) + getConfig(state), ) : ownProps.visible; @@ -1083,7 +1077,7 @@ export const mapStateToLayoutProps = ( uischema, undefined, // layouts have no associated schema rootData, - config + config, ); const readonly: boolean = isInherentlyReadonly( state, @@ -1091,7 +1085,7 @@ export const mapStateToLayoutProps = ( uischema, undefined, // layouts have no associated schema rootData, - config + config, ); // some layouts have labels which might need to be translated @@ -1121,8 +1115,7 @@ export type RefResolver = (schema: JsonSchema) => Promise; export interface OwnPropsOfJsonFormsRenderer extends OwnPropsOfRenderer {} -export interface StatePropsOfJsonFormsRenderer - extends OwnPropsOfJsonFormsRenderer { +export interface StatePropsOfJsonFormsRenderer extends OwnPropsOfJsonFormsRenderer { rootSchema: JsonSchema; config: any; } @@ -1131,7 +1124,7 @@ export interface JsonFormsProps extends StatePropsOfJsonFormsRenderer {} export const mapStateToJsonFormsRendererProps = ( state: JsonFormsState, - ownProps: OwnPropsOfJsonFormsRenderer + ownProps: OwnPropsOfJsonFormsRenderer, ): StatePropsOfJsonFormsRenderer => { return { renderers: ownProps.renderers || get(state.jsonforms, 'renderers'), @@ -1162,7 +1155,7 @@ export interface StatePropsOfCombinator extends StatePropsOfControl { export const mapStateToCombinatorRendererProps = ( state: JsonFormsState, ownProps: OwnPropsOfControl, - keyword: CombinatorKeyword + keyword: CombinatorKeyword, ): StatePropsOfCombinator => { const { data, schema, rootSchema, i18nKeyPrefix, label, ...props } = mapStateToControlProps(state, ownProps); @@ -1200,7 +1193,7 @@ export const mapStateToCombinatorRendererProps = ( } } catch (error) { console.debug( - "Combinator subschema is not self contained, can't hand it over to AJV" + "Combinator subschema is not self contained, can't hand it over to AJV", ); } } @@ -1218,8 +1211,7 @@ export const mapStateToCombinatorRendererProps = ( }; export interface CombinatorRendererProps - extends StatePropsOfCombinator, - DispatchPropsOfControl {} + extends StatePropsOfCombinator, DispatchPropsOfControl {} /** * Map state to all of renderer props. * @param state the store's state @@ -1228,20 +1220,20 @@ export interface CombinatorRendererProps */ export const mapStateToAllOfProps = ( state: JsonFormsState, - ownProps: OwnPropsOfControl + ownProps: OwnPropsOfControl, ): StatePropsOfCombinator => mapStateToCombinatorRendererProps(state, ownProps, 'allOf'); export const mapStateToAnyOfProps = ( state: JsonFormsState, - ownProps: OwnPropsOfControl + ownProps: OwnPropsOfControl, ): StatePropsOfCombinator => { return mapStateToCombinatorRendererProps(state, ownProps, 'anyOf'); }; export const mapStateToOneOfProps = ( state: JsonFormsState, - ownProps: OwnPropsOfControl + ownProps: OwnPropsOfControl, ): StatePropsOfCombinator => { return mapStateToCombinatorRendererProps(state, ownProps, 'oneOf'); }; @@ -1265,7 +1257,7 @@ export interface StatePropsOfArrayLayout extends StatePropsOfControlWithDetail { */ export const mapStateToArrayLayoutProps = ( state: JsonFormsState, - ownProps: OwnPropsOfControl + ownProps: OwnPropsOfControl, ): StatePropsOfArrayLayout => { const { path, schema, uischema, errors, label, ...props } = mapStateToControlWithDetailProps(state, ownProps); @@ -1279,7 +1271,7 @@ export const mapStateToArrayLayoutProps = ( t, undefined, undefined, - undefined + undefined, ); const allErrors = @@ -1303,8 +1295,7 @@ export const mapStateToArrayLayoutProps = ( * Props of an array control. */ export interface ArrayLayoutProps - extends StatePropsOfArrayLayout, - DispatchPropsOfArrayControl {} + extends StatePropsOfArrayLayout, DispatchPropsOfArrayControl {} export interface StatePropsOfLabel extends StatePropsOfRenderer { text?: string; @@ -1313,7 +1304,7 @@ export interface LabelProps extends StatePropsOfLabel {} export const mapStateToLabelProps = ( state: JsonFormsState, - props: OwnPropsOfLabel + props: OwnPropsOfLabel, ) => { const { uischema } = props; @@ -1324,14 +1315,14 @@ export const mapStateToLabelProps = ( getData(state), props.path, getAjv(state), - getConfig(state) + getConfig(state), ) : props.visible; const text = uischema.text; const t = getTranslator()(state); const i18nKeyPrefix = getI18nKeyPrefixBySchema(undefined, uischema); - const i18nKey = i18nKeyPrefix ? `${i18nKeyPrefix}.text` : text ?? ''; + const i18nKey = i18nKeyPrefix ? `${i18nKeyPrefix}.text` : (text ?? ''); const i18nText = t(i18nKey, text, { uischema }); return { @@ -1361,7 +1352,7 @@ export const computeChildLabel = ( schema: JsonSchema, rootSchema: JsonSchema, translateFct: Translator, - uiSchema: UISchemaElement + uiSchema: UISchemaElement, ): string => { const childData = Resolve.data(data, childPath); @@ -1385,7 +1376,7 @@ export const computeChildLabel = ( const childSchema = Resolve.schema( schema, '#' + getPropPath(childLabelProp), - rootSchema + rootSchema, ); let enumOption: EnumOption = undefined; @@ -1396,13 +1387,13 @@ export const computeChildLabel = ( getI18nKeyPrefix( childSchema, findUiControl(uiSchema, childLabelProp), - childPath + '.' + childLabelProp - ) + childPath + '.' + childLabelProp, + ), ); } else if (isOneOfEnumSchema(childSchema)) { const oneOfArray = childSchema.oneOf as JsonSchema[]; const oneOfSchema = oneOfArray.find((e: JsonSchema) => - isEqual(e.const, currentValue) + isEqual(e.const, currentValue), ); if (oneOfSchema) { @@ -1412,8 +1403,8 @@ export const computeChildLabel = ( getI18nKeyPrefix( oneOfSchema, undefined, - childPath + '.' + childLabelProp - ) + childPath + '.' + childLabelProp, + ), ); } } diff --git a/packages/core/test/mappers/renderer.test.ts b/packages/core/test/mappers/renderer.test.ts index 9d0dd516cc..9b4db00e3d 100644 --- a/packages/core/test/mappers/renderer.test.ts +++ b/packages/core/test/mappers/renderer.test.ts @@ -72,11 +72,11 @@ const middlewares: Redux.Middleware[] = []; const mockStore = configureStore(middlewares); const mockDispatch = ( - initialCore: JsonFormsCore + initialCore: JsonFormsCore, ): [() => JsonFormsCore, Redux.Dispatch] => { const coreContainer = { core: initialCore }; const dispatch: Redux.Dispatch = ( - action: T + action: T, ): T => { coreContainer.core = coreReducer(coreContainer.core, action); return action; @@ -420,11 +420,11 @@ test('mapStateToControlProps - no duplicate error messages', (t) => { const initCoreState = coreReducer(undefined, init({}, schema, coreUISchema)); const updateCoreState = coreReducer( initCoreState, - update('firstName', () => true) + update('firstName', () => true), ); const props = mapStateToControlProps( { jsonforms: { core: updateCoreState } }, - { uischema: coreUISchema } + { uischema: coreUISchema }, ); t.is(props.errors.split('\n').length, 1); }); @@ -454,7 +454,7 @@ test('mapStateToControlProps - hide errors in hide validation mode', (t) => { }; const initCoreState = coreReducer( undefined, - init({ animal: 100 }, schema, uischema) + init({ animal: 100 }, schema, uischema), ); t.is(initCoreState.errors.length, 1); @@ -463,19 +463,19 @@ test('mapStateToControlProps - hide errors in hide validation mode', (t) => { }; const props = mapStateToControlProps( { jsonforms: { core: initCoreState } }, - ownProps + ownProps, ); t.not(props.errors.length, 0); const hideErrorsState = coreReducer( initCoreState, - setValidationMode('ValidateAndHide') + setValidationMode('ValidateAndHide'), ); t.is(hideErrorsState.errors.length, 1); const hideErrorsProps = mapStateToControlProps( { jsonforms: { core: hideErrorsState } }, - ownProps + ownProps, ); t.is(hideErrorsProps.errors, ''); }); @@ -500,9 +500,9 @@ test('createDefaultValue', (t) => { { type: 'string', format: 'date', - } + }, ), - convertDateToString(new Date(), 'date') + convertDateToString(new Date(), 'date'), ); t.regex( createDefaultValue( @@ -513,9 +513,9 @@ test('createDefaultValue', (t) => { { type: 'string', format: 'date-time', - } + }, ), - /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/ + /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/, ); t.regex( createDefaultValue( @@ -526,9 +526,9 @@ test('createDefaultValue', (t) => { { type: 'string', format: 'time', - } + }, ), - /^\d{2}:\d{2}:\d{2}$/ + /^\d{2}:\d{2}:\d{2}$/, ); t.is(createDefaultValue({ type: 'string' }, { type: 'string' }), ''); t.is(createDefaultValue({ type: 'number' }, { type: 'number' }), 0); @@ -539,7 +539,7 @@ test('createDefaultValue', (t) => { t.deepEqual(createDefaultValue({ type: 'object' }, { type: 'object' }), {}); t.deepEqual( createDefaultValue({ type: 'something' }, { type: 'something' }), - {} + {}, ); // defaults: @@ -552,53 +552,53 @@ test('createDefaultValue', (t) => { { type: 'string', default: '2023-10-10', - } + }, ), - '2023-10-10' + '2023-10-10', ); t.is( createDefaultValue( { type: 'string', default: 'excellent' }, - { type: 'string', default: 'excellent' } + { type: 'string', default: 'excellent' }, ), - 'excellent' + 'excellent', ); t.is( createDefaultValue( { type: 'number', default: 10 }, - { type: 'number', default: 10 } + { type: 'number', default: 10 }, ), - 10 + 10, ); t.is( createDefaultValue( { type: 'boolean', default: true }, - { type: 'boolean', default: true } + { type: 'boolean', default: true }, ), - true + true, ); t.is( createDefaultValue( { type: 'integer', default: 11 }, - { type: 'integer', default: 11 } + { type: 'integer', default: 11 }, ), - 11 + 11, ); t.deepEqual( createDefaultValue( { type: 'array', default: ['a', 'b', 'c'] }, - { type: 'array', default: ['a', 'b', 'c'] } + { type: 'array', default: ['a', 'b', 'c'] }, ), - ['a', 'b', 'c'] + ['a', 'b', 'c'], ); t.deepEqual( createDefaultValue( { type: 'object', default: { foo: 'bar' } }, - { type: 'object', default: { foo: 'bar' } } + { type: 'object', default: { foo: 'bar' } }, ), { foo: 'bar', - } + }, ); const objectSchema = { type: 'object', @@ -733,7 +733,7 @@ test('createDefaultValue', (t) => { }; const defaultValueOneOfEmpty = createDefaultValue( schemaOneOfEmpty, - rootSchemaOneOfEmpty + rootSchemaOneOfEmpty, ); t.deepEqual(defaultValueOneOfEmpty, ''); @@ -752,7 +752,7 @@ test('createDefaultValue', (t) => { }; const defaultValueAnyOfEmpty = createDefaultValue( schemaAnyOfEmpty, - rootSchemaAnyOfEmpty + rootSchemaAnyOfEmpty, ); t.deepEqual(defaultValueAnyOfEmpty, ''); @@ -779,7 +779,7 @@ test('createDefaultValue', (t) => { }; const defaultValueAllOfEmpty = createDefaultValue( schemaAllOfEmpty, - rootSchemaAllOfEmpty + rootSchemaAllOfEmpty, ); console.log('defaultValueAllOfEmpty', defaultValueAllOfEmpty); t.deepEqual(defaultValueAllOfEmpty, {}); @@ -1551,6 +1551,43 @@ test('mapDispatchToMultiEnumProps - oneOf schema - removeItem', (t) => { t.is(getCore().data.colors.length, 0); }); +test('mapDispatchToMultiEnumProps - handleChange', (t) => { + const uischema: ControlElement = { + type: 'Control', + scope: '#/properties/colors', + }; + const schema = { + type: 'object', + properties: { + colors: { + type: 'array', + items: { + type: 'string', + enum: ['red', 'green', 'pink'], + }, + uniqueItems: true, + }, + }, + }; + const data = { colors: ['green'] }; + const initCore: JsonFormsCore = { + uischema, + schema, + data, + errors: [] as ErrorObject[], + }; + const [getCore, dispatch] = mockDispatch(initCore); + dispatch(init(data, schema, uischema, createAjv({ useDefaults: true }))); + const props = mapDispatchToMultiEnumProps(dispatch); + + if (props.handleChange) { + props.handleChange('colors', ['green', 'red']); + } + + t.is(getCore().data.colors.length, 2); + t.deepEqual(getCore().data.colors, ['green', 'red']); +}); + test('should assign defaults to enum', (t) => { const schema: JsonSchema = { type: 'object', @@ -1585,7 +1622,7 @@ test('should assign defaults to enum', (t) => { }; const newCore = coreReducer( initState.jsonforms.core, - init(data, schema, uischema, createAjv({ useDefaults: true })) + init(data, schema, uischema, createAjv({ useDefaults: true })), ); t.is(newCore.data.color, 'green'); }); @@ -1624,7 +1661,7 @@ test('should assign defaults to empty item within nested object of an array', (t const newCore = coreReducer( initState.jsonforms.core, - init(data, schema, uischema, createAjv({ useDefaults: true })) + init(data, schema, uischema, createAjv({ useDefaults: true })), ); t.is(newCore.data.length, 1); t.deepEqual(newCore.data[0], { message: 'foo' }); @@ -1772,7 +1809,7 @@ test('mapStateToControlProps - i18n - translation via path key', (t) => { state.jsonforms.i18n = defaultJsonFormsI18nState; state.jsonforms.i18n.translate = ( key: string, - defaultMessage: string | undefined + defaultMessage: string | undefined, ) => { switch (key) { case 'firstName.label': @@ -1795,7 +1832,7 @@ test('mapStateToControlProps - i18n - translation via default message', (t) => { state.jsonforms.i18n = defaultJsonFormsI18nState; state.jsonforms.i18n.translate = ( _key: string, - defaultMessage: string | undefined + defaultMessage: string | undefined, ) => { switch (defaultMessage) { case 'First Name': @@ -1820,7 +1857,7 @@ test('mapStateToControlProps - i18n - translation via JSON Schema i18n key', (t) 'my-key'; state.jsonforms.i18n.translate = ( key: string, - defaultMessage: string | undefined + defaultMessage: string | undefined, ) => { switch (key) { case 'my-key.label': @@ -1846,7 +1883,7 @@ test('mapStateToControlProps - i18n - translation via UI Schema i18n key', (t) = ownProps.uischema = { ...ownProps.uischema, i18n: 'my-key' }; state.jsonforms.i18n.translate = ( key: string, - defaultMessage: string | undefined + defaultMessage: string | undefined, ) => { switch (key) { case 'my-key.label': @@ -1875,8 +1912,8 @@ test('mapStateToControlProps - i18n errors - should not crash without i18n', (t) state.jsonforms.core.data, state.jsonforms.core.schema, state.jsonforms.core.uischema, - createAjv() - ) + createAjv(), + ), ); state.jsonforms.i18n = undefined; @@ -1896,8 +1933,8 @@ test('mapStateToControlProps - i18n errors - default translation has no effect', state.jsonforms.core.data, state.jsonforms.core.schema, state.jsonforms.core.uischema, - createAjv() - ) + createAjv(), + ), ); state.jsonforms.i18n = defaultJsonFormsI18nState; @@ -1917,13 +1954,13 @@ test('mapStateToControlProps - i18n errors - translate via error message key', ( state.jsonforms.core.data, state.jsonforms.core.schema, state.jsonforms.core.uischema, - createAjv() - ) + createAjv(), + ), ); state.jsonforms.i18n = defaultJsonFormsI18nState; state.jsonforms.i18n.translate = ( key: string, - defaultMessage: string | undefined + defaultMessage: string | undefined, ) => { switch (key) { case 'must match pattern "[0-9]+"': @@ -1951,13 +1988,13 @@ test('mapStateToControlProps - i18n errors - translate via i18 specialized error state.jsonforms.core.data, state.jsonforms.core.schema, state.jsonforms.core.uischema, - createAjv() - ) + createAjv(), + ), ); state.jsonforms.i18n = defaultJsonFormsI18nState; state.jsonforms.i18n.translate = ( key: string, - defaultMessage: string | undefined + defaultMessage: string | undefined, ) => { switch (key) { case 'my-key.error.pattern': @@ -1985,13 +2022,13 @@ test('mapStateToControlProps - i18n errors - translate via i18 general error key state.jsonforms.core.data, state.jsonforms.core.schema, state.jsonforms.core.uischema, - createAjv() - ) + createAjv(), + ), ); state.jsonforms.i18n = defaultJsonFormsI18nState; state.jsonforms.i18n.translate = ( key: string, - defaultMessage: string | undefined + defaultMessage: string | undefined, ) => { switch (key) { case 'error.pattern': @@ -2019,13 +2056,13 @@ test('mapStateToControlProps - i18n errors - specialized keyword wins over gener state.jsonforms.core.data, state.jsonforms.core.schema, state.jsonforms.core.uischema, - createAjv() - ) + createAjv(), + ), ); state.jsonforms.i18n = defaultJsonFormsI18nState; state.jsonforms.i18n.translate = ( key: string, - defaultMessage: string | undefined + defaultMessage: string | undefined, ) => { switch (key) { case 'my-key.error.pattern': @@ -2056,13 +2093,13 @@ test('mapStateToControlProps - i18n errors - multiple errors customization', (t) state.jsonforms.core.data, state.jsonforms.core.schema, state.jsonforms.core.uischema, - createAjv() - ) + createAjv(), + ), ); state.jsonforms.i18n = defaultJsonFormsI18nState; state.jsonforms.i18n.translate = ( key: string, - defaultMessage: string | undefined + defaultMessage: string | undefined, ) => { switch (key) { case 'error.maxLength': @@ -2093,13 +2130,13 @@ test('mapStateToControlProps - i18n errors - custom keyword wins over all other state.jsonforms.core.data, state.jsonforms.core.schema, state.jsonforms.core.uischema, - createAjv() - ) + createAjv(), + ), ); state.jsonforms.i18n = defaultJsonFormsI18nState; state.jsonforms.i18n.translate = ( key: string, - defaultMessage: string | undefined + defaultMessage: string | undefined, ) => { switch (key) { case 'my-key.error.custom': @@ -2182,7 +2219,7 @@ test('mapStateToEnumControlProps - i18n - path label translation', (t) => { state.jsonforms.i18n = defaultJsonFormsI18nState; state.jsonforms.i18n.translate = ( key: string, - defaultMessage: string | undefined + defaultMessage: string | undefined, ) => { switch (key) { case 'firstName.a': @@ -2205,7 +2242,7 @@ test('mapStateToEnumControlProps - i18n - defaultMessage translation', (t) => { state.jsonforms.i18n = defaultJsonFormsI18nState; state.jsonforms.i18n.translate = ( _key: string, - defaultMessage: string | undefined + defaultMessage: string | undefined, ) => { switch (defaultMessage) { case 'a': @@ -2230,7 +2267,7 @@ test('mapStateToEnumControlProps - i18n - i18n key translation', (t) => { state.jsonforms.i18n = defaultJsonFormsI18nState; state.jsonforms.i18n.translate = ( key: string, - defaultMessage: string | undefined + defaultMessage: string | undefined, ) => { switch (key) { case 'my-key.a': @@ -2286,7 +2323,7 @@ test('mapStateToOneOfEnumControlProps - i18n - path label translation', (t) => { state.jsonforms.i18n = defaultJsonFormsI18nState; state.jsonforms.i18n.translate = ( key: string, - defaultMessage: string | undefined + defaultMessage: string | undefined, ) => { switch (key) { case 'firstName.foo': @@ -2312,7 +2349,7 @@ test('mapStateToOneOfEnumControlProps - i18n - default message translation', (t) state.jsonforms.i18n = defaultJsonFormsI18nState; state.jsonforms.i18n.translate = ( _key: string, - defaultMessage: string | undefined + defaultMessage: string | undefined, ) => { switch (defaultMessage) { case 'foo': @@ -2338,7 +2375,7 @@ test('mapStateToOneOfEnumControlProps - i18n - i18n key translation', (t) => { state.jsonforms.i18n = defaultJsonFormsI18nState; state.jsonforms.i18n.translate = ( key: string, - defaultMessage: string | undefined + defaultMessage: string | undefined, ) => { switch (key) { case 'my-foo': @@ -2396,7 +2433,7 @@ test('mapStateToLabelProps - i18n - default key translation', (t) => { state.jsonforms.i18n = defaultJsonFormsI18nState; state.jsonforms.i18n.translate = ( key: string, - defaultMessage: string | undefined + defaultMessage: string | undefined, ) => { switch (key) { case 'foo': @@ -2423,7 +2460,7 @@ test('mapStateToLabelProps - i18n - default message translation', (t) => { state.jsonforms.i18n = defaultJsonFormsI18nState; state.jsonforms.i18n.translate = ( _key: string, - defaultMessage: string | undefined + defaultMessage: string | undefined, ) => { switch (defaultMessage) { case 'foo': @@ -2450,7 +2487,7 @@ test('mapStateToLabelProps - i18n - i18n key translation', (t) => { state.jsonforms.i18n = defaultJsonFormsI18nState; state.jsonforms.i18n.translate = ( key: string, - defaultMessage: string | undefined + defaultMessage: string | undefined, ): string | undefined => { switch (key) { case 'bar.text': From 2fefc81f9a195b41c6d6df89a046de4b4bb3fadc Mon Sep 17 00:00:00 2001 From: darius-lesch <134329447+darius-lesch@users.noreply.github.com> Date: Thu, 8 Oct 2026 14:16:13 +0000 Subject: [PATCH 13/17] test(vue-vuetify): cover partial deselect for multiselect component Updated the "emits change when an item is deselected" test for the MultiEnumSelectRenderer to simulate removing a single item from a populated array (emitting `['a']` instead of `[]`) rather than a full clear. Added a new test specifically targeting the full clear scenario via empty array (`[]`), where `undefined` is appropriately expected as the emitted control data. Also, ran tests with `--update` to refresh snapshot ids that changed due to the added test block, and ran prettier. --- .../complex/MultiEnumSelectRenderer.spec.ts | 8 ++++++++ .../MultiEnumSelectRenderer.spec.ts.snap | 20 +++++++++---------- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/packages/vue-vuetify/tests/unit/complex/MultiEnumSelectRenderer.spec.ts b/packages/vue-vuetify/tests/unit/complex/MultiEnumSelectRenderer.spec.ts index fab56042f0..92580053c0 100644 --- a/packages/vue-vuetify/tests/unit/complex/MultiEnumSelectRenderer.spec.ts +++ b/packages/vue-vuetify/tests/unit/complex/MultiEnumSelectRenderer.spec.ts @@ -53,6 +53,14 @@ describe('MultiEnumSelectRenderer.vue', () => { }); it('emits change when an item is deselected', async () => { + const select = wrapper.findComponent({ name: 'VSelect' }); + await select.vm.$emit('update:modelValue', ['a']); + + const component = wrapper.getComponent(MultiEnumSelectRenderer); + expect(component.vm.control.data).toEqual(['a']); + }); + + it('emits change when cleared via empty array', async () => { const select = wrapper.findComponent({ name: 'VSelect' }); await select.vm.$emit('update:modelValue', []); diff --git a/packages/vue-vuetify/tests/unit/complex/__snapshots__/MultiEnumSelectRenderer.spec.ts.snap b/packages/vue-vuetify/tests/unit/complex/__snapshots__/MultiEnumSelectRenderer.spec.ts.snap index 97a66f163d..5a1bdb17b6 100644 --- a/packages/vue-vuetify/tests/unit/complex/__snapshots__/MultiEnumSelectRenderer.spec.ts.snap +++ b/packages/vue-vuetify/tests/unit/complex/__snapshots__/MultiEnumSelectRenderer.spec.ts.snap @@ -3,7 +3,7 @@ exports[`MultiEnumSelectRenderer.vue > with enum schema > should render component and match snapshot 1`] = ` "
-
+
@@ -24,9 +24,9 @@ exports[`MultiEnumSelectRenderer.vue > with enum schema > should render componen
-
@@ -60,7 +60,7 @@ exports[`MultiEnumSelectRenderer.vue > with enum schema > should render componen
-