@narrative.io/jsonforms-provider-protocols 2.11.0 → 3.0.0-beta.10

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 (110) hide show
  1. package/README.md +101 -29
  2. package/dist/core/initFormData.d.ts +10 -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 +32 -0
  7. package/dist/core/projection.d.ts.map +1 -0
  8. package/dist/core/projection.js +74 -0
  9. package/dist/core/projection.js.map +1 -0
  10. package/dist/core/resolveScope.d.ts +11 -0
  11. package/dist/core/resolveScope.d.ts.map +1 -0
  12. package/dist/core/resolveScope.js +22 -0
  13. package/dist/core/resolveScope.js.map +1 -0
  14. package/dist/core/transforms.d.ts +8 -10
  15. package/dist/core/transforms.d.ts.map +1 -1
  16. package/dist/core/transforms.js +58 -13
  17. package/dist/core/transforms.js.map +1 -1
  18. package/dist/core/types.d.ts +8 -0
  19. package/dist/core/types.d.ts.map +1 -1
  20. package/dist/index.d.ts +5 -1
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +14 -3
  23. package/dist/index.js.map +1 -1
  24. package/dist/jsonforms-provider-protocols.css +2 -2
  25. package/dist/vue/components/ProviderAutocomplete.vue.d.ts.map +1 -1
  26. package/dist/vue/components/ProviderAutocomplete.vue.js +10 -5
  27. package/dist/vue/components/ProviderAutocomplete.vue.js.map +1 -1
  28. package/dist/vue/components/ProviderMultiSelect.vue.d.ts.map +1 -1
  29. package/dist/vue/components/ProviderMultiSelect.vue.js +1 -1
  30. package/dist/vue/components/ProviderMultiSelect.vue2.js +12 -7
  31. package/dist/vue/components/ProviderMultiSelect.vue2.js.map +1 -1
  32. package/dist/vue/components/ProviderSelect.vue.d.ts.map +1 -1
  33. package/dist/vue/components/ProviderSelect.vue.js +1 -1
  34. package/dist/vue/components/ProviderSelect.vue2.js +13 -6
  35. package/dist/vue/components/ProviderSelect.vue2.js.map +1 -1
  36. package/dist/vue/composables/useDataLayer.d.ts +10 -0
  37. package/dist/vue/composables/useDataLayer.d.ts.map +1 -0
  38. package/dist/vue/composables/useDataLayer.js +26 -0
  39. package/dist/vue/composables/useDataLayer.js.map +1 -0
  40. package/dist/vue/composables/useDerive.d.ts +5 -2
  41. package/dist/vue/composables/useDerive.d.ts.map +1 -1
  42. package/dist/vue/composables/useDerive.js +29 -12
  43. package/dist/vue/composables/useDerive.js.map +1 -1
  44. package/dist/vue/composables/useDeriveInitialValue.d.ts +36 -0
  45. package/dist/vue/composables/useDeriveInitialValue.d.ts.map +1 -0
  46. package/dist/vue/composables/useDeriveInitialValue.js +125 -0
  47. package/dist/vue/composables/useDeriveInitialValue.js.map +1 -0
  48. package/dist/vue/composables/useDirtyValidation.d.ts +9 -0
  49. package/dist/vue/composables/useDirtyValidation.d.ts.map +1 -0
  50. package/dist/vue/composables/useDirtyValidation.js +15 -0
  51. package/dist/vue/composables/useDirtyValidation.js.map +1 -0
  52. package/dist/vue/composables/useProjection.d.ts +41 -0
  53. package/dist/vue/composables/useProjection.d.ts.map +1 -0
  54. package/dist/vue/composables/useProjection.js +84 -0
  55. package/dist/vue/composables/useProjection.js.map +1 -0
  56. package/dist/vue/index.d.ts +7 -0
  57. package/dist/vue/index.d.ts.map +1 -1
  58. package/dist/vue/index.js +35 -27
  59. package/dist/vue/index.js.map +1 -1
  60. package/dist/vue/primevue/JfBoolean.vue.d.ts +9 -0
  61. package/dist/vue/primevue/JfBoolean.vue.d.ts.map +1 -1
  62. package/dist/vue/primevue/JfBoolean.vue.js +35 -13
  63. package/dist/vue/primevue/JfBoolean.vue.js.map +1 -1
  64. package/dist/vue/primevue/JfEnum.vue.d.ts +9 -0
  65. package/dist/vue/primevue/JfEnum.vue.d.ts.map +1 -1
  66. package/dist/vue/primevue/JfEnum.vue.js +31 -22
  67. package/dist/vue/primevue/JfEnum.vue.js.map +1 -1
  68. package/dist/vue/primevue/JfEnumArray.vue.d.ts +9 -0
  69. package/dist/vue/primevue/JfEnumArray.vue.d.ts.map +1 -1
  70. package/dist/vue/primevue/JfEnumArray.vue.js +33 -18
  71. package/dist/vue/primevue/JfEnumArray.vue.js.map +1 -1
  72. package/dist/vue/primevue/JfNumber.vue.d.ts +9 -0
  73. package/dist/vue/primevue/JfNumber.vue.d.ts.map +1 -1
  74. package/dist/vue/primevue/JfNumber.vue.js +31 -22
  75. package/dist/vue/primevue/JfNumber.vue.js.map +1 -1
  76. package/dist/vue/primevue/JfText.vue.d.ts +9 -0
  77. package/dist/vue/primevue/JfText.vue.d.ts.map +1 -1
  78. package/dist/vue/primevue/JfText.vue.js +40 -32
  79. package/dist/vue/primevue/JfText.vue.js.map +1 -1
  80. package/dist/vue/primevue/JfTextArea.vue.d.ts +9 -0
  81. package/dist/vue/primevue/JfTextArea.vue.d.ts.map +1 -1
  82. package/dist/vue/primevue/JfTextArea.vue.js +32 -18
  83. package/dist/vue/primevue/JfTextArea.vue.js.map +1 -1
  84. package/dist/vue/primevue/index.d.ts.map +1 -1
  85. package/dist/vue/primevue/index.js +100 -8
  86. package/dist/vue/primevue/index.js.map +1 -1
  87. package/package.json +3 -1
  88. package/src/core/initFormData.ts +189 -0
  89. package/src/core/projection.ts +136 -0
  90. package/src/core/resolveScope.ts +39 -0
  91. package/src/core/transforms.ts +118 -26
  92. package/src/core/types.ts +9 -0
  93. package/src/index.ts +7 -1
  94. package/src/vue/components/ProviderAutocomplete.vue +10 -5
  95. package/src/vue/components/ProviderMultiSelect.vue +14 -7
  96. package/src/vue/components/ProviderSelect.vue +15 -6
  97. package/src/vue/composables/useDataLayer.ts +43 -0
  98. package/src/vue/composables/useDerive.ts +62 -16
  99. package/src/vue/composables/useDeriveInitialValue.ts +195 -0
  100. package/src/vue/composables/useDirtyValidation.ts +20 -0
  101. package/src/vue/composables/useProjection.ts +181 -0
  102. package/src/vue/index.ts +35 -41
  103. package/src/vue/primevue/JfBoolean.vue +25 -7
  104. package/src/vue/primevue/JfEnum.vue +29 -22
  105. package/src/vue/primevue/JfEnumArray.vue +31 -16
  106. package/src/vue/primevue/JfNumber.vue +29 -22
  107. package/src/vue/primevue/JfText.vue +34 -27
  108. package/src/vue/primevue/JfTextArea.vue +29 -17
  109. package/src/vue/primevue/index.ts +114 -8
  110. package/src/vue/styles.css +26 -1
@@ -0,0 +1,195 @@
1
+ import { computed, inject, ref, watch, type Ref, type ComputedRef } from "vue";
2
+ import { type ControlElement } from "@jsonforms/core";
3
+ import { renderTpl, renderObj } from "../../core/templating";
4
+ import { jp } from "../../core/jsonpath";
5
+ import {
6
+ applyTransformPipeline,
7
+ type TransformPipeline,
8
+ } from "../../core/transforms";
9
+ import type { AuthConfig } from "../../core/types";
10
+
11
+ export interface DeriveInitialValueCfg {
12
+ protocol: string;
13
+ config: {
14
+ url: string;
15
+ method?: "GET" | "POST";
16
+ headers?: Record<string, string>;
17
+ query?: Record<string, unknown>;
18
+ body?: unknown;
19
+ auth?: AuthConfig;
20
+ items: string;
21
+ map: { value: string };
22
+ transforms?: TransformPipeline;
23
+ showError?: boolean;
24
+ };
25
+ }
26
+
27
+ interface DeriveInitialValueOptions {
28
+ control: Ref<{
29
+ uischema: ControlElement;
30
+ path: string;
31
+ data: unknown;
32
+ }>;
33
+ handleChange: (path: string, value: unknown) => void;
34
+ data?: Ref<unknown> | ComputedRef<unknown>;
35
+ }
36
+
37
+ function buildAuthHeaders(
38
+ auth?: AuthConfig,
39
+ globalAuth?: Record<string, unknown>,
40
+ ): Record<string, string> {
41
+ const headers: Record<string, string> = {};
42
+ if (!auth) return headers;
43
+
44
+ if (auth.use && globalAuth?.[auth.use]) {
45
+ const globalValue = globalAuth[auth.use];
46
+ const value =
47
+ typeof globalValue === "function" ? globalValue() : globalValue;
48
+ if (auth.use === "apiKey") headers["X-API-Key"] = String(value);
49
+ else if (auth.use === "bearer")
50
+ headers["Authorization"] = `Bearer ${value}`;
51
+ else if (auth.use === "token") headers["Authorization"] = `Token ${value}`;
52
+ return headers;
53
+ }
54
+
55
+ if (auth.bearer) {
56
+ const v = typeof auth.bearer === "function" ? auth.bearer() : auth.bearer;
57
+ headers["Authorization"] = `Bearer ${v}`;
58
+ }
59
+ if (auth.apiKey) {
60
+ const v = typeof auth.apiKey === "function" ? auth.apiKey() : auth.apiKey;
61
+ headers["X-API-Key"] = String(v);
62
+ }
63
+ if (auth.token) {
64
+ const v = typeof auth.token === "function" ? auth.token() : auth.token;
65
+ headers["Authorization"] = `Token ${v}`;
66
+ }
67
+
68
+ return headers;
69
+ }
70
+
71
+ /**
72
+ * Returns true when the template URL contains `{{…}}` placeholders but
73
+ * one or more of those placeholders resolved to an empty string, which
74
+ * means a required data dependency hasn't been set yet.
75
+ */
76
+ function hasUnresolvedTemplates(
77
+ templateUrl: string,
78
+ renderedUrl: string,
79
+ ): boolean {
80
+ if (!templateUrl.includes("{{")) return false;
81
+ // After protocol, empty segments indicate unresolved vars
82
+ const pathPart = renderedUrl.replace(/^https?:\/\/[^/]+/, "");
83
+ if (pathPart.includes("//")) return true;
84
+ // Trailing slash when the template didn't have one
85
+ if (renderedUrl.endsWith("/") && !templateUrl.endsWith("/")) return true;
86
+ return false;
87
+ }
88
+
89
+ export function useDeriveInitialValue({
90
+ control,
91
+ handleChange,
92
+ }: DeriveInitialValueOptions) {
93
+ const injectedFormData = inject<{ value: unknown }>("formData", {
94
+ value: {},
95
+ });
96
+ const rootData = computed(() => injectedFormData.value || {});
97
+ const auth = inject("providerAuth", {}) as Record<string, unknown>;
98
+
99
+ const cfg = computed<DeriveInitialValueCfg | undefined>(() => {
100
+ return control.value.uischema?.options?.deriveInitialValue as
101
+ | DeriveInitialValueCfg
102
+ | undefined;
103
+ });
104
+
105
+ // Compute the resolved URL reactively
106
+ const resolvedUrl = computed<string | null>(() => {
107
+ const c = cfg.value;
108
+ if (!c?.config?.url) return null;
109
+ const rendered = renderTpl(c.config.url, { data: rootData.value });
110
+ if (hasUnresolvedTemplates(c.config.url, rendered)) return null;
111
+ return rendered;
112
+ });
113
+
114
+ const lastFetchedUrl = ref<string | null>(null);
115
+ const loading = ref(false);
116
+ const error = ref<string | undefined>(undefined);
117
+
118
+ watch(
119
+ resolvedUrl,
120
+ async (url) => {
121
+ if (!url || !cfg.value) return;
122
+ // Only fetch when the URL changes (new context).
123
+ // Same URL = same context; don't override user selection.
124
+ if (url === lastFetchedUrl.value) return;
125
+ lastFetchedUrl.value = url;
126
+
127
+ loading.value = true;
128
+ error.value = undefined;
129
+
130
+ try {
131
+ const c = cfg.value.config;
132
+ const fullUrl = new URL(url);
133
+
134
+ // Query params
135
+ const q = renderObj(c.query ?? {}, {
136
+ data: rootData.value,
137
+ }) as Record<string, unknown>;
138
+ for (const [k, v] of Object.entries(q)) {
139
+ if (v !== undefined && v !== "")
140
+ fullUrl.searchParams.set(k, String(v));
141
+ }
142
+
143
+ // Headers
144
+ const baseHeaders = renderObj(c.headers ?? {}, {
145
+ data: rootData.value,
146
+ }) as Record<string, string>;
147
+ const authHeaders = buildAuthHeaders(c.auth, auth);
148
+ const headers = { ...baseHeaders, ...authHeaders };
149
+
150
+ const method = c.method ?? "GET";
151
+ const requestInit: RequestInit = { method, headers };
152
+ if (method !== "GET" && c.body) {
153
+ requestInit.body = JSON.stringify(
154
+ renderObj(c.body, { data: rootData.value }),
155
+ );
156
+ }
157
+
158
+ const res = await fetch(fullUrl.toString(), requestInit);
159
+ if (!res.ok) {
160
+ if (c.showError !== false) {
161
+ throw new Error(`REST ${res.status}`);
162
+ }
163
+ return;
164
+ }
165
+
166
+ const json = await res.json();
167
+ let items = jp(json, c.items);
168
+
169
+ // Apply transforms if provided
170
+ if (c.transforms && c.transforms.length > 0) {
171
+ items = applyTransformPipeline(items, c.transforms);
172
+ }
173
+
174
+ if (items.length === 0) return; // No items → leave field empty
175
+
176
+ // Extract value from first item
177
+ const derivedValue = jp(items[0], c.map.value)[0];
178
+ if (derivedValue !== undefined) {
179
+ handleChange(control.value.path, derivedValue);
180
+ }
181
+ } catch (e) {
182
+ error.value = (e as Error)?.message ?? String(e);
183
+ console.warn(
184
+ `deriveInitialValue fetch failed for ${control.value.path}:`,
185
+ e,
186
+ );
187
+ } finally {
188
+ loading.value = false;
189
+ }
190
+ },
191
+ { immediate: true },
192
+ );
193
+
194
+ return { loading, error };
195
+ }
@@ -0,0 +1,20 @@
1
+ import { ref, computed, type Ref, type ComputedRef } from "vue";
2
+
3
+ export function useDirtyValidation(
4
+ control: Ref<{ errors: string }>,
5
+ errorsOverride?: Ref<string> | ComputedRef<string>,
6
+ ) {
7
+ const hasInteracted = ref(false);
8
+
9
+ const showErrors = computed(
10
+ () =>
11
+ hasInteracted.value &&
12
+ !!(errorsOverride ? errorsOverride.value : control.value.errors),
13
+ );
14
+
15
+ const markDirty = () => {
16
+ hasInteracted.value = true;
17
+ };
18
+
19
+ return { hasInteracted, showErrors, markDirty };
20
+ }
@@ -0,0 +1,181 @@
1
+ import { computed, inject, 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
+ errors: string;
12
+ label?: string;
13
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
14
+ schema: Record<string, any>;
15
+ uischema: { options?: { projection?: string; [key: string]: unknown } };
16
+ }
17
+
18
+ // Minimal AJV ErrorObject shape for filtering
19
+ interface ErrorLike {
20
+ instancePath?: string;
21
+ keyword?: string;
22
+ message?: string;
23
+ params?: { missingProperty?: string };
24
+ }
25
+
26
+ /**
27
+ * Resolve the display label for a control.
28
+ * Priority: uischema options.label → schemaTitle (projected sub-schema) → control.label.
29
+ */
30
+ function resolveLabel(ctrl: ProjectionControl, schemaTitle?: string): string {
31
+ return (
32
+ (ctrl.uischema?.options?.label as string) ?? schemaTitle ?? ctrl.label ?? ""
33
+ );
34
+ }
35
+
36
+ /**
37
+ * Normalize AJV error message fragments into user-friendly text.
38
+ * e.g. "is a required property" → "is required"
39
+ */
40
+ function normalizeErrors(errors: string): string {
41
+ if (!errors) return errors;
42
+ return errors
43
+ .replace(/is a required property/g, "is required")
44
+ .replace(/must have required property '[^']*'/g, "is required");
45
+ }
46
+
47
+ /**
48
+ * Prefix each error message line with the field label so that AJV fragments
49
+ * like "is required" become "Name is required".
50
+ */
51
+ function prefixErrors(label: string, errors: string): string {
52
+ if (!label || !errors) return errors;
53
+ return errors
54
+ .split("\n")
55
+ .map((line) => `${label} ${line}`)
56
+ .join("\n");
57
+ }
58
+
59
+ /**
60
+ * Convert an AJV ErrorObject's instancePath to a dot-separated control path.
61
+ * Replicates the logic from @jsonforms/core getControlPath.
62
+ */
63
+ function getErrorPath(error: ErrorLike): string {
64
+ let p = (error.instancePath || "").replace(/\//g, ".").replace(/^\./, "");
65
+ if (error.keyword === "required" && error.params?.missingProperty) {
66
+ p = p
67
+ ? p + "." + error.params.missingProperty
68
+ : error.params.missingProperty;
69
+ }
70
+ return p;
71
+ }
72
+
73
+ export interface ProjectionResult {
74
+ /** The value at the projected path (for rendering) */
75
+ projectedData: ComputedRef<unknown>;
76
+ /** The schema at the projected path (for renderer selection) */
77
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
78
+ projectedSchema: ComputedRef<Record<string, any>>;
79
+ /** Wrapped handleChange that writes through the projection */
80
+ handleProjectedChange: (path: string, value: unknown) => void;
81
+ /** Whether projection is active */
82
+ hasProjection: boolean;
83
+ /** Resolved display label (options.label → projected schema title → control.label) */
84
+ projectedLabel: ComputedRef<string>;
85
+ /** Error string combining base-path and projected sub-path errors */
86
+ projectedErrors: ComputedRef<string>;
87
+ }
88
+
89
+ /**
90
+ * Composable that wraps a JSON Forms control with projection support.
91
+ *
92
+ * When `options.projection` is set on the uischema, this composable:
93
+ * - Reads the projected sub-value from the control data
94
+ * - Resolves the projected sub-schema for renderer type resolution
95
+ * - Wraps handleChange to write back through the projection path (preserving siblings)
96
+ *
97
+ * When no projection is set, it passes through control data/schema/handleChange unchanged.
98
+ */
99
+ export function useProjection(
100
+ control: Ref<ProjectionControl>,
101
+ handleChange: (path: string, value: unknown) => void,
102
+ ): ProjectionResult {
103
+ const projection = control.value.uischema?.options?.projection as
104
+ | string
105
+ | undefined;
106
+
107
+ if (!projection) {
108
+ const label = computed(() => resolveLabel(control.value));
109
+ return {
110
+ projectedData: computed(() => control.value.data),
111
+ projectedSchema: computed(() => control.value.schema),
112
+ handleProjectedChange: handleChange,
113
+ hasProjection: false,
114
+ projectedLabel: label,
115
+ projectedErrors: computed(() =>
116
+ prefixErrors(
117
+ label.value.replace(/\*$/, "").trim(),
118
+ normalizeErrors(control.value.errors),
119
+ ),
120
+ ),
121
+ };
122
+ }
123
+
124
+ // Inject JSONForms state to access raw AJV errors for projected sub-paths.
125
+ // control.errors only contains errors at the exact control path (e.g. "data_rates"),
126
+ // but projected fields need errors at the full path (e.g. "data_rates.0.video_rate_usd").
127
+ const jsonforms = inject<{ core?: { errors?: ErrorLike[] } } | null>(
128
+ "jsonforms",
129
+ null,
130
+ );
131
+
132
+ const fullProjectedPath = control.value.path + "." + projection;
133
+
134
+ const projectedData = computed(() =>
135
+ getProjectedValue(control.value.data, projection),
136
+ );
137
+
138
+ const projectedSchema = computed(() =>
139
+ getProjectedSchema(control.value.schema, projection),
140
+ );
141
+
142
+ const label = computed(() =>
143
+ resolveLabel(control.value, projectedSchema.value?.title),
144
+ );
145
+
146
+ const projectedErrors = computed(() => {
147
+ const baseErrors = normalizeErrors(control.value.errors || "");
148
+
149
+ const rawErrors = jsonforms?.core?.errors ?? [];
150
+ const matching = rawErrors.filter(
151
+ (err) => getErrorPath(err) === fullProjectedPath,
152
+ );
153
+
154
+ let errStr: string;
155
+ if (matching.length === 0) {
156
+ errStr = baseErrors;
157
+ } else {
158
+ const projMsg = matching
159
+ .map((e) => (e.keyword === "required" ? "is required" : e.message))
160
+ .filter(Boolean)
161
+ .join("\n");
162
+ errStr = [baseErrors, projMsg].filter(Boolean).join("\n");
163
+ }
164
+
165
+ return prefixErrors(label.value.replace(/\*$/, "").trim(), errStr);
166
+ });
167
+
168
+ const handleProjectedChange = (path: string, value: unknown) => {
169
+ const fullValue = setProjectedValue(control.value.data, projection, value);
170
+ handleChange(path, fullValue);
171
+ };
172
+
173
+ return {
174
+ projectedData,
175
+ projectedSchema,
176
+ handleProjectedChange,
177
+ hasProjection: true,
178
+ projectedLabel: label,
179
+ projectedErrors,
180
+ };
181
+ }
package/src/vue/index.ts CHANGED
@@ -1,12 +1,14 @@
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";
10
12
  import ProviderAutocomplete from "./components/ProviderAutocomplete.vue";
11
13
  import ProviderSelect from "./components/ProviderSelect.vue";
12
14
  import ProviderMultiSelect from "./components/ProviderMultiSelect.vue";
@@ -16,28 +18,28 @@ const hasProvider = (uischema: UISchemaElement) => {
16
18
  return uischema?.options?.provider !== undefined;
17
19
  };
18
20
 
21
+ // Integer fallback tester — handles nested scopes like #/properties/parent/properties/child
22
+ const isIntegerScope = (uischema: unknown, schema: unknown) => {
23
+ const ui = uischema as { type?: string; scope?: string };
24
+ if (ui?.type !== "Control" || !ui?.scope) return false;
25
+
26
+ const propertySchema = resolveScopeSchema(
27
+ ui.scope,
28
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
29
+ schema as Record<string, any>,
30
+ );
31
+ return propertySchema?.type === "integer";
32
+ };
33
+
19
34
  // Create specific testers for each component type
20
35
  const providerSelectTester = rankWith(
21
- 106, // Higher than PrimeVue base (100) to ensure providers take precedence
36
+ 107, // Higher than PrimeVue JfNumber integer renderer (106) so provider wins
22
37
  and(
23
38
  or(
24
39
  isStringControl,
25
40
  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
+ isIntegerControl,
42
+ and(isControl, isIntegerScope),
41
43
  ),
42
44
  hasProvider,
43
45
  (uischema) => !uischema?.options?.autocomplete,
@@ -45,51 +47,36 @@ const providerSelectTester = rankWith(
45
47
  );
46
48
 
47
49
  const providerAutocompleteTester = rankWith(
48
- 107, // Higher than PrimeVue base (100) to ensure providers take precedence
50
+ 108, // Higher than providerSelectTester so autocomplete variant wins when flagged
49
51
  and(
50
52
  or(
51
53
  isStringControl,
52
54
  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
- }),
55
+ isIntegerControl,
56
+ and(isControl, isIntegerScope),
68
57
  ),
69
58
  hasProvider,
70
59
  (uischema) => uischema?.options?.autocomplete === true,
71
60
  ),
72
61
  );
73
62
 
74
- // Custom array tester - check both uischema control type and schema type
63
+ // Custom array tester - supports nested scope paths
75
64
  const isArrayControl = (uischema: UISchemaElement, schema: unknown) => {
76
65
  const controlSchema = uischema as { type: string; scope?: string };
77
66
  if (controlSchema.type !== "Control" || !controlSchema.scope) {
78
67
  return false;
79
68
  }
80
69
 
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
-
70
+ const propertySchema = resolveScopeSchema(
71
+ controlSchema.scope,
72
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
73
+ schema as Record<string, any>,
74
+ );
88
75
  return propertySchema?.type === "array";
89
76
  };
90
77
 
91
78
  const providerMultiSelectTester = rankWith(
92
- 108, // Highest priority for array controls with providers
79
+ 109, // Highest priority for array controls with providers
93
80
  and(isArrayControl, hasProvider),
94
81
  );
95
82
 
@@ -105,6 +92,13 @@ export { primevueRenderers, registerPrimevueRenderers } from "./primevue";
105
92
  // Export individual components
106
93
  export { ProviderAutocomplete, ProviderSelect, ProviderMultiSelect };
107
94
  export { useProvider } from "./composables/useProvider";
95
+ export { useProjection } from "./composables/useProjection";
96
+ export type { ProjectionResult } from "./composables/useProjection";
97
+ export { createDataLayer, useDataLayer } from "./composables/useDataLayer";
98
+ export type { DataLayer } from "./composables/useDataLayer";
99
+ export { useDeriveInitialValue } from "./composables/useDeriveInitialValue";
100
+ export type { DeriveInitialValueCfg } from "./composables/useDeriveInitialValue";
101
+ export { useDirtyValidation } from "./composables/useDirtyValidation";
108
102
  export * from "./testers";
109
103
 
110
104
  // 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,41 @@ 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="flex items-center gap-2">
69
+ <div class="jf-control" style="flex-direction: row; align-items: center">
53
70
  <Checkbox
54
71
  :binary="true"
55
- :model-value="!!control.data"
72
+ :model-value="!!projectedData"
56
73
  :disabled="!control.enabled"
57
- :aria-invalid="!!control.errors || undefined"
74
+ :class="{ 'p-invalid': showErrors }"
75
+ :aria-invalid="showErrors || undefined"
58
76
  @update:model-value="onToggle"
59
77
  />
60
- <label v-if="control.label">{{ control.label }}</label>
61
- <small v-if="control.errors" class="p-error">{{ control.errors }}</small>
78
+ <label v-if="projectedLabel" class="jf-label">{{ projectedLabel }}</label>
79
+ <small v-if="showErrors" class="p-error">{{ projectedErrors }}</small>
62
80
  </div>
63
81
  </template>