@narrative.io/jsonforms-provider-protocols 2.11.0-beta.0 → 2.12.0

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 (148) hide show
  1. package/README.md +193 -33
  2. package/dist/core/initFormData.d.ts +17 -0
  3. package/dist/core/initFormData.d.ts.map +1 -0
  4. package/dist/core/initFormData.js +99 -0
  5. package/dist/core/initFormData.js.map +1 -0
  6. package/dist/core/projection.d.ts +36 -0
  7. package/dist/core/projection.d.ts.map +1 -0
  8. package/dist/core/projection.js +77 -0
  9. package/dist/core/projection.js.map +1 -0
  10. package/dist/core/refs.d.ts +58 -0
  11. package/dist/core/refs.d.ts.map +1 -0
  12. package/dist/core/refs.js +70 -0
  13. package/dist/core/refs.js.map +1 -0
  14. package/dist/core/resolveScope.d.ts +17 -0
  15. package/dist/core/resolveScope.d.ts.map +1 -0
  16. package/dist/core/resolveScope.js +28 -0
  17. package/dist/core/resolveScope.js.map +1 -0
  18. package/dist/core/seedProjectionTargets.d.ts +60 -0
  19. package/dist/core/seedProjectionTargets.d.ts.map +1 -0
  20. package/dist/core/seedProjectionTargets.js +52 -0
  21. package/dist/core/seedProjectionTargets.js.map +1 -0
  22. package/dist/core/transforms.d.ts +8 -10
  23. package/dist/core/transforms.d.ts.map +1 -1
  24. package/dist/core/transforms.js +58 -13
  25. package/dist/core/transforms.js.map +1 -1
  26. package/dist/core/types.d.ts +8 -0
  27. package/dist/core/types.d.ts.map +1 -1
  28. package/dist/index.d.ts +9 -1
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +21 -3
  31. package/dist/index.js.map +1 -1
  32. package/dist/jsonforms-provider-protocols.css +6 -2
  33. package/dist/no-eval-ajv.d.ts +70 -0
  34. package/dist/no-eval-ajv.d.ts.map +1 -0
  35. package/dist/no-eval-ajv.js +247 -0
  36. package/dist/no-eval-ajv.js.map +1 -0
  37. package/dist/vue/components/ProviderAutocomplete.vue.d.ts.map +1 -1
  38. package/dist/vue/components/ProviderAutocomplete.vue.js +10 -4
  39. package/dist/vue/components/ProviderAutocomplete.vue.js.map +1 -1
  40. package/dist/vue/components/ProviderMultiSelect.vue.d.ts.map +1 -1
  41. package/dist/vue/components/ProviderMultiSelect.vue.js +1 -1
  42. package/dist/vue/components/ProviderMultiSelect.vue2.js +19 -9
  43. package/dist/vue/components/ProviderMultiSelect.vue2.js.map +1 -1
  44. package/dist/vue/components/ProviderObjectMultiSelect.vue.d.ts +9 -0
  45. package/dist/vue/components/ProviderObjectMultiSelect.vue.d.ts.map +1 -0
  46. package/dist/vue/components/ProviderObjectMultiSelect.vue.js +8 -0
  47. package/dist/vue/components/ProviderObjectMultiSelect.vue.js.map +1 -0
  48. package/dist/vue/components/ProviderObjectMultiSelect.vue2.js +142 -0
  49. package/dist/vue/components/ProviderObjectMultiSelect.vue2.js.map +1 -0
  50. package/dist/vue/components/ProviderSelect.vue.d.ts.map +1 -1
  51. package/dist/vue/components/ProviderSelect.vue.js +1 -1
  52. package/dist/vue/components/ProviderSelect.vue2.js +20 -8
  53. package/dist/vue/components/ProviderSelect.vue2.js.map +1 -1
  54. package/dist/vue/composables/useDataLayer.d.ts +10 -0
  55. package/dist/vue/composables/useDataLayer.d.ts.map +1 -0
  56. package/dist/vue/composables/useDataLayer.js +26 -0
  57. package/dist/vue/composables/useDataLayer.js.map +1 -0
  58. package/dist/vue/composables/useDerive.d.ts +5 -2
  59. package/dist/vue/composables/useDerive.d.ts.map +1 -1
  60. package/dist/vue/composables/useDerive.js +29 -12
  61. package/dist/vue/composables/useDerive.js.map +1 -1
  62. package/dist/vue/composables/useDeriveInitialValue.d.ts +36 -0
  63. package/dist/vue/composables/useDeriveInitialValue.d.ts.map +1 -0
  64. package/dist/vue/composables/useDeriveInitialValue.js +125 -0
  65. package/dist/vue/composables/useDeriveInitialValue.js.map +1 -0
  66. package/dist/vue/composables/useDirtyValidation.d.ts +9 -0
  67. package/dist/vue/composables/useDirtyValidation.d.ts.map +1 -0
  68. package/dist/vue/composables/useDirtyValidation.js +15 -0
  69. package/dist/vue/composables/useDirtyValidation.js.map +1 -0
  70. package/dist/vue/composables/useProjection.d.ts +42 -0
  71. package/dist/vue/composables/useProjection.d.ts.map +1 -0
  72. package/dist/vue/composables/useProjection.js +116 -0
  73. package/dist/vue/composables/useProjection.js.map +1 -0
  74. package/dist/vue/composables/useProvider.d.ts +2 -2
  75. package/dist/vue/composables/useProvider.d.ts.map +1 -1
  76. package/dist/vue/composables/useProvider.js +14 -10
  77. package/dist/vue/composables/useProvider.js.map +1 -1
  78. package/dist/vue/index.d.ts +9 -1
  79. package/dist/vue/index.d.ts.map +1 -1
  80. package/dist/vue/index.js +72 -34
  81. package/dist/vue/index.js.map +1 -1
  82. package/dist/vue/primevue/JfBoolean.vue.d.ts +9 -0
  83. package/dist/vue/primevue/JfBoolean.vue.d.ts.map +1 -1
  84. package/dist/vue/primevue/JfBoolean.vue.js +42 -21
  85. package/dist/vue/primevue/JfBoolean.vue.js.map +1 -1
  86. package/dist/vue/primevue/JfEnum.vue.d.ts +9 -0
  87. package/dist/vue/primevue/JfEnum.vue.d.ts.map +1 -1
  88. package/dist/vue/primevue/JfEnum.vue.js +35 -21
  89. package/dist/vue/primevue/JfEnum.vue.js.map +1 -1
  90. package/dist/vue/primevue/JfEnumArray.vue.d.ts +9 -0
  91. package/dist/vue/primevue/JfEnumArray.vue.d.ts.map +1 -1
  92. package/dist/vue/primevue/JfEnumArray.vue.js +37 -17
  93. package/dist/vue/primevue/JfEnumArray.vue.js.map +1 -1
  94. package/dist/vue/primevue/JfNumber.vue.d.ts +9 -0
  95. package/dist/vue/primevue/JfNumber.vue.d.ts.map +1 -1
  96. package/dist/vue/primevue/JfNumber.vue.js +30 -20
  97. package/dist/vue/primevue/JfNumber.vue.js.map +1 -1
  98. package/dist/vue/primevue/JfText.vue.d.ts +9 -0
  99. package/dist/vue/primevue/JfText.vue.d.ts.map +1 -1
  100. package/dist/vue/primevue/JfText.vue.js +48 -32
  101. package/dist/vue/primevue/JfText.vue.js.map +1 -1
  102. package/dist/vue/primevue/JfTextArea.vue.d.ts +9 -0
  103. package/dist/vue/primevue/JfTextArea.vue.d.ts.map +1 -1
  104. package/dist/vue/primevue/JfTextArea.vue.js +31 -16
  105. package/dist/vue/primevue/JfTextArea.vue.js.map +1 -1
  106. package/dist/vue/primevue/index.d.ts.map +1 -1
  107. package/dist/vue/primevue/index.js +74 -7
  108. package/dist/vue/primevue/index.js.map +1 -1
  109. package/dist/vue/utils/autoSelect.js.map +1 -1
  110. package/dist/vue/utils/objectMultiSelect.d.ts +68 -0
  111. package/dist/vue/utils/objectMultiSelect.d.ts.map +1 -0
  112. package/dist/vue/utils/objectMultiSelect.js +72 -0
  113. package/dist/vue/utils/objectMultiSelect.js.map +1 -0
  114. package/dist/vue/utils/placeholder.d.ts +17 -0
  115. package/dist/vue/utils/placeholder.d.ts.map +1 -0
  116. package/dist/vue/utils/placeholder.js +17 -0
  117. package/dist/vue/utils/placeholder.js.map +1 -0
  118. package/package.json +10 -2
  119. package/src/core/initFormData.ts +208 -0
  120. package/src/core/projection.ts +147 -0
  121. package/src/core/refs.ts +166 -0
  122. package/src/core/resolveScope.ts +54 -0
  123. package/src/core/seedProjectionTargets.ts +144 -0
  124. package/src/core/transforms.ts +118 -26
  125. package/src/core/types.ts +9 -0
  126. package/src/index.ts +22 -2
  127. package/src/no-eval-ajv.ts +381 -0
  128. package/src/vue/components/ProviderAutocomplete.vue +10 -6
  129. package/src/vue/components/ProviderMultiSelect.vue +22 -15
  130. package/src/vue/components/ProviderObjectMultiSelect.vue +169 -0
  131. package/src/vue/components/ProviderSelect.vue +23 -14
  132. package/src/vue/composables/useDataLayer.ts +43 -0
  133. package/src/vue/composables/useDerive.ts +62 -16
  134. package/src/vue/composables/useDeriveInitialValue.ts +195 -0
  135. package/src/vue/composables/useDirtyValidation.ts +20 -0
  136. package/src/vue/composables/useProjection.ts +245 -0
  137. package/src/vue/composables/useProvider.ts +28 -11
  138. package/src/vue/index.ts +83 -47
  139. package/src/vue/primevue/JfBoolean.vue +35 -12
  140. package/src/vue/primevue/JfEnum.vue +34 -25
  141. package/src/vue/primevue/JfEnumArray.vue +36 -19
  142. package/src/vue/primevue/JfNumber.vue +30 -22
  143. package/src/vue/primevue/JfText.vue +46 -31
  144. package/src/vue/primevue/JfTextArea.vue +30 -19
  145. package/src/vue/primevue/index.ts +88 -7
  146. package/src/vue/utils/autoSelect.ts +2 -2
  147. package/src/vue/utils/objectMultiSelect.ts +171 -0
  148. package/src/vue/utils/placeholder.ts +42 -0
@@ -0,0 +1,245 @@
1
+ import { computed, inject, type ComputedRef, type Ref } from "vue";
2
+ import {
3
+ getProjectedValue,
4
+ setProjectedValue,
5
+ getProjectedSchema,
6
+ parseProjectionPath,
7
+ } from "../../core/projection";
8
+ import { deref } from "../../core/refs";
9
+
10
+ interface ProjectionControl {
11
+ data: unknown;
12
+ path: string;
13
+ errors: string;
14
+ label?: string;
15
+ required?: boolean;
16
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
17
+ schema: Record<string, any>;
18
+ uischema: { options?: { projection?: string; [key: string]: unknown } };
19
+ }
20
+
21
+ // Minimal AJV ErrorObject shape for filtering
22
+ interface ErrorLike {
23
+ instancePath?: string;
24
+ keyword?: string;
25
+ message?: string;
26
+ params?: { missingProperty?: string };
27
+ }
28
+
29
+ /**
30
+ * Resolve the display label for a control.
31
+ * Priority: uischema options.label → schemaTitle (projected sub-schema) → control.label.
32
+ * Appends a trailing asterisk when the control is for a required property.
33
+ */
34
+ function resolveLabel(
35
+ ctrl: ProjectionControl,
36
+ schemaTitle?: string,
37
+ required?: boolean,
38
+ ): string {
39
+ const base =
40
+ (ctrl.uischema?.options?.label as string) ??
41
+ schemaTitle ??
42
+ ctrl.label ??
43
+ "";
44
+ if (!base) return base;
45
+ return required ? `${base} *` : base;
46
+ }
47
+
48
+ /**
49
+ * Determine whether the leaf of a projection path is listed in its parent
50
+ * schema's `required` array. Numeric leaf segments (array indices) are not
51
+ * considered "required properties".
52
+ *
53
+ * Dereferences `$ref` at every step against `root` (defaulting to `schema`)
54
+ * so that schemas like `items: { $ref: '#/$defs/Foo' }` resolve correctly.
55
+ */
56
+ function isProjectedFieldRequired(
57
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
58
+ schema: Record<string, any>,
59
+ path: string,
60
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
61
+ root?: Record<string, any>,
62
+ ): boolean {
63
+ const segments = parseProjectionPath(path);
64
+ if (segments.length === 0) return false;
65
+ const rootSchema = root ?? schema;
66
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
67
+ let current: Record<string, any> | undefined = deref(schema, rootSchema);
68
+ for (let i = 0; i < segments.length - 1; i++) {
69
+ const seg = segments[i]!;
70
+ if (typeof seg === "number") {
71
+ current = deref(current?.items, rootSchema);
72
+ } else {
73
+ current = deref(current?.properties?.[seg], rootSchema);
74
+ }
75
+ if (!current) return false;
76
+ }
77
+ const last = segments[segments.length - 1];
78
+ if (typeof last !== "string") return false;
79
+ current = deref(current, rootSchema);
80
+ return Array.isArray(current?.required) && current.required.includes(last);
81
+ }
82
+
83
+ /**
84
+ * Normalize AJV error message fragments into user-friendly text.
85
+ * e.g. "is a required property" → "is required"
86
+ */
87
+ function normalizeErrors(errors: string): string {
88
+ if (!errors) return errors;
89
+ return errors
90
+ .replace(/is a required property/g, "is required")
91
+ .replace(/must have required property '[^']*'/g, "is required");
92
+ }
93
+
94
+ /**
95
+ * Prefix each error message line with the field label so that AJV fragments
96
+ * like "is required" become "Name is required".
97
+ */
98
+ function prefixErrors(label: string, errors: string): string {
99
+ if (!label || !errors) return errors;
100
+ return errors
101
+ .split("\n")
102
+ .map((line) => `${label} ${line}`)
103
+ .join("\n");
104
+ }
105
+
106
+ /**
107
+ * Convert an AJV ErrorObject's instancePath to a dot-separated control path.
108
+ * Replicates the logic from @jsonforms/core getControlPath.
109
+ */
110
+ function getErrorPath(error: ErrorLike): string {
111
+ let p = (error.instancePath || "").replace(/\//g, ".").replace(/^\./, "");
112
+ if (error.keyword === "required" && error.params?.missingProperty) {
113
+ p = p
114
+ ? p + "." + error.params.missingProperty
115
+ : error.params.missingProperty;
116
+ }
117
+ return p;
118
+ }
119
+
120
+ export interface ProjectionResult {
121
+ /** The value at the projected path (for rendering) */
122
+ projectedData: ComputedRef<unknown>;
123
+ /** The schema at the projected path (for renderer selection) */
124
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
125
+ projectedSchema: ComputedRef<Record<string, any>>;
126
+ /** Wrapped handleChange that writes through the projection */
127
+ handleProjectedChange: (path: string, value: unknown) => void;
128
+ /** Whether projection is active */
129
+ hasProjection: boolean;
130
+ /** Resolved display label (options.label → projected schema title → control.label) */
131
+ projectedLabel: ComputedRef<string>;
132
+ /** Error string combining base-path and projected sub-path errors */
133
+ projectedErrors: ComputedRef<string>;
134
+ }
135
+
136
+ /**
137
+ * Composable that wraps a JSON Forms control with projection support.
138
+ *
139
+ * When `options.projection` is set on the uischema, this composable:
140
+ * - Reads the projected sub-value from the control data
141
+ * - Resolves the projected sub-schema for renderer type resolution
142
+ * - Wraps handleChange to write back through the projection path (preserving siblings)
143
+ *
144
+ * When no projection is set, it passes through control data/schema/handleChange unchanged.
145
+ */
146
+ export function useProjection(
147
+ control: Ref<ProjectionControl>,
148
+ handleChange: (path: string, value: unknown) => void,
149
+ ): ProjectionResult {
150
+ const projection = control.value.uischema?.options?.projection as
151
+ | string
152
+ | undefined;
153
+
154
+ if (!projection) {
155
+ const label = computed(() =>
156
+ resolveLabel(control.value, undefined, control.value.required),
157
+ );
158
+ return {
159
+ projectedData: computed(() => control.value.data),
160
+ projectedSchema: computed(() => control.value.schema),
161
+ handleProjectedChange: handleChange,
162
+ hasProjection: false,
163
+ projectedLabel: label,
164
+ projectedErrors: computed(() =>
165
+ prefixErrors(
166
+ label.value.replace(/\*$/, "").trim(),
167
+ normalizeErrors(control.value.errors),
168
+ ),
169
+ ),
170
+ };
171
+ }
172
+
173
+ // Inject JSONForms state to access raw AJV errors for projected sub-paths,
174
+ // and the root schema so that `$ref` resolution inside the projected slice
175
+ // can find `$defs` (which live at the root, not on the control's schema).
176
+ const jsonforms = inject<{
177
+ core?: {
178
+ errors?: ErrorLike[];
179
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
180
+ schema?: Record<string, any>;
181
+ };
182
+ } | null>("jsonforms", null);
183
+
184
+ const fullProjectedPath = control.value.path + "." + projection;
185
+
186
+ const projectedData = computed(() =>
187
+ getProjectedValue(control.value.data, projection),
188
+ );
189
+
190
+ const projectedSchema = computed(() =>
191
+ getProjectedSchema(control.value.schema, projection),
192
+ );
193
+
194
+ const projectedRequired = computed(() =>
195
+ isProjectedFieldRequired(
196
+ control.value.schema,
197
+ projection,
198
+ jsonforms?.core?.schema ?? control.value.schema,
199
+ ),
200
+ );
201
+
202
+ const label = computed(() =>
203
+ resolveLabel(
204
+ control.value,
205
+ projectedSchema.value?.title,
206
+ projectedRequired.value,
207
+ ),
208
+ );
209
+
210
+ const projectedErrors = computed(() => {
211
+ const baseErrors = normalizeErrors(control.value.errors || "");
212
+
213
+ const rawErrors = jsonforms?.core?.errors ?? ([] as ErrorLike[]);
214
+ const matching = rawErrors.filter(
215
+ (err) => getErrorPath(err) === fullProjectedPath,
216
+ );
217
+
218
+ let errStr: string;
219
+ if (matching.length === 0) {
220
+ errStr = baseErrors;
221
+ } else {
222
+ const projMsg = matching
223
+ .map((e) => (e.keyword === "required" ? "is required" : e.message))
224
+ .filter(Boolean)
225
+ .join("\n");
226
+ errStr = [baseErrors, projMsg].filter(Boolean).join("\n");
227
+ }
228
+
229
+ return prefixErrors(label.value.replace(/\*$/, "").trim(), errStr);
230
+ });
231
+
232
+ const handleProjectedChange = (path: string, value: unknown) => {
233
+ const fullValue = setProjectedValue(control.value.data, projection, value);
234
+ handleChange(path, fullValue);
235
+ };
236
+
237
+ return {
238
+ projectedData,
239
+ projectedSchema,
240
+ handleProjectedChange,
241
+ hasProjection: true,
242
+ projectedLabel: label,
243
+ projectedErrors,
244
+ };
245
+ }
@@ -10,6 +10,7 @@ import {
10
10
  } from "vue";
11
11
  import { cache as globalCache } from "../../core/cache";
12
12
  import { registry as globalRegistry } from "../../core/registry";
13
+ import { renderObj } from "../../core/templating";
13
14
  import type {
14
15
  AuthConfig,
15
16
  ProviderBinding,
@@ -25,8 +26,11 @@ export function useProvider(
25
26
  ctxBits: {
26
27
  data: unknown | Ref<unknown> | ComputedRef<unknown>;
27
28
  path: string;
28
- dependsOnValues?: unknown[];
29
- uiQuery?: string;
29
+ dependsOnValues?: unknown[] | Ref<unknown[]> | ComputedRef<unknown[]>;
30
+ uiQuery?:
31
+ | string
32
+ | Ref<string | undefined>
33
+ | ComputedRef<string | undefined>;
30
34
  },
31
35
  ) {
32
36
  const registry = inject("providerRegistry", globalRegistry);
@@ -41,14 +45,27 @@ export function useProvider(
41
45
  const error = ref<string | undefined>(undefined);
42
46
  const ac = new AbortController();
43
47
 
44
- const cacheKey = computed(() =>
45
- JSON.stringify({
46
- b: unref(binding),
47
- d: ctxBits.dependsOnValues ?? [],
48
- q: ctxBits.uiQuery ?? "",
49
- data: unref(ctxBits.data), // Include data in cache key for reactivity
50
- }),
51
- );
48
+ // Cache key keys on the *rendered* binding config (with template variables
49
+ // substituted) plus dependsOnValues + uiQuery. This means:
50
+ // - `{{data.country}}` in the URL → cache key changes when country changes,
51
+ // auto-refetching without callers having to declare dependsOn.
52
+ // - Unrelated form fields → rendered URL is unchanged → no refetch.
53
+ // The protocol's resolve() still receives the raw config and current data,
54
+ // so per-fetch URL rendering works as before.
55
+ const cacheKey = computed(() => {
56
+ const b = unref(binding);
57
+ const renderedBinding = b
58
+ ? {
59
+ ...b,
60
+ config: renderObj(b.config ?? {}, { data: unref(ctxBits.data) }),
61
+ }
62
+ : b;
63
+ return JSON.stringify({
64
+ b: renderedBinding,
65
+ d: unref(ctxBits.dependsOnValues) ?? [],
66
+ q: unref(ctxBits.uiQuery) ?? "",
67
+ });
68
+ });
52
69
 
53
70
  async function load() {
54
71
  const bindingValue = unref(binding);
@@ -71,7 +88,7 @@ export function useProvider(
71
88
  const out = await driver.resolve(bindingValue.config ?? {}, {
72
89
  data: unref(ctxBits.data),
73
90
  path: ctxBits.path,
74
- ui: { query: ctxBits.uiQuery },
91
+ ui: { query: unref(ctxBits.uiQuery) },
75
92
  signal: ac.signal,
76
93
  auth: resolveAuth(bindingValue.auth, auth),
77
94
  });
package/src/vue/index.ts CHANGED
@@ -1,43 +1,47 @@
1
1
  import type { UISchemaElement } from "@jsonforms/core";
2
2
  import {
3
3
  and,
4
+ isIntegerControl,
4
5
  isNumberControl,
5
6
  isStringControl,
6
7
  or,
7
8
  rankWith,
8
9
  isControl,
9
10
  } from "@jsonforms/core";
11
+ import { resolveScopeSchema } from "../core/resolveScope";
12
+ import { resolveItemsSchema } from "./utils/objectMultiSelect";
10
13
  import ProviderAutocomplete from "./components/ProviderAutocomplete.vue";
11
14
  import ProviderSelect from "./components/ProviderSelect.vue";
12
15
  import ProviderMultiSelect from "./components/ProviderMultiSelect.vue";
16
+ import ProviderObjectMultiSelect from "./components/ProviderObjectMultiSelect.vue";
13
17
 
14
18
  // Custom tester that checks if provider option exists (as object or boolean)
15
19
  const hasProvider = (uischema: UISchemaElement) => {
16
20
  return uischema?.options?.provider !== undefined;
17
21
  };
18
22
 
23
+ // Integer fallback tester — handles nested scopes like #/properties/parent/properties/child
24
+ const isIntegerScope = (uischema: unknown, schema: unknown) => {
25
+ const ui = uischema as { type?: string; scope?: string };
26
+ if (ui?.type !== "Control" || !ui?.scope) return false;
27
+
28
+ const propertySchema = resolveScopeSchema(
29
+ ui.scope,
30
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
31
+ schema as Record<string, any>,
32
+ );
33
+ return propertySchema?.type === "integer";
34
+ };
35
+
19
36
  // Create specific testers for each component type
20
37
  const providerSelectTester = rankWith(
21
- 106, // Higher than PrimeVue base (100) to ensure providers take precedence
38
+ 107, // Higher than PrimeVue JfNumber integer renderer (106) so provider wins
22
39
  and(
23
40
  or(
24
41
  isStringControl,
25
42
  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
- }),
43
+ isIntegerControl,
44
+ and(isControl, isIntegerScope),
41
45
  ),
42
46
  hasProvider,
43
47
  (uischema) => !uischema?.options?.autocomplete,
@@ -45,55 +49,75 @@ const providerSelectTester = rankWith(
45
49
  );
46
50
 
47
51
  const providerAutocompleteTester = rankWith(
48
- 107, // Higher than PrimeVue base (100) to ensure providers take precedence
52
+ 108, // Higher than providerSelectTester so autocomplete variant wins when flagged
49
53
  and(
50
54
  or(
51
55
  isStringControl,
52
56
  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
- }),
57
+ isIntegerControl,
58
+ and(isControl, isIntegerScope),
68
59
  ),
69
60
  hasProvider,
70
61
  (uischema) => uischema?.options?.autocomplete === true,
71
62
  ),
72
63
  );
73
64
 
74
- // Custom array tester - check both uischema control type and schema type
75
- const isArrayControl = (uischema: UISchemaElement, schema: unknown) => {
65
+ // Resolve the array schema at a control's scope, or undefined if the
66
+ // control isn't an array. Shared by both array testers below.
67
+ const getArraySchema = (uischema: UISchemaElement, schema: unknown) => {
76
68
  const controlSchema = uischema as { type: string; scope?: string };
77
- if (controlSchema.type !== "Control" || !controlSchema.scope) {
78
- return false;
79
- }
69
+ if (controlSchema.type !== "Control" || !controlSchema.scope) return undefined;
70
+ const propertySchema = resolveScopeSchema(
71
+ controlSchema.scope,
72
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
73
+ schema as Record<string, any>,
74
+ );
75
+ if (propertySchema?.type !== "array") return undefined;
76
+ return propertySchema;
77
+ };
80
78
 
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
- };
79
+ // Object-item arrays render with paired-object MultiSelect.
80
+ const isObjectArrayControl = (uischema: UISchemaElement, schema: unknown) => {
81
+ const arr = getArraySchema(uischema, schema);
82
+ if (!arr) return false;
83
+ const items = resolveItemsSchema(
84
+ arr,
85
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
86
+ schema as Record<string, any>,
87
+ );
88
+ return items?.type === "object";
89
+ };
87
90
 
88
- return propertySchema?.type === "array";
91
+ // Scalar-item arrays — the original tester. Excludes object-item arrays so
92
+ // the new ObjectMultiSelect renderer can claim them at a higher rank.
93
+ const isScalarArrayControl = (uischema: UISchemaElement, schema: unknown) => {
94
+ const arr = getArraySchema(uischema, schema);
95
+ if (!arr) return false;
96
+ const items = resolveItemsSchema(
97
+ arr,
98
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
99
+ schema as Record<string, any>,
100
+ );
101
+ // Treat unknown / no-items shapes as scalar (matches prior behavior where
102
+ // any array control was eligible for ProviderMultiSelect).
103
+ return items?.type !== "object";
89
104
  };
90
105
 
106
+ const providerObjectMultiSelectTester = rankWith(
107
+ 110, // One higher than scalar multi-select so object-item arrays match first.
108
+ and(isObjectArrayControl, hasProvider),
109
+ );
110
+
91
111
  const providerMultiSelectTester = rankWith(
92
- 108, // Highest priority for array controls with providers
93
- and(isArrayControl, hasProvider),
112
+ 109,
113
+ and(isScalarArrayControl, hasProvider),
94
114
  );
95
115
 
96
116
  export const providerRenderers = [
117
+ {
118
+ tester: providerObjectMultiSelectTester,
119
+ renderer: ProviderObjectMultiSelect,
120
+ },
97
121
  { tester: providerMultiSelectTester, renderer: ProviderMultiSelect },
98
122
  { tester: providerAutocompleteTester, renderer: ProviderAutocomplete },
99
123
  { tester: providerSelectTester, renderer: ProviderSelect },
@@ -103,8 +127,20 @@ export const providerRenderers = [
103
127
  export { primevueRenderers, registerPrimevueRenderers } from "./primevue";
104
128
 
105
129
  // Export individual components
106
- export { ProviderAutocomplete, ProviderSelect, ProviderMultiSelect };
130
+ export {
131
+ ProviderAutocomplete,
132
+ ProviderSelect,
133
+ ProviderMultiSelect,
134
+ ProviderObjectMultiSelect,
135
+ };
107
136
  export { useProvider } from "./composables/useProvider";
137
+ export { useProjection } from "./composables/useProjection";
138
+ export type { ProjectionResult } from "./composables/useProjection";
139
+ export { createDataLayer, useDataLayer } from "./composables/useDataLayer";
140
+ export type { DataLayer } from "./composables/useDataLayer";
141
+ export { useDeriveInitialValue } from "./composables/useDeriveInitialValue";
142
+ export type { DeriveInitialValueCfg } from "./composables/useDeriveInitialValue";
143
+ export { useDirtyValidation } from "./composables/useDirtyValidation";
108
144
  export * from "./testers";
109
145
 
110
146
  // 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,26 +41,46 @@ 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";
45
+ import { useDirtyValidation } from "../composables/useDirtyValidation";
41
46
  import Checkbox from "primevue/checkbox";
42
47
 
43
48
  // Access props from the component instance
44
49
  const instance = getCurrentInstance()!;
45
50
  const props = instance.props as unknown as ControlProps;
46
- const { control, handleChange } = useJsonFormsControl(props);
51
+ const { control, handleChange: rawHandleChange } = useJsonFormsControl(props);
52
+ const {
53
+ projectedData,
54
+ handleProjectedChange: handleChange,
55
+ projectedErrors,
56
+ projectedLabel,
57
+ } = useProjection(control, rawHandleChange);
47
58
 
48
- const onToggle = (val: boolean) => handleChange(control.value.path, val);
59
+ // Track user interaction errors only show after first toggle
60
+ const { showErrors, markDirty } = useDirtyValidation(control, projectedErrors);
61
+
62
+ const onToggle = (val: boolean) => {
63
+ markDirty();
64
+ handleChange(control.value.path, val);
65
+ };
49
66
  </script>
50
67
 
51
68
  <template>
52
- <div class="jf-control" style="flex-direction: row; align-items: center">
53
- <Checkbox
54
- :binary="true"
55
- :model-value="!!control.data"
56
- :disabled="!control.enabled"
57
- :aria-invalid="!!control.errors || undefined"
58
- @update:model-value="onToggle"
59
- />
60
- <label v-if="control.label" class="jf-label">{{ control.label }}</label>
61
- <small v-if="control.errors" class="p-error">{{ control.errors }}</small>
69
+ <div class="jf-control">
70
+ <div style="display: flex; flex-direction: row; align-items: center; gap: 0.5rem">
71
+ <Checkbox
72
+ :binary="true"
73
+ :model-value="!!projectedData"
74
+ :disabled="!control.enabled"
75
+ :class="{ 'p-invalid': showErrors }"
76
+ :aria-invalid="showErrors || undefined"
77
+ @update:model-value="onToggle"
78
+ />
79
+ <label v-if="projectedLabel" class="jf-label">{{ projectedLabel }}</label>
80
+ </div>
81
+ <div v-if="control.description" class="jf-description">
82
+ {{ control.description }}
83
+ </div>
84
+ <small v-if="showErrors" class="p-error">{{ projectedErrors }}</small>
62
85
  </div>
63
86
  </template>