@narrative.io/jsonforms-provider-protocols 2.11.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 (147) 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 +12 -6
  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 +21 -11
  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 +22 -10
  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 +44 -17
  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 +38 -24
  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 +40 -20
  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 +33 -23
  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 +51 -35
  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 +34 -19
  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 +100 -8
  108. package/dist/vue/primevue/index.js.map +1 -1
  109. package/dist/vue/utils/objectMultiSelect.d.ts +68 -0
  110. package/dist/vue/utils/objectMultiSelect.d.ts.map +1 -0
  111. package/dist/vue/utils/objectMultiSelect.js +72 -0
  112. package/dist/vue/utils/objectMultiSelect.js.map +1 -0
  113. package/dist/vue/utils/placeholder.d.ts +17 -0
  114. package/dist/vue/utils/placeholder.d.ts.map +1 -0
  115. package/dist/vue/utils/placeholder.js +17 -0
  116. package/dist/vue/utils/placeholder.js.map +1 -0
  117. package/package.json +10 -2
  118. package/src/core/initFormData.ts +208 -0
  119. package/src/core/projection.ts +147 -0
  120. package/src/core/refs.ts +166 -0
  121. package/src/core/resolveScope.ts +54 -0
  122. package/src/core/seedProjectionTargets.ts +144 -0
  123. package/src/core/transforms.ts +118 -26
  124. package/src/core/types.ts +9 -0
  125. package/src/index.ts +22 -2
  126. package/src/no-eval-ajv.ts +381 -0
  127. package/src/vue/components/ProviderAutocomplete.vue +11 -7
  128. package/src/vue/components/ProviderMultiSelect.vue +22 -15
  129. package/src/vue/components/ProviderObjectMultiSelect.vue +169 -0
  130. package/src/vue/components/ProviderSelect.vue +23 -14
  131. package/src/vue/composables/useDataLayer.ts +43 -0
  132. package/src/vue/composables/useDerive.ts +62 -16
  133. package/src/vue/composables/useDeriveInitialValue.ts +195 -0
  134. package/src/vue/composables/useDirtyValidation.ts +20 -0
  135. package/src/vue/composables/useProjection.ts +245 -0
  136. package/src/vue/composables/useProvider.ts +28 -11
  137. package/src/vue/index.ts +83 -47
  138. package/src/vue/primevue/JfBoolean.vue +35 -12
  139. package/src/vue/primevue/JfEnum.vue +35 -26
  140. package/src/vue/primevue/JfEnumArray.vue +37 -20
  141. package/src/vue/primevue/JfNumber.vue +32 -24
  142. package/src/vue/primevue/JfText.vue +48 -33
  143. package/src/vue/primevue/JfTextArea.vue +32 -21
  144. package/src/vue/primevue/index.ts +114 -8
  145. package/src/vue/styles.css +26 -1
  146. package/src/vue/utils/objectMultiSelect.ts +171 -0
  147. package/src/vue/utils/placeholder.ts +42 -0
@@ -0,0 +1,381 @@
1
+ import {
2
+ Validator,
3
+ type OutputUnit,
4
+ type Schema,
5
+ type SchemaDraft,
6
+ } from "@cfworker/json-schema";
7
+
8
+ /**
9
+ * CSP-safe JSON Schema validation backed by `@cfworker/json-schema`.
10
+ *
11
+ * Cloudflare Pages and similar strict-CSP environments forbid `new Function`,
12
+ * which is how AJV compiles validators. This factory returns an
13
+ * AJV-shaped facade whose `compile()` produces a synchronous validator
14
+ * function `(data) => boolean` with a mutable `errors` property — enough
15
+ * for `<JsonForms :ajv="..." />` to drop in without any other changes.
16
+ *
17
+ * Only `compile()` is functional; the rest of the surface (`addSchema`,
18
+ * `getSchema`, `removeSchema`, `addFormat`, `addKeyword`, `opts`) exists
19
+ * as no-ops for forward compatibility with plugins that probe the AJV
20
+ * interface but never reach those methods at runtime.
21
+ */
22
+
23
+ // AJV's ErrorObject shape. We don't import `ajv` here to keep the lib's
24
+ // zero-runtime-deps philosophy intact; the consumer's transitive AJV types
25
+ // are structurally compatible.
26
+ export interface NoEvalErrorObject {
27
+ instancePath: string;
28
+ schemaPath: string;
29
+ keyword: string;
30
+ params: Record<string, unknown>;
31
+ message?: string;
32
+ }
33
+
34
+ export interface NoEvalValidateFunction {
35
+ (data: unknown): boolean;
36
+ errors: NoEvalErrorObject[] | null;
37
+ }
38
+
39
+ // Minimal subset of AJV's surface that JsonForms touches. The factory
40
+ // returns this as `unknown as Ajv` at the call site; consumers cast on
41
+ // import or pass directly to `<JsonForms :ajv="..." />`.
42
+ export interface NoEvalAjv {
43
+ compile: (schema: unknown) => NoEvalValidateFunction;
44
+ addSchema: (schema: unknown, key?: string) => NoEvalAjv;
45
+ getSchema: (key: string) => NoEvalValidateFunction | undefined;
46
+ removeSchema: (schemaKeyRef?: unknown) => NoEvalAjv;
47
+ addFormat: (name: string, format: unknown) => NoEvalAjv;
48
+ addKeyword: (definition: unknown) => NoEvalAjv;
49
+ opts: Readonly<Record<string, unknown>>;
50
+ }
51
+
52
+ export interface CreateNoEvalAjvOptions {
53
+ /**
54
+ * JSON Schema draft to validate against. Defaults to `'2020-12'` to match
55
+ * the spec; schemas authored against draft-07 should pass `'7'`.
56
+ */
57
+ draft?: SchemaDraft;
58
+ }
59
+
60
+ const KEYWORD_WRAPPERS = new Set([
61
+ "properties",
62
+ "items",
63
+ "prefixItems",
64
+ "$ref",
65
+ ]);
66
+
67
+ const QUOTED_RE = /(['"])((?:\\.|(?!\1).)*?)\1/g;
68
+
69
+ function findQuoted(s: string): string[] {
70
+ const out: string[] = [];
71
+ let m: RegExpExecArray | null;
72
+ QUOTED_RE.lastIndex = 0;
73
+ while ((m = QUOTED_RE.exec(s)) !== null) {
74
+ out.push(m[2]!);
75
+ }
76
+ return out;
77
+ }
78
+
79
+ function firstQuoted(s: string): string | undefined {
80
+ const all = findQuoted(s);
81
+ return all[0];
82
+ }
83
+
84
+ function allQuoted(s: string): string[] {
85
+ return findQuoted(s);
86
+ }
87
+
88
+ const NUMBER_RE = /-?\d+(?:\.\d+)?/g;
89
+
90
+ function extractNumbers(s: string): number[] {
91
+ const out: number[] = [];
92
+ let m: RegExpExecArray | null;
93
+ NUMBER_RE.lastIndex = 0;
94
+ while ((m = NUMBER_RE.exec(s)) !== null) {
95
+ out.push(Number(m[0]));
96
+ }
97
+ return out;
98
+ }
99
+
100
+ function firstNumber(s: string): number | undefined {
101
+ const nums = extractNumbers(s);
102
+ return nums[0];
103
+ }
104
+
105
+ function stripPointerPrefix(p: string): string {
106
+ return p.startsWith("#") ? p.slice(1) : p;
107
+ }
108
+
109
+ interface KeywordTransform {
110
+ params: Record<string, unknown>;
111
+ message: string | undefined;
112
+ }
113
+
114
+ function transformByKeyword(unit: OutputUnit): KeywordTransform {
115
+ const { keyword, error } = unit;
116
+
117
+ switch (keyword) {
118
+ case "required": {
119
+ const prop = firstQuoted(error);
120
+ return {
121
+ params: { missingProperty: prop ?? "" },
122
+ message: prop
123
+ ? `must have required property '${prop}'`
124
+ : "must have required property",
125
+ };
126
+ }
127
+
128
+ case "type": {
129
+ const quoted = allQuoted(error);
130
+ // cfworker emits `Instance type "X" is invalid. Expected "Y".` (single)
131
+ // or `... Expected "Y" or "Z".` (union). Per AJV: `params.type` is the
132
+ // expected type as a string; for unions it's comma-separated.
133
+ const expected = quoted.slice(1).join(",") || quoted[0];
134
+ const message = expected ? `must be ${expected}` : "must be of expected type";
135
+ return {
136
+ params: expected ? { type: expected } : {},
137
+ message,
138
+ };
139
+ }
140
+
141
+ case "enum": {
142
+ // cfworker error: `Instance does not match any of [...].`
143
+ const match = error.match(/\[.*\]/);
144
+ let allowedValues: unknown[] = [];
145
+ if (match) {
146
+ try {
147
+ allowedValues = JSON.parse(match[0]);
148
+ } catch {
149
+ allowedValues = [];
150
+ }
151
+ }
152
+ return {
153
+ params: { allowedValues },
154
+ message: "must be equal to one of the allowed values",
155
+ };
156
+ }
157
+
158
+ case "const":
159
+ return {
160
+ params: {},
161
+ message: "must be equal to constant",
162
+ };
163
+
164
+ case "minimum": {
165
+ const limit = extractNumbers(error)[1];
166
+ return {
167
+ params: { comparison: ">=", limit: limit ?? 0 },
168
+ message: `must be >= ${limit ?? 0}`,
169
+ };
170
+ }
171
+
172
+ case "exclusiveMinimum": {
173
+ const limit = extractNumbers(error)[1];
174
+ return {
175
+ params: { comparison: ">", limit: limit ?? 0 },
176
+ message: `must be > ${limit ?? 0}`,
177
+ };
178
+ }
179
+
180
+ case "maximum": {
181
+ const limit = extractNumbers(error)[1];
182
+ return {
183
+ params: { comparison: "<=", limit: limit ?? 0 },
184
+ message: `must be <= ${limit ?? 0}`,
185
+ };
186
+ }
187
+
188
+ case "exclusiveMaximum": {
189
+ const limit = extractNumbers(error)[1];
190
+ return {
191
+ params: { comparison: "<", limit: limit ?? 0 },
192
+ message: `must be < ${limit ?? 0}`,
193
+ };
194
+ }
195
+
196
+ case "minLength": {
197
+ const limit = firstNumber(error) ?? 0;
198
+ return {
199
+ params: { limit },
200
+ message: `must NOT have fewer than ${limit} characters`,
201
+ };
202
+ }
203
+
204
+ case "maxLength": {
205
+ const limit = firstNumber(error) ?? 0;
206
+ return {
207
+ params: { limit },
208
+ message: `must NOT have more than ${limit} characters`,
209
+ };
210
+ }
211
+
212
+ case "minItems": {
213
+ const limit = firstNumber(error) ?? 0;
214
+ return {
215
+ params: { limit },
216
+ message: `must NOT have fewer than ${limit} items`,
217
+ };
218
+ }
219
+
220
+ case "maxItems": {
221
+ const limit = firstNumber(error) ?? 0;
222
+ return {
223
+ params: { limit },
224
+ message: `must NOT have more than ${limit} items`,
225
+ };
226
+ }
227
+
228
+ case "pattern": {
229
+ const pattern = firstQuoted(error) ?? "";
230
+ return {
231
+ params: { pattern },
232
+ message: `must match pattern "${pattern}"`,
233
+ };
234
+ }
235
+
236
+ case "format": {
237
+ const format = firstQuoted(error) ?? "";
238
+ return {
239
+ params: { format },
240
+ message: `must match format "${format}"`,
241
+ };
242
+ }
243
+
244
+ case "multipleOf": {
245
+ // cfworker emits two numbers: the value and the divisor.
246
+ const nums = extractNumbers(error);
247
+ const multipleOf = nums[1] ?? nums[0] ?? 1;
248
+ return {
249
+ params: { multipleOf },
250
+ message: `must be multiple of ${multipleOf}`,
251
+ };
252
+ }
253
+
254
+ case "uniqueItems":
255
+ return {
256
+ params: {},
257
+ message: "must NOT have duplicate items",
258
+ };
259
+
260
+ case "additionalProperties": {
261
+ const additionalProperty = firstQuoted(error) ?? "";
262
+ return {
263
+ params: { additionalProperty },
264
+ message: `must NOT have additional property '${additionalProperty}'`,
265
+ };
266
+ }
267
+
268
+ case "anyOf":
269
+ return { params: {}, message: "must match a schema in anyOf" };
270
+
271
+ case "oneOf":
272
+ return {
273
+ params: { passingSchemas: null },
274
+ message: "must match exactly one schema in oneOf",
275
+ };
276
+
277
+ case "allOf":
278
+ return { params: {}, message: "must match all schemas in allOf" };
279
+
280
+ case "not":
281
+ return { params: {}, message: "must NOT be valid against schema" };
282
+
283
+ default:
284
+ return { params: {}, message: undefined };
285
+ }
286
+ }
287
+
288
+ /**
289
+ * Transform a single cfworker `OutputUnit` into an AJV-compatible
290
+ * `ErrorObject`, or `null` if the unit is a structural wrapper that should
291
+ * be filtered out (`properties` / `items` / `prefixItems` / `$ref`).
292
+ *
293
+ * Exported for unit testing.
294
+ */
295
+ export function transformUnit(unit: OutputUnit): NoEvalErrorObject | null {
296
+ if (KEYWORD_WRAPPERS.has(unit.keyword)) return null;
297
+
298
+ const { params, message } = transformByKeyword(unit);
299
+
300
+ return {
301
+ instancePath: stripPointerPrefix(unit.instanceLocation),
302
+ schemaPath: unit.keywordLocation,
303
+ keyword: unit.keyword,
304
+ params,
305
+ message: message ?? unit.error,
306
+ };
307
+ }
308
+
309
+ /**
310
+ * Transform a cfworker `OutputUnit[]` into an AJV-compatible
311
+ * `ErrorObject[]`, with structural wrappers filtered out.
312
+ *
313
+ * Exported for unit testing.
314
+ */
315
+ export function transformErrors(units: OutputUnit[]): NoEvalErrorObject[] {
316
+ const out: NoEvalErrorObject[] = [];
317
+ for (const unit of units) {
318
+ const transformed = transformUnit(unit);
319
+ if (transformed !== null) out.push(transformed);
320
+ }
321
+ return out;
322
+ }
323
+
324
+ const FROZEN_OPTS: Readonly<Record<string, unknown>> = Object.freeze({
325
+ allErrors: true,
326
+ verbose: true,
327
+ errorDataPath: "",
328
+ strict: false,
329
+ });
330
+
331
+ /**
332
+ * Build a CSP-safe AJV-shaped validator backed by `@cfworker/json-schema`.
333
+ *
334
+ * Each `compile(schema)` call constructs a `Validator` and returns a
335
+ * synchronous validator function. The validator function's `errors`
336
+ * property is reassigned on every call, matching AJV's contract.
337
+ *
338
+ * Validators are memoized per-schema via a `WeakMap`, so JsonForms's
339
+ * repeated `compile()` calls on prop changes don't re-parse the schema
340
+ * graph each time.
341
+ */
342
+ export function createNoEvalAjv(
343
+ options: CreateNoEvalAjvOptions = {},
344
+ ): NoEvalAjv {
345
+ const draft: SchemaDraft = options.draft ?? "2020-12";
346
+ const cache = new WeakMap<object, NoEvalValidateFunction>();
347
+
348
+ const compile = (schema: unknown): NoEvalValidateFunction => {
349
+ if (schema && typeof schema === "object") {
350
+ const cached = cache.get(schema as object);
351
+ if (cached) return cached;
352
+ }
353
+
354
+ const validator = new Validator(schema as Schema | boolean, draft, false);
355
+
356
+ const fn: NoEvalValidateFunction = ((data: unknown) => {
357
+ const result = validator.validate(data);
358
+ fn.errors = result.valid ? null : transformErrors(result.errors);
359
+ return result.valid;
360
+ }) as NoEvalValidateFunction;
361
+
362
+ fn.errors = null;
363
+
364
+ if (schema && typeof schema === "object") {
365
+ cache.set(schema as object, fn);
366
+ }
367
+ return fn;
368
+ };
369
+
370
+ const facade: NoEvalAjv = {
371
+ compile,
372
+ addSchema: () => facade,
373
+ getSchema: () => undefined,
374
+ removeSchema: () => facade,
375
+ addFormat: () => facade,
376
+ addKeyword: () => facade,
377
+ opts: FROZEN_OPTS,
378
+ };
379
+
380
+ return facade;
381
+ }
@@ -3,6 +3,7 @@ import type { ControlElement, JsonSchema } from "@jsonforms/core";
3
3
  import { useJsonFormsControl } from "@jsonforms/vue";
4
4
  import { computed, ref, watch } from "vue";
5
5
  import { useProvider } from "../composables/useProvider";
6
+ import { useProjection } from "../composables/useProjection";
6
7
  import AutoComplete from "primevue/autocomplete";
7
8
 
8
9
  const props = defineProps<{
@@ -10,7 +11,12 @@ const props = defineProps<{
10
11
  schema: JsonSchema;
11
12
  path: string;
12
13
  }>();
13
- const { control, handleChange } = useJsonFormsControl(props);
14
+ const { control, handleChange: rawHandleChange } = useJsonFormsControl(props);
15
+ const {
16
+ projectedData,
17
+ projectedLabel,
18
+ handleProjectedChange: handleChange,
19
+ } = useProjection(control, rawHandleChange);
14
20
 
15
21
  const binding = computed(() => {
16
22
  const provider = control.value.uischema?.options?.provider;
@@ -33,7 +39,7 @@ watch(query, () => {
33
39
  });
34
40
 
35
41
  const value = computed({
36
- get: () => control.value.data,
42
+ get: () => projectedData.value,
37
43
  set: (v) => handleChange(control.value.path, v),
38
44
  });
39
45
 
@@ -51,11 +57,9 @@ const onSelect = (event: { value?: { value?: unknown } | unknown }) => {
51
57
  </script>
52
58
 
53
59
  <template>
54
- <div class="flex flex-col gap-1">
55
- <label v-if="control.schema.title" class="text-color text-left">{{
56
- control.schema.title
57
- }}</label>
58
- <div v-if="control.description" class="text-color-secondary text-left">
60
+ <div class="jf-control">
61
+ <label v-if="projectedLabel" class="jf-label">{{ projectedLabel }}</label>
62
+ <div v-if="control.description" class="jf-description">
59
63
  {{ control.description }}
60
64
  </div>
61
65
  <AutoComplete
@@ -3,7 +3,9 @@ import type { ControlElement, JsonSchema } from "@jsonforms/core";
3
3
  import { useJsonFormsControl } from "@jsonforms/vue";
4
4
  import { computed, inject, watch } from "vue";
5
5
  import { useProvider } from "../composables/useProvider";
6
+ import { useProjection } from "../composables/useProjection";
6
7
  import { shouldAutoSelectMulti } from "../utils/autoSelect";
8
+ import { resolvePlaceholder } from "../utils/placeholder";
7
9
  import MultiSelect from "primevue/multiselect";
8
10
 
9
11
  const props = defineProps<{
@@ -11,7 +13,12 @@ const props = defineProps<{
11
13
  schema: JsonSchema;
12
14
  path: string;
13
15
  }>();
14
- const { control, handleChange } = useJsonFormsControl(props);
16
+ const { control, handleChange: rawHandleChange } = useJsonFormsControl(props);
17
+ const {
18
+ projectedData,
19
+ projectedLabel,
20
+ handleProjectedChange: handleChange,
21
+ } = useProjection(control, rawHandleChange);
15
22
 
16
23
  const binding = computed(() => {
17
24
  const provider = control.value.uischema?.options?.provider;
@@ -39,7 +46,7 @@ const rootData = computed(() => injectedFormData.value || {});
39
46
  const { items, loading, error } = useProvider(binding, {
40
47
  data: rootData, // Pass the reactive reference
41
48
  path: control.value.path,
42
- dependsOnValues: depValues.value,
49
+ dependsOnValues: depValues,
43
50
  });
44
51
 
45
52
  // Provider will automatically reload when rootData changes due to reactive cache key
@@ -53,7 +60,9 @@ watch(
53
60
  control.value.uischema?.options?.autoSelectSingle === true,
54
61
  isLoading,
55
62
  items: newItems,
56
- currentValue: Array.isArray(control.value.data) ? control.value.data : [],
63
+ currentValue: Array.isArray(projectedData.value)
64
+ ? projectedData.value
65
+ : [],
57
66
  });
58
67
 
59
68
  if (valueToSelect !== null) {
@@ -74,33 +83,31 @@ const sameSet = (a: unknown[], b: unknown[]) => {
74
83
  // v-model with guard to avoid recursive updates
75
84
  const value = computed({
76
85
  get() {
77
- const curr = Array.isArray(control.value.data) ? control.value.data : [];
86
+ const curr = Array.isArray(projectedData.value) ? projectedData.value : [];
78
87
  // return a fresh copy so MultiSelect can't mutate JSONForms' array in place
79
88
  return [...curr];
80
89
  },
81
90
  set(val) {
82
91
  const next = Array.isArray(val) ? [...val] : [];
83
- const curr = Array.isArray(control.value.data) ? control.value.data : [];
92
+ const curr = Array.isArray(projectedData.value) ? projectedData.value : [];
84
93
  if (!sameSet(curr, next)) handleChange(control.value.path, next);
85
94
  },
86
95
  });
87
96
 
88
97
  const placeholder = computed(() => {
89
98
  if (loading.value) return "Loading…";
90
- // Check for placeholder in uischema options
91
- const uischemaPlaceholder = (
92
- control.value.uischema as { options?: { placeholder?: string } }
93
- )?.options?.placeholder;
94
- return uischemaPlaceholder || "Select…";
99
+ return resolvePlaceholder(
100
+ control.value.uischema,
101
+ projectedLabel.value,
102
+ "select",
103
+ );
95
104
  });
96
105
  </script>
97
106
 
98
107
  <template>
99
- <div class="flex flex-col gap-2">
100
- <label v-if="control.schema.title" class="text-color text-left">{{
101
- control.schema.title
102
- }}</label>
103
- <div v-if="control.description" class="text-color-secondary text-left">
108
+ <div class="jf-control">
109
+ <label v-if="projectedLabel" class="jf-label">{{ projectedLabel }}</label>
110
+ <div v-if="control.description" class="jf-description">
104
111
  {{ control.description }}
105
112
  </div>
106
113
  <MultiSelect
@@ -0,0 +1,169 @@
1
+ <script setup lang="ts">
2
+ import type { ControlElement, JsonSchema } from "@jsonforms/core";
3
+ import { useJsonFormsControl } from "@jsonforms/vue";
4
+ import { computed, inject, watch } from "vue";
5
+ import { useProvider } from "../composables/useProvider";
6
+ import { useProjection } from "../composables/useProjection";
7
+ import { resolvePlaceholder } from "../utils/placeholder";
8
+ import {
9
+ fromMultiSelectShape,
10
+ resolveItemsSchema,
11
+ resolveObjectKeys,
12
+ sameObjectSet,
13
+ toMultiSelectShape,
14
+ type MultiSelectOption,
15
+ type ObjectKeys,
16
+ } from "../utils/objectMultiSelect";
17
+ import MultiSelect from "primevue/multiselect";
18
+
19
+ const props = defineProps<{
20
+ uischema: ControlElement;
21
+ schema: JsonSchema;
22
+ path: string;
23
+ }>();
24
+ const { control, handleChange: rawHandleChange } = useJsonFormsControl(props);
25
+ const {
26
+ projectedData,
27
+ projectedLabel,
28
+ handleProjectedChange: handleChange,
29
+ } = useProjection(control, rawHandleChange);
30
+
31
+ // Pull root schema so item-schema $refs resolve against the consumer's $defs.
32
+ const jsonforms = inject<{
33
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
34
+ core?: { schema?: Record<string, any> };
35
+ } | null>("jsonforms", null);
36
+ const rootSchema = computed(
37
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
38
+ () => (jsonforms?.core?.schema ?? control.value.schema) as Record<string, any>,
39
+ );
40
+
41
+ const itemsSchema = computed(() =>
42
+ resolveItemsSchema(
43
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
44
+ control.value.schema as Record<string, any>,
45
+ rootSchema.value,
46
+ ),
47
+ );
48
+
49
+ const objectKeys = computed<ObjectKeys>(() => {
50
+ const resolved = resolveObjectKeys(
51
+ control.value.uischema?.options as
52
+ | { objectKeys?: { value?: unknown; label?: unknown } }
53
+ | undefined,
54
+ itemsSchema.value,
55
+ );
56
+ if (!resolved) {
57
+ throw new Error(
58
+ "[ProviderObjectMultiSelect] objectKeys could not be resolved. " +
59
+ "Specify `uischema.options.objectKeys = { value, label }` or declare " +
60
+ "exactly two `required` properties on the array's items schema so " +
61
+ "they can be inferred.",
62
+ );
63
+ }
64
+ return resolved;
65
+ });
66
+
67
+ const binding = computed(() => {
68
+ const provider = control.value.uischema?.options?.provider;
69
+ if (provider && typeof provider === "object" && !provider.load) {
70
+ return { ...provider, load: "mount" };
71
+ }
72
+ return provider;
73
+ });
74
+
75
+ const deps = computed(
76
+ () =>
77
+ ((
78
+ (control.value.schema as Record<string, unknown>)?.[
79
+ "x-provider"
80
+ ] as Record<string, unknown>
81
+ )?.dependsOn as string[]) ?? [],
82
+ );
83
+ const depValues = computed(() => deps.value.map(() => null));
84
+
85
+ const injectedFormData = inject<{ value: unknown }>("formData", { value: {} });
86
+ const rootData = computed(() => injectedFormData.value || {});
87
+
88
+ const { items, loading, error } = useProvider(binding, {
89
+ data: rootData,
90
+ path: control.value.path,
91
+ dependsOnValues: depValues,
92
+ });
93
+
94
+ // Auto-select when provider returns only one item — paired-object variant.
95
+ watch(
96
+ [items, loading],
97
+ ([newItems, isLoading]) => {
98
+ if (
99
+ !control.value.uischema?.options?.autoSelectSingle ||
100
+ isLoading ||
101
+ newItems.length !== 1
102
+ ) {
103
+ return;
104
+ }
105
+ const single = newItems[0]!;
106
+ const current = Array.isArray(projectedData.value) ? projectedData.value : [];
107
+ if (current.length === 0) {
108
+ handleChange(control.value.path, [
109
+ {
110
+ [objectKeys.value.value]: single.value,
111
+ [objectKeys.value.label]: single.label,
112
+ },
113
+ ]);
114
+ }
115
+ },
116
+ { immediate: true },
117
+ );
118
+
119
+ const value = computed<MultiSelectOption[]>({
120
+ get() {
121
+ return toMultiSelectShape(projectedData.value, objectKeys.value);
122
+ },
123
+ set(val) {
124
+ const next = fromMultiSelectShape(val, objectKeys.value);
125
+ const curr = Array.isArray(projectedData.value) ? projectedData.value : [];
126
+ if (!sameObjectSet(curr, next, objectKeys.value.value)) {
127
+ handleChange(control.value.path, next);
128
+ }
129
+ },
130
+ });
131
+
132
+ const placeholder = computed(() => {
133
+ if (loading.value) return "Loading…";
134
+ return resolvePlaceholder(
135
+ control.value.uischema,
136
+ projectedLabel.value,
137
+ "select",
138
+ );
139
+ });
140
+ </script>
141
+
142
+ <template>
143
+ <div class="jf-control">
144
+ <label v-if="projectedLabel" class="jf-label">{{ projectedLabel }}</label>
145
+ <div v-if="control.description" class="jf-description">
146
+ {{ control.description }}
147
+ </div>
148
+ <MultiSelect
149
+ v-model="value"
150
+ class="w-full!"
151
+ :options="items"
152
+ option-label="label"
153
+ data-key="value"
154
+ display="chip"
155
+ :placeholder="placeholder"
156
+ :disabled="!control.enabled || loading"
157
+ :show-clear="true"
158
+ />
159
+ <small v-if="error" class="p-error" role="alert"
160
+ >Failed to load: {{ error }}</small
161
+ >
162
+ </div>
163
+ </template>
164
+
165
+ <style scoped>
166
+ :deep(.p-multiselect-label) {
167
+ text-align: left;
168
+ }
169
+ </style>