@narrative.io/jsonforms-provider-protocols 3.0.0-beta.1 → 3.0.0-beta.11
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.
- package/dist/core/initFormData.d.ts +10 -0
- package/dist/core/initFormData.d.ts.map +1 -0
- package/dist/core/initFormData.js +99 -0
- package/dist/core/initFormData.js.map +1 -0
- package/dist/core/projection.d.ts.map +1 -1
- package/dist/core/projection.js.map +1 -1
- package/dist/core/transforms.d.ts.map +1 -1
- package/dist/core/transforms.js +3 -1
- package/dist/core/transforms.js.map +1 -1
- package/dist/core/types.d.ts +1 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/jsonforms-provider-protocols.css +2 -2
- package/dist/vue/components/ProviderAutocomplete.vue.d.ts.map +1 -1
- package/dist/vue/components/ProviderAutocomplete.vue.js +8 -5
- package/dist/vue/components/ProviderAutocomplete.vue.js.map +1 -1
- package/dist/vue/components/ProviderMultiSelect.vue.d.ts.map +1 -1
- package/dist/vue/components/ProviderMultiSelect.vue.js +1 -1
- package/dist/vue/components/ProviderMultiSelect.vue2.js +9 -6
- package/dist/vue/components/ProviderMultiSelect.vue2.js.map +1 -1
- package/dist/vue/components/ProviderSelect.vue.d.ts.map +1 -1
- package/dist/vue/components/ProviderSelect.vue.js +1 -1
- package/dist/vue/components/ProviderSelect.vue2.js +11 -6
- package/dist/vue/components/ProviderSelect.vue2.js.map +1 -1
- package/dist/vue/composables/useDataLayer.d.ts +1 -0
- package/dist/vue/composables/useDataLayer.d.ts.map +1 -1
- package/dist/vue/composables/useDataLayer.js +1 -0
- package/dist/vue/composables/useDataLayer.js.map +1 -1
- package/dist/vue/composables/useDerive.d.ts +1 -1
- package/dist/vue/composables/useDerive.d.ts.map +1 -1
- package/dist/vue/composables/useDerive.js +19 -2
- package/dist/vue/composables/useDerive.js.map +1 -1
- package/dist/vue/composables/useDeriveInitialValue.d.ts +36 -0
- package/dist/vue/composables/useDeriveInitialValue.d.ts.map +1 -0
- package/dist/vue/composables/useDeriveInitialValue.js +125 -0
- package/dist/vue/composables/useDeriveInitialValue.js.map +1 -0
- package/dist/vue/composables/useDirtyValidation.d.ts +9 -0
- package/dist/vue/composables/useDirtyValidation.d.ts.map +1 -0
- package/dist/vue/composables/useDirtyValidation.js +15 -0
- package/dist/vue/composables/useDirtyValidation.js.map +1 -0
- package/dist/vue/composables/useProjection.d.ts +6 -0
- package/dist/vue/composables/useProjection.d.ts.map +1 -1
- package/dist/vue/composables/useProjection.js +54 -3
- package/dist/vue/composables/useProjection.js.map +1 -1
- package/dist/vue/composables/useProvider.d.ts +2 -2
- package/dist/vue/composables/useProvider.d.ts.map +1 -1
- package/dist/vue/composables/useProvider.js +14 -10
- package/dist/vue/composables/useProvider.js.map +1 -1
- package/dist/vue/index.d.ts +3 -0
- package/dist/vue/index.d.ts.map +1 -1
- package/dist/vue/index.js +32 -10
- package/dist/vue/index.js.map +1 -1
- package/dist/vue/primevue/JfBoolean.vue.d.ts.map +1 -1
- package/dist/vue/primevue/JfBoolean.vue.js +26 -9
- package/dist/vue/primevue/JfBoolean.vue.js.map +1 -1
- package/dist/vue/primevue/JfEnum.vue.d.ts.map +1 -1
- package/dist/vue/primevue/JfEnum.vue.js +21 -17
- package/dist/vue/primevue/JfEnum.vue.js.map +1 -1
- package/dist/vue/primevue/JfEnumArray.vue.d.ts.map +1 -1
- package/dist/vue/primevue/JfEnumArray.vue.js +22 -12
- package/dist/vue/primevue/JfEnumArray.vue.js.map +1 -1
- package/dist/vue/primevue/JfNumber.vue.d.ts.map +1 -1
- package/dist/vue/primevue/JfNumber.vue.js +21 -17
- package/dist/vue/primevue/JfNumber.vue.js.map +1 -1
- package/dist/vue/primevue/JfText.vue.d.ts.map +1 -1
- package/dist/vue/primevue/JfText.vue.js +29 -26
- package/dist/vue/primevue/JfText.vue.js.map +1 -1
- package/dist/vue/primevue/JfTextArea.vue.d.ts.map +1 -1
- package/dist/vue/primevue/JfTextArea.vue.js +22 -13
- package/dist/vue/primevue/JfTextArea.vue.js.map +1 -1
- package/dist/vue/primevue/index.d.ts.map +1 -1
- package/dist/vue/primevue/index.js +93 -16
- package/dist/vue/primevue/index.js.map +1 -1
- package/dist/vue/utils/autoSelect.js.map +1 -1
- package/package.json +3 -1
- package/src/core/initFormData.ts +189 -0
- package/src/core/projection.ts +5 -5
- package/src/core/transforms.ts +33 -6
- package/src/core/types.ts +1 -0
- package/src/index.ts +1 -0
- package/src/vue/components/ProviderAutocomplete.vue +8 -5
- package/src/vue/components/ProviderMultiSelect.vue +13 -8
- package/src/vue/components/ProviderSelect.vue +14 -7
- package/src/vue/composables/useDataLayer.ts +1 -1
- package/src/vue/composables/useDerive.ts +46 -3
- package/src/vue/composables/useDeriveInitialValue.ts +195 -0
- package/src/vue/composables/useDirtyValidation.ts +20 -0
- package/src/vue/composables/useProjection.ts +108 -1
- package/src/vue/composables/useProvider.ts +28 -11
- package/src/vue/index.ts +29 -9
- package/src/vue/primevue/JfBoolean.vue +19 -6
- package/src/vue/primevue/JfEnum.vue +23 -21
- package/src/vue/primevue/JfEnumArray.vue +25 -15
- package/src/vue/primevue/JfNumber.vue +22 -20
- package/src/vue/primevue/JfText.vue +26 -24
- package/src/vue/primevue/JfTextArea.vue +22 -15
- package/src/vue/primevue/index.ts +104 -23
- package/src/vue/styles.css +26 -1
- 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
|
}
|
|
@@ -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?:
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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,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
|
-
|
|
26
|
-
|
|
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
|
-
|
|
36
|
+
107, // Higher than PrimeVue JfNumber integer renderer (106) so provider wins
|
|
33
37
|
and(
|
|
34
|
-
or(
|
|
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
|
-
|
|
50
|
+
108, // Higher than providerSelectTester so autocomplete variant wins when flagged
|
|
42
51
|
and(
|
|
43
|
-
or(
|
|
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
|
-
|
|
57
|
-
|
|
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
|
-
|
|
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 {
|
|
52
|
+
const {
|
|
53
|
+
projectedData,
|
|
54
|
+
handleProjectedChange: handleChange,
|
|
55
|
+
projectedErrors,
|
|
56
|
+
projectedLabel,
|
|
57
|
+
} = useProjection(control, rawHandleChange);
|
|
52
58
|
|
|
53
|
-
|
|
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
|
|
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
|
-
:
|
|
74
|
+
:class="{ 'p-invalid': showErrors }"
|
|
75
|
+
:aria-invalid="showErrors || undefined"
|
|
63
76
|
@update:model-value="onToggle"
|
|
64
77
|
/>
|
|
65
|
-
<label v-if="
|
|
66
|
-
<small v-if="
|
|
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,
|
|
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 {
|
|
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[] => {
|
|
@@ -119,7 +126,7 @@ const {
|
|
|
119
126
|
} = useProvider(binding, {
|
|
120
127
|
data: rootData,
|
|
121
128
|
path: control.value.path,
|
|
122
|
-
dependsOnValues: depValues
|
|
129
|
+
dependsOnValues: depValues,
|
|
123
130
|
});
|
|
124
131
|
|
|
125
132
|
const placeholder = computed<string | undefined>(() => {
|
|
@@ -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
|
|
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="
|
|
179
|
-
<label v-if="
|
|
180
|
-
|
|
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="
|
|
195
|
+
:aria-invalid="showErrors || undefined"
|
|
194
196
|
:show-clear="true"
|
|
195
197
|
@update:model-value="onSelect"
|
|
196
|
-
@blur="
|
|
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">{{
|
|
203
|
+
<small v-else-if="showErrors" class="p-error">{{ projectedErrors }}</small>
|
|
202
204
|
</div>
|
|
203
205
|
</template>
|