@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.
Files changed (99) hide show
  1. package/README.md +166 -30
  2. package/dist/core/projection.d.ts +32 -0
  3. package/dist/core/projection.d.ts.map +1 -0
  4. package/dist/core/projection.js +74 -0
  5. package/dist/core/projection.js.map +1 -0
  6. package/dist/core/resolveScope.d.ts +11 -0
  7. package/dist/core/resolveScope.d.ts.map +1 -0
  8. package/dist/core/resolveScope.js +22 -0
  9. package/dist/core/resolveScope.js.map +1 -0
  10. package/dist/core/transforms.d.ts +8 -10
  11. package/dist/core/transforms.d.ts.map +1 -1
  12. package/dist/core/transforms.js +56 -13
  13. package/dist/core/transforms.js.map +1 -1
  14. package/dist/core/types.d.ts +7 -0
  15. package/dist/core/types.d.ts.map +1 -1
  16. package/dist/index.d.ts +4 -1
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +12 -2
  19. package/dist/index.js.map +1 -1
  20. package/dist/jsonforms-provider-protocols.css +2 -2
  21. package/dist/vue/components/ProviderAutocomplete.vue.d.ts.map +1 -1
  22. package/dist/vue/components/ProviderAutocomplete.vue.js +4 -2
  23. package/dist/vue/components/ProviderAutocomplete.vue.js.map +1 -1
  24. package/dist/vue/components/ProviderMultiSelect.vue.d.ts.map +1 -1
  25. package/dist/vue/components/ProviderMultiSelect.vue.js +1 -1
  26. package/dist/vue/components/ProviderMultiSelect.vue2.js +22 -4
  27. package/dist/vue/components/ProviderMultiSelect.vue2.js.map +1 -1
  28. package/dist/vue/components/ProviderSelect.vue.d.ts.map +1 -1
  29. package/dist/vue/components/ProviderSelect.vue.js +1 -1
  30. package/dist/vue/components/ProviderSelect.vue2.js +5 -3
  31. package/dist/vue/components/ProviderSelect.vue2.js.map +1 -1
  32. package/dist/vue/composables/useDataLayer.d.ts +9 -0
  33. package/dist/vue/composables/useDataLayer.d.ts.map +1 -0
  34. package/dist/vue/composables/useDataLayer.js +25 -0
  35. package/dist/vue/composables/useDataLayer.js.map +1 -0
  36. package/dist/vue/composables/useDerive.d.ts +5 -2
  37. package/dist/vue/composables/useDerive.d.ts.map +1 -1
  38. package/dist/vue/composables/useDerive.js +12 -12
  39. package/dist/vue/composables/useDerive.js.map +1 -1
  40. package/dist/vue/composables/useProjection.d.ts +35 -0
  41. package/dist/vue/composables/useProjection.d.ts.map +1 -0
  42. package/dist/vue/composables/useProjection.js +33 -0
  43. package/dist/vue/composables/useProjection.js.map +1 -0
  44. package/dist/vue/index.d.ts +4 -0
  45. package/dist/vue/index.d.ts.map +1 -1
  46. package/dist/vue/index.js +15 -29
  47. package/dist/vue/index.js.map +1 -1
  48. package/dist/vue/primevue/JfBoolean.vue.d.ts +9 -0
  49. package/dist/vue/primevue/JfBoolean.vue.d.ts.map +1 -1
  50. package/dist/vue/primevue/JfBoolean.vue.js +10 -5
  51. package/dist/vue/primevue/JfBoolean.vue.js.map +1 -1
  52. package/dist/vue/primevue/JfEnum.vue.d.ts +9 -0
  53. package/dist/vue/primevue/JfEnum.vue.d.ts.map +1 -1
  54. package/dist/vue/primevue/JfEnum.vue.js +12 -7
  55. package/dist/vue/primevue/JfEnum.vue.js.map +1 -1
  56. package/dist/vue/primevue/JfEnumArray.vue.d.ts +9 -0
  57. package/dist/vue/primevue/JfEnumArray.vue.d.ts.map +1 -1
  58. package/dist/vue/primevue/JfEnumArray.vue.js +29 -8
  59. package/dist/vue/primevue/JfEnumArray.vue.js.map +1 -1
  60. package/dist/vue/primevue/JfNumber.vue.d.ts +9 -0
  61. package/dist/vue/primevue/JfNumber.vue.d.ts.map +1 -1
  62. package/dist/vue/primevue/JfNumber.vue.js +11 -6
  63. package/dist/vue/primevue/JfNumber.vue.js.map +1 -1
  64. package/dist/vue/primevue/JfText.vue.d.ts +9 -0
  65. package/dist/vue/primevue/JfText.vue.d.ts.map +1 -1
  66. package/dist/vue/primevue/JfText.vue.js +13 -8
  67. package/dist/vue/primevue/JfText.vue.js.map +1 -1
  68. package/dist/vue/primevue/JfTextArea.vue.d.ts +9 -0
  69. package/dist/vue/primevue/JfTextArea.vue.d.ts.map +1 -1
  70. package/dist/vue/primevue/JfTextArea.vue.js +11 -6
  71. package/dist/vue/primevue/JfTextArea.vue.js.map +1 -1
  72. package/dist/vue/primevue/index.d.ts.map +1 -1
  73. package/dist/vue/primevue/index.js +22 -7
  74. package/dist/vue/primevue/index.js.map +1 -1
  75. package/dist/vue/utils/autoSelect.d.ts +19 -2
  76. package/dist/vue/utils/autoSelect.d.ts.map +1 -1
  77. package/dist/vue/utils/autoSelect.js +21 -1
  78. package/dist/vue/utils/autoSelect.js.map +1 -1
  79. package/package.json +7 -3
  80. package/src/core/projection.ts +136 -0
  81. package/src/core/resolveScope.ts +39 -0
  82. package/src/core/transforms.ts +91 -26
  83. package/src/core/types.ts +8 -0
  84. package/src/index.ts +7 -0
  85. package/src/vue/components/ProviderAutocomplete.vue +4 -2
  86. package/src/vue/components/ProviderMultiSelect.vue +26 -4
  87. package/src/vue/components/ProviderSelect.vue +5 -3
  88. package/src/vue/composables/useDataLayer.ts +43 -0
  89. package/src/vue/composables/useDerive.ts +19 -16
  90. package/src/vue/composables/useProjection.ts +74 -0
  91. package/src/vue/index.ts +20 -46
  92. package/src/vue/primevue/JfBoolean.vue +7 -2
  93. package/src/vue/primevue/JfEnum.vue +9 -4
  94. package/src/vue/primevue/JfEnumArray.vue +30 -5
  95. package/src/vue/primevue/JfNumber.vue +8 -3
  96. package/src/vue/primevue/JfText.vue +10 -5
  97. package/src/vue/primevue/JfTextArea.vue +8 -3
  98. package/src/vue/primevue/index.ts +32 -7
  99. 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 - check both uischema control type and schema type
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
- // Extract the property schema from the root schema
82
- const rootSchema = schema as { properties?: Record<string, unknown> };
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="!!control.data"
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: control.value.data,
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="control.data ?? null"
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(control.value.data) ? control.value.data : [];
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(control.value.data) ? control.value.data : [];
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 control.data === 'number' ? control.data : null"
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 (control.value.data !== newValue) {
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="control.data ?? ''"
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="control.data ?? ''"
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 (control.value.data !== newValue) {
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="control.data ?? ''"
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 and returns the value to select.
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
+ }