@narrative.io/jsonforms-provider-protocols 3.0.0-beta.1 → 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 (97) hide show
  1. package/dist/core/initFormData.d.ts +10 -0
  2. package/dist/core/initFormData.d.ts.map +1 -0
  3. package/dist/core/initFormData.js +99 -0
  4. package/dist/core/initFormData.js.map +1 -0
  5. package/dist/core/projection.d.ts.map +1 -1
  6. package/dist/core/projection.js.map +1 -1
  7. package/dist/core/transforms.d.ts.map +1 -1
  8. package/dist/core/transforms.js +3 -1
  9. package/dist/core/transforms.js.map +1 -1
  10. package/dist/core/types.d.ts +1 -0
  11. package/dist/core/types.d.ts.map +1 -1
  12. package/dist/index.d.ts +1 -0
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +2 -0
  15. package/dist/index.js.map +1 -1
  16. package/dist/jsonforms-provider-protocols.css +2 -2
  17. package/dist/vue/components/ProviderAutocomplete.vue.d.ts.map +1 -1
  18. package/dist/vue/components/ProviderAutocomplete.vue.js +8 -5
  19. package/dist/vue/components/ProviderAutocomplete.vue.js.map +1 -1
  20. package/dist/vue/components/ProviderMultiSelect.vue.d.ts.map +1 -1
  21. package/dist/vue/components/ProviderMultiSelect.vue.js +1 -1
  22. package/dist/vue/components/ProviderMultiSelect.vue2.js +8 -5
  23. package/dist/vue/components/ProviderMultiSelect.vue2.js.map +1 -1
  24. package/dist/vue/components/ProviderSelect.vue.d.ts.map +1 -1
  25. package/dist/vue/components/ProviderSelect.vue.js +1 -1
  26. package/dist/vue/components/ProviderSelect.vue2.js +10 -5
  27. package/dist/vue/components/ProviderSelect.vue2.js.map +1 -1
  28. package/dist/vue/composables/useDataLayer.d.ts +1 -0
  29. package/dist/vue/composables/useDataLayer.d.ts.map +1 -1
  30. package/dist/vue/composables/useDataLayer.js +1 -0
  31. package/dist/vue/composables/useDataLayer.js.map +1 -1
  32. package/dist/vue/composables/useDerive.d.ts +1 -1
  33. package/dist/vue/composables/useDerive.d.ts.map +1 -1
  34. package/dist/vue/composables/useDerive.js +19 -2
  35. package/dist/vue/composables/useDerive.js.map +1 -1
  36. package/dist/vue/composables/useDeriveInitialValue.d.ts +36 -0
  37. package/dist/vue/composables/useDeriveInitialValue.d.ts.map +1 -0
  38. package/dist/vue/composables/useDeriveInitialValue.js +125 -0
  39. package/dist/vue/composables/useDeriveInitialValue.js.map +1 -0
  40. package/dist/vue/composables/useDirtyValidation.d.ts +9 -0
  41. package/dist/vue/composables/useDirtyValidation.d.ts.map +1 -0
  42. package/dist/vue/composables/useDirtyValidation.js +15 -0
  43. package/dist/vue/composables/useDirtyValidation.js.map +1 -0
  44. package/dist/vue/composables/useProjection.d.ts +6 -0
  45. package/dist/vue/composables/useProjection.d.ts.map +1 -1
  46. package/dist/vue/composables/useProjection.js +54 -3
  47. package/dist/vue/composables/useProjection.js.map +1 -1
  48. package/dist/vue/index.d.ts +3 -0
  49. package/dist/vue/index.d.ts.map +1 -1
  50. package/dist/vue/index.js +32 -10
  51. package/dist/vue/index.js.map +1 -1
  52. package/dist/vue/primevue/JfBoolean.vue.d.ts.map +1 -1
  53. package/dist/vue/primevue/JfBoolean.vue.js +26 -9
  54. package/dist/vue/primevue/JfBoolean.vue.js.map +1 -1
  55. package/dist/vue/primevue/JfEnum.vue.d.ts.map +1 -1
  56. package/dist/vue/primevue/JfEnum.vue.js +20 -16
  57. package/dist/vue/primevue/JfEnum.vue.js.map +1 -1
  58. package/dist/vue/primevue/JfEnumArray.vue.d.ts.map +1 -1
  59. package/dist/vue/primevue/JfEnumArray.vue.js +21 -11
  60. package/dist/vue/primevue/JfEnumArray.vue.js.map +1 -1
  61. package/dist/vue/primevue/JfNumber.vue.d.ts.map +1 -1
  62. package/dist/vue/primevue/JfNumber.vue.js +21 -17
  63. package/dist/vue/primevue/JfNumber.vue.js.map +1 -1
  64. package/dist/vue/primevue/JfText.vue.d.ts.map +1 -1
  65. package/dist/vue/primevue/JfText.vue.js +28 -25
  66. package/dist/vue/primevue/JfText.vue.js.map +1 -1
  67. package/dist/vue/primevue/JfTextArea.vue.d.ts.map +1 -1
  68. package/dist/vue/primevue/JfTextArea.vue.js +22 -13
  69. package/dist/vue/primevue/JfTextArea.vue.js.map +1 -1
  70. package/dist/vue/primevue/index.d.ts.map +1 -1
  71. package/dist/vue/primevue/index.js +93 -16
  72. package/dist/vue/primevue/index.js.map +1 -1
  73. package/dist/vue/utils/autoSelect.js.map +1 -1
  74. package/package.json +3 -1
  75. package/src/core/initFormData.ts +189 -0
  76. package/src/core/projection.ts +5 -5
  77. package/src/core/transforms.ts +33 -6
  78. package/src/core/types.ts +1 -0
  79. package/src/index.ts +1 -0
  80. package/src/vue/components/ProviderAutocomplete.vue +8 -5
  81. package/src/vue/components/ProviderMultiSelect.vue +12 -7
  82. package/src/vue/components/ProviderSelect.vue +13 -6
  83. package/src/vue/composables/useDataLayer.ts +1 -1
  84. package/src/vue/composables/useDerive.ts +46 -3
  85. package/src/vue/composables/useDeriveInitialValue.ts +195 -0
  86. package/src/vue/composables/useDirtyValidation.ts +20 -0
  87. package/src/vue/composables/useProjection.ts +108 -1
  88. package/src/vue/index.ts +29 -9
  89. package/src/vue/primevue/JfBoolean.vue +19 -6
  90. package/src/vue/primevue/JfEnum.vue +22 -20
  91. package/src/vue/primevue/JfEnumArray.vue +24 -14
  92. package/src/vue/primevue/JfNumber.vue +22 -20
  93. package/src/vue/primevue/JfText.vue +25 -23
  94. package/src/vue/primevue/JfTextArea.vue +22 -15
  95. package/src/vue/primevue/index.ts +104 -23
  96. package/src/vue/styles.css +26 -1
  97. package/src/vue/utils/autoSelect.ts +2 -2
@@ -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
+ }
@@ -1,4 +1,4 @@
1
- import { computed, type ComputedRef, type Ref } from "vue";
1
+ import { computed, inject, type ComputedRef, type Ref } from "vue";
2
2
  import {
3
3
  getProjectedValue,
4
4
  setProjectedValue,
@@ -8,11 +8,68 @@ import {
8
8
  interface ProjectionControl {
9
9
  data: unknown;
10
10
  path: string;
11
+ errors: string;
12
+ label?: string;
11
13
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
12
14
  schema: Record<string, any>;
13
15
  uischema: { options?: { projection?: string; [key: string]: unknown } };
14
16
  }
15
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
+
16
73
  export interface ProjectionResult {
17
74
  /** The value at the projected path (for rendering) */
18
75
  projectedData: ComputedRef<unknown>;
@@ -23,6 +80,10 @@ export interface ProjectionResult {
23
80
  handleProjectedChange: (path: string, value: unknown) => void;
24
81
  /** Whether projection is active */
25
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>;
26
87
  }
27
88
 
28
89
  /**
@@ -44,14 +105,32 @@ export function useProjection(
44
105
  | undefined;
45
106
 
46
107
  if (!projection) {
108
+ const label = computed(() => resolveLabel(control.value));
47
109
  return {
48
110
  projectedData: computed(() => control.value.data),
49
111
  projectedSchema: computed(() => control.value.schema),
50
112
  handleProjectedChange: handleChange,
51
113
  hasProjection: false,
114
+ projectedLabel: label,
115
+ projectedErrors: computed(() =>
116
+ prefixErrors(
117
+ label.value.replace(/\*$/, "").trim(),
118
+ normalizeErrors(control.value.errors),
119
+ ),
120
+ ),
52
121
  };
53
122
  }
54
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
+
55
134
  const projectedData = computed(() =>
56
135
  getProjectedValue(control.value.data, projection),
57
136
  );
@@ -60,6 +139,32 @@ export function useProjection(
60
139
  getProjectedSchema(control.value.schema, projection),
61
140
  );
62
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
+
63
168
  const handleProjectedChange = (path: string, value: unknown) => {
64
169
  const fullValue = setProjectedValue(control.value.data, projection, value);
65
170
  handleChange(path, fullValue);
@@ -70,5 +175,7 @@ export function useProjection(
70
175
  projectedSchema,
71
176
  handleProjectedChange,
72
177
  hasProjection: true,
178
+ projectedLabel: label,
179
+ projectedErrors,
73
180
  };
74
181
  }
package/src/vue/index.ts CHANGED
@@ -1,6 +1,7 @@
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,
@@ -22,25 +23,38 @@ const isIntegerScope = (uischema: unknown, schema: unknown) => {
22
23
  const ui = uischema as { type?: string; scope?: string };
23
24
  if (ui?.type !== "Control" || !ui?.scope) return false;
24
25
 
25
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
26
- const propertySchema = resolveScopeSchema(ui.scope, schema as Record<string, any>);
26
+ const propertySchema = resolveScopeSchema(
27
+ ui.scope,
28
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
29
+ schema as Record<string, any>,
30
+ );
27
31
  return propertySchema?.type === "integer";
28
32
  };
29
33
 
30
34
  // Create specific testers for each component type
31
35
  const providerSelectTester = rankWith(
32
- 106, // Higher than PrimeVue base (100) to ensure providers take precedence
36
+ 107, // Higher than PrimeVue JfNumber integer renderer (106) so provider wins
33
37
  and(
34
- or(isStringControl, isNumberControl, and(isControl, isIntegerScope)),
38
+ or(
39
+ isStringControl,
40
+ isNumberControl,
41
+ isIntegerControl,
42
+ and(isControl, isIntegerScope),
43
+ ),
35
44
  hasProvider,
36
45
  (uischema) => !uischema?.options?.autocomplete,
37
46
  ),
38
47
  );
39
48
 
40
49
  const providerAutocompleteTester = rankWith(
41
- 107, // Higher than PrimeVue base (100) to ensure providers take precedence
50
+ 108, // Higher than providerSelectTester so autocomplete variant wins when flagged
42
51
  and(
43
- or(isStringControl, isNumberControl, and(isControl, isIntegerScope)),
52
+ or(
53
+ isStringControl,
54
+ isNumberControl,
55
+ isIntegerControl,
56
+ and(isControl, isIntegerScope),
57
+ ),
44
58
  hasProvider,
45
59
  (uischema) => uischema?.options?.autocomplete === true,
46
60
  ),
@@ -53,13 +67,16 @@ const isArrayControl = (uischema: UISchemaElement, schema: unknown) => {
53
67
  return false;
54
68
  }
55
69
 
56
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
57
- const propertySchema = resolveScopeSchema(controlSchema.scope, schema as Record<string, any>);
70
+ const propertySchema = resolveScopeSchema(
71
+ controlSchema.scope,
72
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
73
+ schema as Record<string, any>,
74
+ );
58
75
  return propertySchema?.type === "array";
59
76
  };
60
77
 
61
78
  const providerMultiSelectTester = rankWith(
62
- 108, // Highest priority for array controls with providers
79
+ 109, // Highest priority for array controls with providers
63
80
  and(isArrayControl, hasProvider),
64
81
  );
65
82
 
@@ -79,6 +96,9 @@ export { useProjection } from "./composables/useProjection";
79
96
  export type { ProjectionResult } from "./composables/useProjection";
80
97
  export { createDataLayer, useDataLayer } from "./composables/useDataLayer";
81
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";
82
102
  export * from "./testers";
83
103
 
84
104
  // Export individual PrimeVue components using lazy evaluation to avoid circular deps
@@ -42,27 +42,40 @@ import type { ControlProps } from "@jsonforms/vue";
42
42
  import { useJsonFormsControl } from "@jsonforms/vue";
43
43
  import { getCurrentInstance } from "vue";
44
44
  import { useProjection } from "../composables/useProjection";
45
+ import { useDirtyValidation } from "../composables/useDirtyValidation";
45
46
  import Checkbox from "primevue/checkbox";
46
47
 
47
48
  // Access props from the component instance
48
49
  const instance = getCurrentInstance()!;
49
50
  const props = instance.props as unknown as ControlProps;
50
51
  const { control, handleChange: rawHandleChange } = useJsonFormsControl(props);
51
- const { projectedData, handleProjectedChange: handleChange } = useProjection(control, rawHandleChange);
52
+ const {
53
+ projectedData,
54
+ handleProjectedChange: handleChange,
55
+ projectedErrors,
56
+ projectedLabel,
57
+ } = useProjection(control, rawHandleChange);
52
58
 
53
- 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
+ };
54
66
  </script>
55
67
 
56
68
  <template>
57
- <div class="flex items-center gap-2">
69
+ <div class="jf-control" style="flex-direction: row; align-items: center">
58
70
  <Checkbox
59
71
  :binary="true"
60
72
  :model-value="!!projectedData"
61
73
  :disabled="!control.enabled"
62
- :aria-invalid="!!control.errors || undefined"
74
+ :class="{ 'p-invalid': showErrors }"
75
+ :aria-invalid="showErrors || undefined"
63
76
  @update:model-value="onToggle"
64
77
  />
65
- <label v-if="control.label">{{ control.label }}</label>
66
- <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>
67
80
  </div>
68
81
  </template>
@@ -42,10 +42,12 @@ export default {
42
42
  import type { JsonSchema } from "@jsonforms/core";
43
43
  import type { ControlProps } from "@jsonforms/vue";
44
44
  import { useJsonFormsControl } from "@jsonforms/vue";
45
- import { computed, ref, inject, getCurrentInstance, watch } from "vue";
45
+ import { computed, inject, getCurrentInstance, watch } from "vue";
46
46
  import { useProvider } from "../composables/useProvider";
47
47
  import { useDerive } from "../composables/useDerive";
48
+ import { useDeriveInitialValue } from "../composables/useDeriveInitialValue";
48
49
  import { useProjection } from "../composables/useProjection";
50
+ import { useDirtyValidation } from "../composables/useDirtyValidation";
49
51
  import { shouldAutoSelect } from "../utils/autoSelect";
50
52
  import Dropdown from "primevue/dropdown";
51
53
 
@@ -53,7 +55,12 @@ import Dropdown from "primevue/dropdown";
53
55
  const instance = getCurrentInstance()!;
54
56
  const props = instance.props as unknown as ControlProps;
55
57
  const { control, handleChange: rawHandleChange } = useJsonFormsControl(props);
56
- const { projectedData, handleProjectedChange: handleChange } = useProjection(control, rawHandleChange);
58
+ const {
59
+ projectedData,
60
+ handleProjectedChange: handleChange,
61
+ projectedErrors,
62
+ projectedLabel,
63
+ } = useProjection(control, rawHandleChange);
57
64
 
58
65
  type Opt = { label: string; value: unknown };
59
66
  const toOptions = (schema?: JsonSchema): Opt[] => {
@@ -141,6 +148,9 @@ const options = computed(() => {
141
148
  // Add derive functionality
142
149
  useDerive({ control, handleChange, data: projectedData });
143
150
 
151
+ // Add deriveInitialValue — async API-based initial value seeding
152
+ useDeriveInitialValue({ control, handleChange });
153
+
144
154
  // Auto-select when provider returns only one item (enabled by default)
145
155
  watch(
146
156
  [providerItems, loading],
@@ -157,47 +167,39 @@ watch(
157
167
  handleChange(control.value.path, valueToSelect);
158
168
  }
159
169
  },
160
- { immediate: true }
170
+ { immediate: true },
161
171
  );
162
172
 
163
- // Track user interaction
164
- const hasInteracted = ref(false);
165
-
166
- const showErrors = computed(() => hasInteracted.value && control.value.errors);
173
+ // Track user interaction — errors only show after blur
174
+ const { showErrors, markDirty } = useDirtyValidation(control, projectedErrors);
167
175
 
168
176
  const onSelect = (val: unknown) => {
169
177
  handleChange(control.value.path, val);
170
178
  };
171
-
172
- const onBlur = () => {
173
- hasInteracted.value = true;
174
- };
175
179
  </script>
176
180
 
177
181
  <template>
178
- <div class="flex flex-column gap-2">
179
- <label v-if="control.label" class="text-color text-left">{{
180
- control.label
181
- }}</label>
182
- <div v-if="control.description" class="text-color-secondary text-left">
182
+ <div class="jf-control">
183
+ <label v-if="projectedLabel" class="jf-label">{{ projectedLabel }}</label>
184
+ <div v-if="control.description" class="jf-description">
183
185
  {{ control.description }}
184
186
  </div>
185
187
  <Dropdown
186
- class="w-full"
188
+ :class="['w-full!', { 'p-invalid': showErrors }]"
187
189
  :options="options"
188
190
  option-label="label"
189
191
  option-value="value"
190
192
  :model-value="projectedData ?? null"
191
193
  :placeholder="placeholder"
192
194
  :disabled="!control.enabled || loading"
193
- :aria-invalid="!!showErrors || undefined"
195
+ :aria-invalid="showErrors || undefined"
194
196
  :show-clear="true"
195
197
  @update:model-value="onSelect"
196
- @blur="onBlur"
198
+ @blur="markDirty"
197
199
  />
198
200
  <small v-if="error" class="p-error" role="alert"
199
201
  >Failed to load: {{ error }}</small
200
202
  >
201
- <small v-else-if="showErrors" class="p-error">{{ control.errors }}</small>
203
+ <small v-else-if="showErrors" class="p-error">{{ projectedErrors }}</small>
202
204
  </div>
203
205
  </template>