Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
9099496
feat(vue-vuetify): add MultiEnumSelectRenderer for multiselect rendering
darius-lesch Oct 2, 2026
b305067
Merge pull request #4 from darius-lesch/feature/multienum-select-rend…
darius-lesch Oct 2, 2026
2f82463
feat: add multi-select example to the dev playground
darius-lesch Oct 2, 2026
42b1b16
Resolved linting and harmonized imports
darius-lesch Oct 2, 2026
7cecfd9
Merge pull request #5 from darius-lesch/feat-add-multi-select-example…
darius-lesch Oct 2, 2026
b36f863
Merge branch 'eclipsesource:master' into feat/multi-enum-select-renderer
darius-lesch Oct 2, 2026
f457e12
fix: resolved namespace error
darius-lesch Oct 6, 2026
7ef53a0
feat(core, vue-vuetify): support atomic array updates for multi-enum …
darius-lesch Oct 7, 2026
c9ae9d2
refactor(vue-vuetify): extract shared predicate for multi-enum testers
darius-lesch Oct 7, 2026
5ff28df
Merge pull request #11 from darius-lesch/refactor-extract-multi-enum-…
darius-lesch Oct 7, 2026
0c76e8a
chore(examples): consolidate multi-select into enum-multi and clean s…
darius-lesch Oct 7, 2026
5727bb4
Merge pull request #12 from darius-lesch/jules-113073129629200865-d6d…
darius-lesch Oct 7, 2026
e7a0171
fix: prettier formatting fix applied
darius-lesch Oct 7, 2026
515e556
Merge pull request #10 from darius-lesch/feat-atomic-array-updates-mu…
darius-lesch Oct 7, 2026
ce95497
fix: removed leftover blank line
darius-lesch Oct 8, 2026
3631b4f
refactor(vue-vuetify): utilize `adaptValue` in `MultiEnumSelectRenderer`
darius-lesch Oct 8, 2026
1bf9f57
refactor(vue-vuetify): standardize `isMultiEnumControl` API
darius-lesch Oct 8, 2026
3b13513
chore(core): DRY up mapDispatchToMultiEnumProps handleChange implemen…
darius-lesch Oct 8, 2026
06c2b40
Merge pull request #14 from darius-lesch/standardize-is-multi-enum-co…
darius-lesch Oct 8, 2026
77042c4
Merge pull request #13 from darius-lesch/refactor/vue-vuetify-multi-e…
darius-lesch Oct 8, 2026
2fefc81
test(vue-vuetify): cover partial deselect for multiselect component
darius-lesch Oct 8, 2026
84bd290
Merge pull request #15 from darius-lesch/fix-core-mapper-dry-up-94112…
darius-lesch Oct 8, 2026
0b19ce7
Merge pull request #16 from darius-lesch/fix/vuetify-multi-select-des…
darius-lesch Oct 8, 2026
e3e1d5c
test(vue-vuetify): add interaction tests for oneOf schema in MultiEnu…
darius-lesch Oct 8, 2026
26bf3ff
Merge pull request #18 from darius-lesch/test/vue-vuetify-multi-enum-…
darius-lesch Oct 8, 2026
544047d
Merge pull request #17 from darius-lesch/fix/pr-2637-comments-v2
darius-lesch Oct 8, 2026
7b26505
fix(vue-vuetify): update isMultiEnumControl tester to strictly requir…
darius-lesch Oct 8, 2026
b3f9f6d
fix: prettier fixes
darius-lesch Oct 8, 2026
410cd79
Merge pull request #19 from darius-lesch/fix-multi-enum-tester-const-…
darius-lesch Oct 8, 2026
5c9466b
remove unnecessary any cast
lucas-koehler Oct 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/core/src/mappers/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -992,11 +992,13 @@ 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 = (
dispatch: Dispatch<CoreActions>
): DispatchPropsOfMultiEnumControl => ({
...mapDispatchToControlProps(dispatch),
addItem: (path: string, value: any) => {
dispatch(
update(path, (data) => {
Expand Down
37 changes: 37 additions & 0 deletions packages/core/test/mappers/renderer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
22 changes: 21 additions & 1 deletion packages/examples/src/examples/enum-multi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
},
},
},
};

Expand All @@ -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([
{
Expand Down
40 changes: 3 additions & 37 deletions packages/vue-vuetify/src/complex/EnumArrayRenderer.entry.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,8 @@
import {
and,
hasType,
rankWith,
schemaMatches,
schemaSubPathMatches,
uiTypeIs,
type JsonFormsRendererRegistryEntry,
type JsonSchema,
} 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'),
and(
schemaMatches(
(schema) =>
hasType(schema, 'array') &&
!Array.isArray(schema.items) &&
schema.uniqueItems === true,
),
schemaSubPathMatches('items', (schema) => {
return hasOneOfItems(schema) || hasEnumItems(schema);
}),
),
),
),
tester: rankWith(5, 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;
16 changes: 16 additions & 0 deletions packages/vue-vuetify/src/complex/MultiEnumSelectRenderer.entry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import {
and,
optionIs,
rankWith,
type JsonFormsRendererRegistryEntry,
} from '@jsonforms/core';
import controlRenderer from './MultiEnumSelectRenderer.vue';
import { isMultiEnumControl } from '../util/tester';

export const entry: JsonFormsRendererRegistryEntry = {
renderer: controlRenderer,
tester: rankWith(
10,
and(optionIs('format', 'multiselect'), isMultiEnumControl),
),
};
78 changes: 78 additions & 0 deletions packages/vue-vuetify/src/complex/MultiEnumSelectRenderer.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<template>
<control-wrapper
v-bind="controlWrapper"
:styles="styles"
:isFocused="isFocused"
:appliedOptions="appliedOptions"
>
<v-select
v-disabled-icon-focus
:id="control.id + '-input'"
:class="styles.control.input"
:disabled="!control.enabled"
:readonly="control.readonly"
:autofocus="appliedOptions.focus"
:placeholder="appliedOptions.placeholder"
:label="computedLabel"
:hint="control.description"
:persistent-hint="persistentHint()"
:required="control.required"
:error-messages="control.errors"
:clearable="clearable"
:model-value="control.data"
:items="control.options"
item-title="label"
item-value="value"
multiple
chips
closable-chips
v-bind="vuetifyProps('v-select')"
@update:model-value="onChange"
@focus="handleFocus"
@blur="handleBlur"
>
<template v-slot:prepend v-if="$slots.prepend">
<slot name="prepend" />
</template>
<template v-slot:append v-if="$slots.append">
<slot name="append" />
</template>
</v-select>
</control-wrapper>
</template>

<script lang="ts">
import { type ControlElement } from '@jsonforms/core';
import {
rendererProps,
useJsonFormsMultiEnumControl,
type RendererProps,
} from '@jsonforms/vue';
import { defineComponent } from 'vue';
import { VSelect } from 'vuetify/components';
import { determineClearValue, useVuetifyControl } from '../util';
import { default as ControlWrapper } from '../controls/ControlWrapper.vue';
import { DisabledIconFocus } from '../controls/directives';

const controlRenderer = defineComponent({
name: 'multi-enum-select-renderer',
components: {
ControlWrapper,
VSelect,
},
directives: {
DisabledIconFocus,
},
props: {
...rendererProps<ControlElement>(),
},
setup(props: RendererProps<ControlElement>) {
const clearValue = determineClearValue([]);
return useVuetifyControl(useJsonFormsMultiEnumControl(props), (value) =>
value === null || value.length === 0 ? clearValue : value,
);
},
});

export default controlRenderer;
</script>
4 changes: 4 additions & 0 deletions packages/vue-vuetify/src/complex/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand All @@ -21,6 +23,7 @@ export const complexRenderers = [
anyOfRendererEntry,
arrayControlRendererEntry,
enumArrayRendererEntry,
multiEnumSelectRendererEntry,
objectRendererEntry,
oneOfRendererEntry,
oneOfTabRendererEntry,
Expand All @@ -32,6 +35,7 @@ export {
anyOfRendererEntry,
arrayControlRendererEntry,
enumArrayRendererEntry,
multiEnumSelectRendererEntry,
objectRendererEntry,
oneOfRendererEntry,
oneOfTabRendererEntry,
Expand Down
1 change: 1 addition & 0 deletions packages/vue-vuetify/src/util/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export * from './datejs';
export * from './inject';
export * from './options';
export * from './validator';
export * from './tester';
27 changes: 27 additions & 0 deletions packages/vue-vuetify/src/util/tester.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import {
and,
hasType,
isOneOfEnumSchema,
schemaMatches,
schemaSubPathMatches,
uiTypeIs,
type JsonSchema,
} from '@jsonforms/core';

export const isMultiEnumControl = and(
Comment thread
lucas-koehler marked this conversation as resolved.
uiTypeIs('Control'),
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' && schema.enum !== undefined)
);
}),
);
Loading