@narrative.io/jsonforms-provider-protocols 2.9.0 → 3.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +166 -30
- package/dist/core/projection.d.ts +32 -0
- package/dist/core/projection.d.ts.map +1 -0
- package/dist/core/projection.js +74 -0
- package/dist/core/projection.js.map +1 -0
- package/dist/core/resolveScope.d.ts +11 -0
- package/dist/core/resolveScope.d.ts.map +1 -0
- package/dist/core/resolveScope.js +22 -0
- package/dist/core/resolveScope.js.map +1 -0
- package/dist/core/transforms.d.ts +8 -10
- package/dist/core/transforms.d.ts.map +1 -1
- package/dist/core/transforms.js +56 -13
- package/dist/core/transforms.js.map +1 -1
- package/dist/core/types.d.ts +7 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -2
- package/dist/index.js.map +1 -1
- package/dist/jsonforms-provider-protocols.css +2 -2
- package/dist/vue/components/ProviderAutocomplete.vue.d.ts.map +1 -1
- package/dist/vue/components/ProviderAutocomplete.vue.js +4 -2
- package/dist/vue/components/ProviderAutocomplete.vue.js.map +1 -1
- package/dist/vue/components/ProviderMultiSelect.vue.d.ts.map +1 -1
- package/dist/vue/components/ProviderMultiSelect.vue.js +1 -1
- package/dist/vue/components/ProviderMultiSelect.vue2.js +22 -4
- package/dist/vue/components/ProviderMultiSelect.vue2.js.map +1 -1
- package/dist/vue/components/ProviderSelect.vue.d.ts.map +1 -1
- package/dist/vue/components/ProviderSelect.vue.js +1 -1
- package/dist/vue/components/ProviderSelect.vue2.js +5 -3
- package/dist/vue/components/ProviderSelect.vue2.js.map +1 -1
- package/dist/vue/composables/useDataLayer.d.ts +9 -0
- package/dist/vue/composables/useDataLayer.d.ts.map +1 -0
- package/dist/vue/composables/useDataLayer.js +25 -0
- package/dist/vue/composables/useDataLayer.js.map +1 -0
- package/dist/vue/composables/useDerive.d.ts +5 -2
- package/dist/vue/composables/useDerive.d.ts.map +1 -1
- package/dist/vue/composables/useDerive.js +12 -12
- package/dist/vue/composables/useDerive.js.map +1 -1
- package/dist/vue/composables/useProjection.d.ts +35 -0
- package/dist/vue/composables/useProjection.d.ts.map +1 -0
- package/dist/vue/composables/useProjection.js +33 -0
- package/dist/vue/composables/useProjection.js.map +1 -0
- package/dist/vue/index.d.ts +4 -0
- package/dist/vue/index.d.ts.map +1 -1
- package/dist/vue/index.js +15 -29
- package/dist/vue/index.js.map +1 -1
- package/dist/vue/primevue/JfBoolean.vue.d.ts +9 -0
- package/dist/vue/primevue/JfBoolean.vue.d.ts.map +1 -1
- package/dist/vue/primevue/JfBoolean.vue.js +10 -5
- package/dist/vue/primevue/JfBoolean.vue.js.map +1 -1
- package/dist/vue/primevue/JfEnum.vue.d.ts +9 -0
- package/dist/vue/primevue/JfEnum.vue.d.ts.map +1 -1
- package/dist/vue/primevue/JfEnum.vue.js +12 -7
- package/dist/vue/primevue/JfEnum.vue.js.map +1 -1
- package/dist/vue/primevue/JfEnumArray.vue.d.ts +9 -0
- package/dist/vue/primevue/JfEnumArray.vue.d.ts.map +1 -1
- package/dist/vue/primevue/JfEnumArray.vue.js +29 -8
- package/dist/vue/primevue/JfEnumArray.vue.js.map +1 -1
- package/dist/vue/primevue/JfNumber.vue.d.ts +9 -0
- package/dist/vue/primevue/JfNumber.vue.d.ts.map +1 -1
- package/dist/vue/primevue/JfNumber.vue.js +11 -6
- package/dist/vue/primevue/JfNumber.vue.js.map +1 -1
- package/dist/vue/primevue/JfText.vue.d.ts +9 -0
- package/dist/vue/primevue/JfText.vue.d.ts.map +1 -1
- package/dist/vue/primevue/JfText.vue.js +13 -8
- package/dist/vue/primevue/JfText.vue.js.map +1 -1
- package/dist/vue/primevue/JfTextArea.vue.d.ts +9 -0
- package/dist/vue/primevue/JfTextArea.vue.d.ts.map +1 -1
- package/dist/vue/primevue/JfTextArea.vue.js +11 -6
- package/dist/vue/primevue/JfTextArea.vue.js.map +1 -1
- package/dist/vue/primevue/index.d.ts.map +1 -1
- package/dist/vue/primevue/index.js +22 -7
- package/dist/vue/primevue/index.js.map +1 -1
- package/dist/vue/utils/autoSelect.d.ts +19 -2
- package/dist/vue/utils/autoSelect.d.ts.map +1 -1
- package/dist/vue/utils/autoSelect.js +21 -1
- package/dist/vue/utils/autoSelect.js.map +1 -1
- package/package.json +7 -3
- package/src/core/projection.ts +136 -0
- package/src/core/resolveScope.ts +39 -0
- package/src/core/transforms.ts +91 -26
- package/src/core/types.ts +8 -0
- package/src/index.ts +7 -0
- package/src/vue/components/ProviderAutocomplete.vue +4 -2
- package/src/vue/components/ProviderMultiSelect.vue +26 -4
- package/src/vue/components/ProviderSelect.vue +5 -3
- package/src/vue/composables/useDataLayer.ts +43 -0
- package/src/vue/composables/useDerive.ts +19 -16
- package/src/vue/composables/useProjection.ts +74 -0
- package/src/vue/index.ts +20 -46
- package/src/vue/primevue/JfBoolean.vue +7 -2
- package/src/vue/primevue/JfEnum.vue +9 -4
- package/src/vue/primevue/JfEnumArray.vue +30 -5
- package/src/vue/primevue/JfNumber.vue +8 -3
- package/src/vue/primevue/JfText.vue +10 -5
- package/src/vue/primevue/JfTextArea.vue +8 -3
- package/src/vue/primevue/index.ts +32 -7
- package/src/vue/utils/autoSelect.ts +46 -2
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { computed, type ComputedRef, type Ref } from "vue";
|
|
2
|
+
import {
|
|
3
|
+
getProjectedValue,
|
|
4
|
+
setProjectedValue,
|
|
5
|
+
getProjectedSchema,
|
|
6
|
+
} from "../../core/projection";
|
|
7
|
+
|
|
8
|
+
interface ProjectionControl {
|
|
9
|
+
data: unknown;
|
|
10
|
+
path: string;
|
|
11
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
12
|
+
schema: Record<string, any>;
|
|
13
|
+
uischema: { options?: { projection?: string; [key: string]: unknown } };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ProjectionResult {
|
|
17
|
+
/** The value at the projected path (for rendering) */
|
|
18
|
+
projectedData: ComputedRef<unknown>;
|
|
19
|
+
/** The schema at the projected path (for renderer selection) */
|
|
20
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
21
|
+
projectedSchema: ComputedRef<Record<string, any>>;
|
|
22
|
+
/** Wrapped handleChange that writes through the projection */
|
|
23
|
+
handleProjectedChange: (path: string, value: unknown) => void;
|
|
24
|
+
/** Whether projection is active */
|
|
25
|
+
hasProjection: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Composable that wraps a JSON Forms control with projection support.
|
|
30
|
+
*
|
|
31
|
+
* When `options.projection` is set on the uischema, this composable:
|
|
32
|
+
* - Reads the projected sub-value from the control data
|
|
33
|
+
* - Resolves the projected sub-schema for renderer type resolution
|
|
34
|
+
* - Wraps handleChange to write back through the projection path (preserving siblings)
|
|
35
|
+
*
|
|
36
|
+
* When no projection is set, it passes through control data/schema/handleChange unchanged.
|
|
37
|
+
*/
|
|
38
|
+
export function useProjection(
|
|
39
|
+
control: Ref<ProjectionControl>,
|
|
40
|
+
handleChange: (path: string, value: unknown) => void,
|
|
41
|
+
): ProjectionResult {
|
|
42
|
+
const projection = control.value.uischema?.options?.projection as
|
|
43
|
+
| string
|
|
44
|
+
| undefined;
|
|
45
|
+
|
|
46
|
+
if (!projection) {
|
|
47
|
+
return {
|
|
48
|
+
projectedData: computed(() => control.value.data),
|
|
49
|
+
projectedSchema: computed(() => control.value.schema),
|
|
50
|
+
handleProjectedChange: handleChange,
|
|
51
|
+
hasProjection: false,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const projectedData = computed(() =>
|
|
56
|
+
getProjectedValue(control.value.data, projection),
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const projectedSchema = computed(() =>
|
|
60
|
+
getProjectedSchema(control.value.schema, projection),
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
const handleProjectedChange = (path: string, value: unknown) => {
|
|
64
|
+
const fullValue = setProjectedValue(control.value.data, projection, value);
|
|
65
|
+
handleChange(path, fullValue);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
projectedData,
|
|
70
|
+
projectedSchema,
|
|
71
|
+
handleProjectedChange,
|
|
72
|
+
hasProjection: true,
|
|
73
|
+
};
|
|
74
|
+
}
|
package/src/vue/index.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
rankWith,
|
|
8
8
|
isControl,
|
|
9
9
|
} from "@jsonforms/core";
|
|
10
|
+
import { resolveScopeSchema } from "../core/resolveScope";
|
|
10
11
|
import ProviderAutocomplete from "./components/ProviderAutocomplete.vue";
|
|
11
12
|
import ProviderSelect from "./components/ProviderSelect.vue";
|
|
12
13
|
import ProviderMultiSelect from "./components/ProviderMultiSelect.vue";
|
|
@@ -16,29 +17,21 @@ const hasProvider = (uischema: UISchemaElement) => {
|
|
|
16
17
|
return uischema?.options?.provider !== undefined;
|
|
17
18
|
};
|
|
18
19
|
|
|
20
|
+
// Integer fallback tester — handles nested scopes like #/properties/parent/properties/child
|
|
21
|
+
const isIntegerScope = (uischema: unknown, schema: unknown) => {
|
|
22
|
+
const ui = uischema as { type?: string; scope?: string };
|
|
23
|
+
if (ui?.type !== "Control" || !ui?.scope) return false;
|
|
24
|
+
|
|
25
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
26
|
+
const propertySchema = resolveScopeSchema(ui.scope, schema as Record<string, any>);
|
|
27
|
+
return propertySchema?.type === "integer";
|
|
28
|
+
};
|
|
29
|
+
|
|
19
30
|
// Create specific testers for each component type
|
|
20
31
|
const providerSelectTester = rankWith(
|
|
21
32
|
106, // Higher than PrimeVue base (100) to ensure providers take precedence
|
|
22
33
|
and(
|
|
23
|
-
or(
|
|
24
|
-
isStringControl,
|
|
25
|
-
isNumberControl,
|
|
26
|
-
and(isControl, (uischema: unknown, schema: unknown) => {
|
|
27
|
-
const ui = uischema as { type?: string; scope?: string };
|
|
28
|
-
const rootSchema = schema as {
|
|
29
|
-
properties?: Record<string, { type?: string }>;
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
if (ui?.type !== "Control" || !ui?.scope || !rootSchema?.properties)
|
|
33
|
-
return false;
|
|
34
|
-
|
|
35
|
-
// Extract property name from scope (e.g., "#/properties/age" -> "age")
|
|
36
|
-
const propertyName = ui.scope.replace("#/properties/", "");
|
|
37
|
-
const propertySchema = rootSchema.properties[propertyName];
|
|
38
|
-
|
|
39
|
-
return propertySchema?.type === "integer";
|
|
40
|
-
}),
|
|
41
|
-
),
|
|
34
|
+
or(isStringControl, isNumberControl, and(isControl, isIntegerScope)),
|
|
42
35
|
hasProvider,
|
|
43
36
|
(uischema) => !uischema?.options?.autocomplete,
|
|
44
37
|
),
|
|
@@ -47,44 +40,21 @@ const providerSelectTester = rankWith(
|
|
|
47
40
|
const providerAutocompleteTester = rankWith(
|
|
48
41
|
107, // Higher than PrimeVue base (100) to ensure providers take precedence
|
|
49
42
|
and(
|
|
50
|
-
or(
|
|
51
|
-
isStringControl,
|
|
52
|
-
isNumberControl,
|
|
53
|
-
and(isControl, (uischema: unknown, schema: unknown) => {
|
|
54
|
-
const ui = uischema as { type?: string; scope?: string };
|
|
55
|
-
const rootSchema = schema as {
|
|
56
|
-
properties?: Record<string, { type?: string }>;
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
if (ui?.type !== "Control" || !ui?.scope || !rootSchema?.properties)
|
|
60
|
-
return false;
|
|
61
|
-
|
|
62
|
-
// Extract property name from scope (e.g., "#/properties/age" -> "age")
|
|
63
|
-
const propertyName = ui.scope.replace("#/properties/", "");
|
|
64
|
-
const propertySchema = rootSchema.properties[propertyName];
|
|
65
|
-
|
|
66
|
-
return propertySchema?.type === "integer";
|
|
67
|
-
}),
|
|
68
|
-
),
|
|
43
|
+
or(isStringControl, isNumberControl, and(isControl, isIntegerScope)),
|
|
69
44
|
hasProvider,
|
|
70
45
|
(uischema) => uischema?.options?.autocomplete === true,
|
|
71
46
|
),
|
|
72
47
|
);
|
|
73
48
|
|
|
74
|
-
// Custom array tester -
|
|
49
|
+
// Custom array tester - supports nested scope paths
|
|
75
50
|
const isArrayControl = (uischema: UISchemaElement, schema: unknown) => {
|
|
76
51
|
const controlSchema = uischema as { type: string; scope?: string };
|
|
77
52
|
if (controlSchema.type !== "Control" || !controlSchema.scope) {
|
|
78
53
|
return false;
|
|
79
54
|
}
|
|
80
55
|
|
|
81
|
-
//
|
|
82
|
-
const
|
|
83
|
-
const propertyPath = controlSchema.scope.replace("#/properties/", "");
|
|
84
|
-
const propertySchema = rootSchema?.properties?.[propertyPath] as {
|
|
85
|
-
type?: string;
|
|
86
|
-
};
|
|
87
|
-
|
|
56
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
57
|
+
const propertySchema = resolveScopeSchema(controlSchema.scope, schema as Record<string, any>);
|
|
88
58
|
return propertySchema?.type === "array";
|
|
89
59
|
};
|
|
90
60
|
|
|
@@ -105,6 +75,10 @@ export { primevueRenderers, registerPrimevueRenderers } from "./primevue";
|
|
|
105
75
|
// Export individual components
|
|
106
76
|
export { ProviderAutocomplete, ProviderSelect, ProviderMultiSelect };
|
|
107
77
|
export { useProvider } from "./composables/useProvider";
|
|
78
|
+
export { useProjection } from "./composables/useProjection";
|
|
79
|
+
export type { ProjectionResult } from "./composables/useProjection";
|
|
80
|
+
export { createDataLayer, useDataLayer } from "./composables/useDataLayer";
|
|
81
|
+
export type { DataLayer } from "./composables/useDataLayer";
|
|
108
82
|
export * from "./testers";
|
|
109
83
|
|
|
110
84
|
// Export individual PrimeVue components using lazy evaluation to avoid circular deps
|
|
@@ -21,14 +21,17 @@ export default {
|
|
|
21
21
|
renderers: {
|
|
22
22
|
type: Array,
|
|
23
23
|
required: false,
|
|
24
|
+
default: undefined,
|
|
24
25
|
},
|
|
25
26
|
cells: {
|
|
26
27
|
type: Array,
|
|
27
28
|
required: false,
|
|
29
|
+
default: undefined,
|
|
28
30
|
},
|
|
29
31
|
config: {
|
|
30
32
|
type: Object,
|
|
31
33
|
required: false,
|
|
34
|
+
default: undefined,
|
|
32
35
|
},
|
|
33
36
|
},
|
|
34
37
|
};
|
|
@@ -38,12 +41,14 @@ export default {
|
|
|
38
41
|
import type { ControlProps } from "@jsonforms/vue";
|
|
39
42
|
import { useJsonFormsControl } from "@jsonforms/vue";
|
|
40
43
|
import { getCurrentInstance } from "vue";
|
|
44
|
+
import { useProjection } from "../composables/useProjection";
|
|
41
45
|
import Checkbox from "primevue/checkbox";
|
|
42
46
|
|
|
43
47
|
// Access props from the component instance
|
|
44
48
|
const instance = getCurrentInstance()!;
|
|
45
49
|
const props = instance.props as unknown as ControlProps;
|
|
46
|
-
const { control, handleChange } = useJsonFormsControl(props);
|
|
50
|
+
const { control, handleChange: rawHandleChange } = useJsonFormsControl(props);
|
|
51
|
+
const { projectedData, handleProjectedChange: handleChange } = useProjection(control, rawHandleChange);
|
|
47
52
|
|
|
48
53
|
const onToggle = (val: boolean) => handleChange(control.value.path, val);
|
|
49
54
|
</script>
|
|
@@ -52,7 +57,7 @@ const onToggle = (val: boolean) => handleChange(control.value.path, val);
|
|
|
52
57
|
<div class="flex items-center gap-2">
|
|
53
58
|
<Checkbox
|
|
54
59
|
:binary="true"
|
|
55
|
-
:model-value="!!
|
|
60
|
+
:model-value="!!projectedData"
|
|
56
61
|
:disabled="!control.enabled"
|
|
57
62
|
:aria-invalid="!!control.errors || undefined"
|
|
58
63
|
@update:model-value="onToggle"
|
|
@@ -22,14 +22,17 @@ export default {
|
|
|
22
22
|
renderers: {
|
|
23
23
|
type: Array,
|
|
24
24
|
required: false,
|
|
25
|
+
default: undefined,
|
|
25
26
|
},
|
|
26
27
|
cells: {
|
|
27
28
|
type: Array,
|
|
28
29
|
required: false,
|
|
30
|
+
default: undefined,
|
|
29
31
|
},
|
|
30
32
|
config: {
|
|
31
33
|
type: Object,
|
|
32
34
|
required: false,
|
|
35
|
+
default: undefined,
|
|
33
36
|
},
|
|
34
37
|
},
|
|
35
38
|
};
|
|
@@ -42,13 +45,15 @@ import { useJsonFormsControl } from "@jsonforms/vue";
|
|
|
42
45
|
import { computed, ref, inject, getCurrentInstance, watch } from "vue";
|
|
43
46
|
import { useProvider } from "../composables/useProvider";
|
|
44
47
|
import { useDerive } from "../composables/useDerive";
|
|
48
|
+
import { useProjection } from "../composables/useProjection";
|
|
45
49
|
import { shouldAutoSelect } from "../utils/autoSelect";
|
|
46
50
|
import Dropdown from "primevue/dropdown";
|
|
47
51
|
|
|
48
52
|
// Access props from the component instance
|
|
49
53
|
const instance = getCurrentInstance()!;
|
|
50
54
|
const props = instance.props as unknown as ControlProps;
|
|
51
|
-
const { control, handleChange } = useJsonFormsControl(props);
|
|
55
|
+
const { control, handleChange: rawHandleChange } = useJsonFormsControl(props);
|
|
56
|
+
const { projectedData, handleProjectedChange: handleChange } = useProjection(control, rawHandleChange);
|
|
52
57
|
|
|
53
58
|
type Opt = { label: string; value: unknown };
|
|
54
59
|
const toOptions = (schema?: JsonSchema): Opt[] => {
|
|
@@ -134,7 +139,7 @@ const options = computed(() => {
|
|
|
134
139
|
});
|
|
135
140
|
|
|
136
141
|
// Add derive functionality
|
|
137
|
-
useDerive({ control, handleChange });
|
|
142
|
+
useDerive({ control, handleChange, data: projectedData });
|
|
138
143
|
|
|
139
144
|
// Auto-select when provider returns only one item (enabled by default)
|
|
140
145
|
watch(
|
|
@@ -145,7 +150,7 @@ watch(
|
|
|
145
150
|
control.value.uischema?.options?.autoSelectSingle !== false,
|
|
146
151
|
isLoading,
|
|
147
152
|
items,
|
|
148
|
-
currentValue:
|
|
153
|
+
currentValue: projectedData.value,
|
|
149
154
|
});
|
|
150
155
|
|
|
151
156
|
if (valueToSelect !== null) {
|
|
@@ -182,7 +187,7 @@ const onBlur = () => {
|
|
|
182
187
|
:options="options"
|
|
183
188
|
option-label="label"
|
|
184
189
|
option-value="value"
|
|
185
|
-
:model-value="
|
|
190
|
+
:model-value="projectedData ?? null"
|
|
186
191
|
:placeholder="placeholder"
|
|
187
192
|
:disabled="!control.enabled || loading"
|
|
188
193
|
:aria-invalid="!!showErrors || undefined"
|
|
@@ -21,14 +21,17 @@ export default {
|
|
|
21
21
|
renderers: {
|
|
22
22
|
type: Array,
|
|
23
23
|
required: false,
|
|
24
|
+
default: undefined,
|
|
24
25
|
},
|
|
25
26
|
cells: {
|
|
26
27
|
type: Array,
|
|
27
28
|
required: false,
|
|
29
|
+
default: undefined,
|
|
28
30
|
},
|
|
29
31
|
config: {
|
|
30
32
|
type: Object,
|
|
31
33
|
required: false,
|
|
34
|
+
default: undefined,
|
|
32
35
|
},
|
|
33
36
|
},
|
|
34
37
|
};
|
|
@@ -38,15 +41,18 @@ export default {
|
|
|
38
41
|
import type { JsonSchema } from "@jsonforms/core";
|
|
39
42
|
import type { ControlProps } from "@jsonforms/vue";
|
|
40
43
|
import { useJsonFormsControl } from "@jsonforms/vue";
|
|
41
|
-
import { computed, inject, getCurrentInstance } from "vue";
|
|
44
|
+
import { computed, inject, getCurrentInstance, watch } from "vue";
|
|
42
45
|
import { useProvider } from "../composables/useProvider";
|
|
43
46
|
import { useDerive } from "../composables/useDerive";
|
|
47
|
+
import { useProjection } from "../composables/useProjection";
|
|
48
|
+
import { shouldAutoSelectMulti } from "../utils/autoSelect";
|
|
44
49
|
import MultiSelect from "primevue/multiselect";
|
|
45
50
|
|
|
46
51
|
// Access props from the component instance
|
|
47
52
|
const instance = getCurrentInstance()!;
|
|
48
53
|
const props = instance.props as unknown as ControlProps;
|
|
49
|
-
const { control, handleChange } = useJsonFormsControl(props);
|
|
54
|
+
const { control, handleChange: rawHandleChange } = useJsonFormsControl(props);
|
|
55
|
+
const { projectedData, handleProjectedChange: handleChange } = useProjection(control, rawHandleChange);
|
|
50
56
|
|
|
51
57
|
type Opt = { label: string; value: unknown };
|
|
52
58
|
const toOptions = (schema?: JsonSchema): Opt[] => {
|
|
@@ -124,7 +130,26 @@ const options = computed(() => {
|
|
|
124
130
|
});
|
|
125
131
|
|
|
126
132
|
// Add derive functionality
|
|
127
|
-
useDerive({ control, handleChange });
|
|
133
|
+
useDerive({ control, handleChange, data: projectedData });
|
|
134
|
+
|
|
135
|
+
// Auto-select when provider returns only one item (opt-in for multiselect)
|
|
136
|
+
watch(
|
|
137
|
+
[providerItems, loading],
|
|
138
|
+
([items, isLoading]) => {
|
|
139
|
+
const valueToSelect = shouldAutoSelectMulti({
|
|
140
|
+
autoSelectSingle:
|
|
141
|
+
control.value.uischema?.options?.autoSelectSingle === true,
|
|
142
|
+
isLoading,
|
|
143
|
+
items,
|
|
144
|
+
currentValue: Array.isArray(projectedData.value) ? projectedData.value : [],
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
if (valueToSelect !== null) {
|
|
148
|
+
handleChange(control.value.path, valueToSelect);
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
{ immediate: true }
|
|
152
|
+
);
|
|
128
153
|
|
|
129
154
|
const placeholder = computed<string | undefined>(() => {
|
|
130
155
|
if (loading.value) return "Loading…";
|
|
@@ -145,13 +170,13 @@ const sameSet = (a: unknown[], b: unknown[]) => {
|
|
|
145
170
|
// v-model with guard to avoid recursive updates
|
|
146
171
|
const model = computed<unknown[]>({
|
|
147
172
|
get() {
|
|
148
|
-
const curr = Array.isArray(
|
|
173
|
+
const curr = Array.isArray(projectedData.value) ? projectedData.value : [];
|
|
149
174
|
// return a fresh copy so PrimeMultiSelect can't mutate JSONForms' array in place
|
|
150
175
|
return [...curr];
|
|
151
176
|
},
|
|
152
177
|
set(val) {
|
|
153
178
|
const next = Array.isArray(val) ? [...val] : [];
|
|
154
|
-
const curr = Array.isArray(
|
|
179
|
+
const curr = Array.isArray(projectedData.value) ? projectedData.value : [];
|
|
155
180
|
if (!sameSet(curr, next)) handleChange(control.value.path, next);
|
|
156
181
|
},
|
|
157
182
|
});
|
|
@@ -21,14 +21,17 @@ export default {
|
|
|
21
21
|
renderers: {
|
|
22
22
|
type: Array,
|
|
23
23
|
required: false,
|
|
24
|
+
default: undefined,
|
|
24
25
|
},
|
|
25
26
|
cells: {
|
|
26
27
|
type: Array,
|
|
27
28
|
required: false,
|
|
29
|
+
default: undefined,
|
|
28
30
|
},
|
|
29
31
|
config: {
|
|
30
32
|
type: Object,
|
|
31
33
|
required: false,
|
|
34
|
+
default: undefined,
|
|
32
35
|
},
|
|
33
36
|
},
|
|
34
37
|
};
|
|
@@ -39,12 +42,14 @@ import type { ControlProps } from "@jsonforms/vue";
|
|
|
39
42
|
import { useJsonFormsControl } from "@jsonforms/vue";
|
|
40
43
|
import { computed, ref, getCurrentInstance } from "vue";
|
|
41
44
|
import { useDerive } from "../composables/useDerive";
|
|
45
|
+
import { useProjection } from "../composables/useProjection";
|
|
42
46
|
import InputNumber from "primevue/inputnumber";
|
|
43
47
|
|
|
44
48
|
// Access props from the component instance
|
|
45
49
|
const instance = getCurrentInstance()!;
|
|
46
50
|
const props = instance.props as unknown as ControlProps;
|
|
47
|
-
const { control, handleChange } = useJsonFormsControl(props);
|
|
51
|
+
const { control, handleChange: rawHandleChange } = useJsonFormsControl(props);
|
|
52
|
+
const { projectedData, handleProjectedChange: handleChange } = useProjection(control, rawHandleChange);
|
|
48
53
|
|
|
49
54
|
const options = computed(
|
|
50
55
|
() =>
|
|
@@ -57,7 +62,7 @@ const placeholder = computed<string | undefined>(
|
|
|
57
62
|
);
|
|
58
63
|
|
|
59
64
|
// Add derive functionality
|
|
60
|
-
useDerive({ control, handleChange });
|
|
65
|
+
useDerive({ control, handleChange, data: projectedData });
|
|
61
66
|
|
|
62
67
|
// Currency and decimal configuration
|
|
63
68
|
const mode = computed(() => {
|
|
@@ -125,7 +130,7 @@ const onBlur = () => {
|
|
|
125
130
|
:currency="currency"
|
|
126
131
|
:min-fraction-digits="minFractionDigits"
|
|
127
132
|
:max-fraction-digits="maxFractionDigits"
|
|
128
|
-
:model-value="typeof
|
|
133
|
+
:model-value="typeof projectedData === 'number' ? projectedData : null"
|
|
129
134
|
:placeholder="placeholder"
|
|
130
135
|
:disabled="!control.enabled"
|
|
131
136
|
:aria-invalid="!!showErrors || undefined"
|
|
@@ -21,14 +21,17 @@ export default {
|
|
|
21
21
|
renderers: {
|
|
22
22
|
type: Array,
|
|
23
23
|
required: false,
|
|
24
|
+
default: undefined,
|
|
24
25
|
},
|
|
25
26
|
cells: {
|
|
26
27
|
type: Array,
|
|
27
28
|
required: false,
|
|
29
|
+
default: undefined,
|
|
28
30
|
},
|
|
29
31
|
config: {
|
|
30
32
|
type: Object,
|
|
31
33
|
required: false,
|
|
34
|
+
default: undefined,
|
|
32
35
|
},
|
|
33
36
|
},
|
|
34
37
|
};
|
|
@@ -40,13 +43,15 @@ import { useJsonFormsControl } from "@jsonforms/vue";
|
|
|
40
43
|
import { computed, ref, inject, watch, getCurrentInstance } from "vue";
|
|
41
44
|
import { useProvider } from "../composables/useProvider";
|
|
42
45
|
import { useDerive } from "../composables/useDerive";
|
|
46
|
+
import { useProjection } from "../composables/useProjection";
|
|
43
47
|
import InputText from "primevue/inputtext";
|
|
44
48
|
import AutoComplete from "primevue/autocomplete";
|
|
45
49
|
|
|
46
50
|
// Access props from the component instance
|
|
47
51
|
const instance = getCurrentInstance()!;
|
|
48
52
|
const props = instance.props as unknown as ControlProps;
|
|
49
|
-
const { control, handleChange } = useJsonFormsControl(props);
|
|
53
|
+
const { control, handleChange: rawHandleChange } = useJsonFormsControl(props);
|
|
54
|
+
const { projectedData, handleProjectedChange: handleChange } = useProjection(control, rawHandleChange);
|
|
50
55
|
|
|
51
56
|
// Provider support for autocomplete functionality
|
|
52
57
|
const binding = computed(() => {
|
|
@@ -110,7 +115,7 @@ const placeholder = computed<string | undefined>(() => {
|
|
|
110
115
|
const isAutocomplete = computed(() => !!binding.value);
|
|
111
116
|
|
|
112
117
|
// Add derive functionality
|
|
113
|
-
useDerive({ control, handleChange });
|
|
118
|
+
useDerive({ control, handleChange, data: projectedData });
|
|
114
119
|
|
|
115
120
|
// Track user interaction
|
|
116
121
|
const hasInteracted = ref(false);
|
|
@@ -121,7 +126,7 @@ const showErrors = computed(() => hasInteracted.value && control.value.errors);
|
|
|
121
126
|
function onInput(val: string | undefined) {
|
|
122
127
|
// Convert empty strings to undefined for proper required field validation
|
|
123
128
|
const newValue = val && val.trim() !== "" ? val : undefined;
|
|
124
|
-
if (
|
|
129
|
+
if (projectedData.value !== newValue) {
|
|
125
130
|
handleChange(control.value.path, newValue);
|
|
126
131
|
}
|
|
127
132
|
}
|
|
@@ -158,7 +163,7 @@ const onSelect = (event: { value?: { value?: unknown } | unknown }) => {
|
|
|
158
163
|
<AutoComplete
|
|
159
164
|
v-if="isAutocomplete"
|
|
160
165
|
class="w-full"
|
|
161
|
-
:model-value="
|
|
166
|
+
:model-value="projectedData ?? ''"
|
|
162
167
|
:suggestions="items"
|
|
163
168
|
option-label="label"
|
|
164
169
|
:placeholder="placeholder"
|
|
@@ -173,7 +178,7 @@ const onSelect = (event: { value?: { value?: unknown } | unknown }) => {
|
|
|
173
178
|
<InputText
|
|
174
179
|
v-else
|
|
175
180
|
class="w-full"
|
|
176
|
-
:model-value="
|
|
181
|
+
:model-value="(projectedData as string) ?? ''"
|
|
177
182
|
:disabled="!control.enabled"
|
|
178
183
|
:aria-invalid="!!showErrors || undefined"
|
|
179
184
|
:placeholder="placeholder"
|
|
@@ -21,14 +21,17 @@ export default {
|
|
|
21
21
|
renderers: {
|
|
22
22
|
type: Array,
|
|
23
23
|
required: false,
|
|
24
|
+
default: undefined,
|
|
24
25
|
},
|
|
25
26
|
cells: {
|
|
26
27
|
type: Array,
|
|
27
28
|
required: false,
|
|
29
|
+
default: undefined,
|
|
28
30
|
},
|
|
29
31
|
config: {
|
|
30
32
|
type: Object,
|
|
31
33
|
required: false,
|
|
34
|
+
default: undefined,
|
|
32
35
|
},
|
|
33
36
|
},
|
|
34
37
|
};
|
|
@@ -38,12 +41,14 @@ export default {
|
|
|
38
41
|
import type { ControlProps } from "@jsonforms/vue";
|
|
39
42
|
import { useJsonFormsControl } from "@jsonforms/vue";
|
|
40
43
|
import { computed, ref, getCurrentInstance } from "vue";
|
|
44
|
+
import { useProjection } from "../composables/useProjection";
|
|
41
45
|
import Textarea from "primevue/textarea";
|
|
42
46
|
|
|
43
47
|
// Access props from the component instance
|
|
44
48
|
const instance = getCurrentInstance()!;
|
|
45
49
|
const props = instance.props as unknown as ControlProps;
|
|
46
|
-
const { control, handleChange } = useJsonFormsControl(props);
|
|
50
|
+
const { control, handleChange: rawHandleChange } = useJsonFormsControl(props);
|
|
51
|
+
const { projectedData, handleProjectedChange: handleChange } = useProjection(control, rawHandleChange);
|
|
47
52
|
|
|
48
53
|
const placeholder = computed<string | undefined>(
|
|
49
54
|
() =>
|
|
@@ -59,7 +64,7 @@ const showErrors = computed(() => hasInteracted.value && control.value.errors);
|
|
|
59
64
|
function onInput(val: string | undefined) {
|
|
60
65
|
// Convert empty strings to undefined for proper required field validation
|
|
61
66
|
const newValue = val && val.trim() !== "" ? val : undefined;
|
|
62
|
-
if (
|
|
67
|
+
if (projectedData.value !== newValue) {
|
|
63
68
|
handleChange(control.value.path, newValue);
|
|
64
69
|
}
|
|
65
70
|
}
|
|
@@ -79,7 +84,7 @@ function onBlur() {
|
|
|
79
84
|
</div>
|
|
80
85
|
<Textarea
|
|
81
86
|
class="w-full"
|
|
82
|
-
:model-value="
|
|
87
|
+
:model-value="(projectedData as string) ?? ''"
|
|
83
88
|
:disabled="!control.enabled"
|
|
84
89
|
:aria-invalid="!!showErrors || undefined"
|
|
85
90
|
:placeholder="placeholder"
|
|
@@ -4,6 +4,8 @@ import JfNumber from "./JfNumber.vue";
|
|
|
4
4
|
import JfEnum from "./JfEnum.vue";
|
|
5
5
|
import JfEnumArray from "./JfEnumArray.vue";
|
|
6
6
|
import JfBoolean from "./JfBoolean.vue";
|
|
7
|
+
import { getProjectedSchema } from "../../core/projection";
|
|
8
|
+
import { resolveScopeSchema } from "../../core/resolveScope";
|
|
7
9
|
|
|
8
10
|
// Auto-inject layout styles
|
|
9
11
|
const injectLayoutStyles = () => {
|
|
@@ -54,6 +56,7 @@ export function registerPrimevueRenderers(jsonformsCore: any): unknown[] {
|
|
|
54
56
|
isNumberControl,
|
|
55
57
|
isIntegerControl,
|
|
56
58
|
and,
|
|
59
|
+
or,
|
|
57
60
|
isControl,
|
|
58
61
|
schemaMatches,
|
|
59
62
|
isBooleanControl,
|
|
@@ -97,27 +100,49 @@ export function registerPrimevueRenderers(jsonformsCore: any): unknown[] {
|
|
|
97
100
|
);
|
|
98
101
|
};
|
|
99
102
|
|
|
103
|
+
// Projection-aware schema check: when options.projection is set,
|
|
104
|
+
// resolve the projected schema and test against it instead of the original
|
|
105
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
106
|
+
const projectedSchemaMatches = (check: (schema: any) => boolean) =>
|
|
107
|
+
(uischema: unknown, schema: unknown): boolean => {
|
|
108
|
+
const ui = uischema as { type?: string; scope?: string; options?: { projection?: string } };
|
|
109
|
+
const projection = ui?.options?.projection;
|
|
110
|
+
if (!projection || ui?.type !== "Control" || !ui?.scope) return false;
|
|
111
|
+
|
|
112
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
113
|
+
const propertySchema = resolveScopeSchema(ui.scope, schema as Record<string, any>);
|
|
114
|
+
if (!propertySchema) return false;
|
|
115
|
+
|
|
116
|
+
return check(getProjectedSchema(propertySchema, projection));
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const isMultilineProjection = (uischema: unknown, schema: unknown) => {
|
|
120
|
+
const ui = uischema as { options?: { multi?: boolean } };
|
|
121
|
+
return ui?.options?.multi === true &&
|
|
122
|
+
projectedSchemaMatches((s) => s?.type === "string")(uischema, schema);
|
|
123
|
+
};
|
|
124
|
+
|
|
100
125
|
const renderers = [
|
|
101
126
|
// Multiline text has higher priority than regular text
|
|
102
|
-
{ tester: rankWith(PRIME + 4, isMultilineString), renderer: JfTextArea },
|
|
103
|
-
{ tester: rankWith(PRIME + 3, isStringControl), renderer: JfText },
|
|
127
|
+
{ tester: rankWith(PRIME + 4, or(isMultilineString, isMultilineProjection)), renderer: JfTextArea },
|
|
128
|
+
{ tester: rankWith(PRIME + 3, or(isStringControl, projectedSchemaMatches((s) => s?.type === "string"))), renderer: JfText },
|
|
104
129
|
{
|
|
105
|
-
tester: rankWith(PRIME + 6, isIntegerControl),
|
|
130
|
+
tester: rankWith(PRIME + 6, or(isIntegerControl, projectedSchemaMatches((s) => s?.type === "integer"))),
|
|
106
131
|
renderer: JfNumber,
|
|
107
132
|
},
|
|
108
133
|
{
|
|
109
|
-
tester: rankWith(PRIME + 4, isNumberControl),
|
|
134
|
+
tester: rankWith(PRIME + 4, or(isNumberControl, projectedSchemaMatches((s) => s?.type === "number"))),
|
|
110
135
|
renderer: JfNumber,
|
|
111
136
|
},
|
|
112
137
|
{
|
|
113
|
-
tester: rankWith(PRIME + 7, and(isControl, schemaMatches(isScalarEnum))),
|
|
138
|
+
tester: rankWith(PRIME + 7, or(and(isControl, schemaMatches(isScalarEnum)), and(isControl, projectedSchemaMatches(isScalarEnum)))),
|
|
114
139
|
renderer: JfEnum,
|
|
115
140
|
},
|
|
116
141
|
{
|
|
117
|
-
tester: rankWith(PRIME + 8, and(isControl, schemaMatches(isEnumArray))),
|
|
142
|
+
tester: rankWith(PRIME + 8, or(and(isControl, schemaMatches(isEnumArray)), and(isControl, projectedSchemaMatches(isEnumArray)))),
|
|
118
143
|
renderer: JfEnumArray,
|
|
119
144
|
},
|
|
120
|
-
{ tester: rankWith(PRIME + 3, isBooleanControl), renderer: JfBoolean },
|
|
145
|
+
{ tester: rankWith(PRIME + 3, or(isBooleanControl, projectedSchemaMatches((s) => s?.type === "boolean"))), renderer: JfBoolean },
|
|
121
146
|
];
|
|
122
147
|
|
|
123
148
|
// Update the exported array
|
|
@@ -8,11 +8,11 @@ export interface AutoSelectParams {
|
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
* Determines if auto-select should trigger
|
|
11
|
+
* Determines if auto-select should trigger for single-select dropdowns.
|
|
12
12
|
* Returns null if auto-select should not trigger.
|
|
13
13
|
*
|
|
14
14
|
* Auto-select triggers when:
|
|
15
|
-
* - autoSelectSingle option is enabled
|
|
15
|
+
* - autoSelectSingle option is enabled (default: true for single-select)
|
|
16
16
|
* - Provider has finished loading
|
|
17
17
|
* - Exactly one item is available
|
|
18
18
|
* - Current value is empty (undefined/null) OR not in the current options
|
|
@@ -38,3 +38,47 @@ export function shouldAutoSelect(params: AutoSelectParams): unknown | null {
|
|
|
38
38
|
|
|
39
39
|
return null;
|
|
40
40
|
}
|
|
41
|
+
|
|
42
|
+
export interface AutoSelectMultiParams {
|
|
43
|
+
autoSelectSingle: boolean;
|
|
44
|
+
isLoading: boolean;
|
|
45
|
+
items: ProviderItem[];
|
|
46
|
+
currentValue: unknown[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Determines if auto-select should trigger for multiselect dropdowns.
|
|
51
|
+
* Returns null if auto-select should not trigger, otherwise returns an array with the single value.
|
|
52
|
+
*
|
|
53
|
+
* Auto-select triggers when:
|
|
54
|
+
* - autoSelectSingle option is explicitly enabled (default: false for multiselect)
|
|
55
|
+
* - Provider has finished loading
|
|
56
|
+
* - Exactly one item is available
|
|
57
|
+
* - Current value is empty array OR current selection is not in the current options
|
|
58
|
+
*/
|
|
59
|
+
export function shouldAutoSelectMulti(
|
|
60
|
+
params: AutoSelectMultiParams
|
|
61
|
+
): unknown[] | null {
|
|
62
|
+
const { autoSelectSingle, isLoading, items, currentValue } = params;
|
|
63
|
+
|
|
64
|
+
if (!autoSelectSingle || isLoading || items.length !== 1) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const singleItem = items[0];
|
|
69
|
+
if (!singleItem) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const currentArray = Array.isArray(currentValue) ? currentValue : [];
|
|
74
|
+
const isValueEmpty = currentArray.length === 0;
|
|
75
|
+
const hasValidSelection = currentArray.some((val) =>
|
|
76
|
+
items.some((item) => item.value === val)
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
if (isValueEmpty || !hasValidSelection) {
|
|
80
|
+
return [singleItem.value];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return null;
|
|
84
|
+
}
|