@narrative.io/jsonforms-provider-protocols 3.0.0-beta.2 → 3.0.0-beta.21
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 +17 -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 +4 -0
- package/dist/core/projection.d.ts.map +1 -1
- package/dist/core/projection.js +17 -14
- package/dist/core/projection.js.map +1 -1
- package/dist/core/refs.d.ts +58 -0
- package/dist/core/refs.d.ts.map +1 -0
- package/dist/core/refs.js +70 -0
- package/dist/core/refs.js.map +1 -0
- package/dist/core/resolveScope.d.ts +6 -0
- package/dist/core/resolveScope.d.ts.map +1 -1
- package/dist/core/resolveScope.js +14 -8
- package/dist/core/resolveScope.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 +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -1
- package/dist/index.js.map +1 -1
- package/dist/jsonforms-provider-protocols.css +2 -2
- package/dist/no-eval-ajv.d.ts +70 -0
- package/dist/no-eval-ajv.d.ts.map +1 -0
- package/dist/no-eval-ajv.js +247 -0
- package/dist/no-eval-ajv.js.map +1 -0
- package/dist/vue/components/ProviderAutocomplete.vue.d.ts.map +1 -1
- package/dist/vue/components/ProviderAutocomplete.vue.js +10 -6
- 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 +17 -9
- 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 +19 -9
- 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 +3 -3
- package/dist/vue/composables/useDirtyValidation.d.ts.map +1 -1
- package/dist/vue/composables/useDirtyValidation.js +2 -2
- package/dist/vue/composables/useDirtyValidation.js.map +1 -1
- package/dist/vue/composables/useProjection.d.ts +7 -0
- package/dist/vue/composables/useProjection.d.ts.map +1 -1
- package/dist/vue/composables/useProjection.js +87 -4
- 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 +2 -0
- package/dist/vue/index.d.ts.map +1 -1
- package/dist/vue/index.js +30 -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 +17 -6
- 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 +22 -10
- 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 +20 -10
- 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 +18 -10
- 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 +27 -12
- 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 +15 -9
- 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/dist/vue/utils/placeholder.d.ts +17 -0
- package/dist/vue/utils/placeholder.d.ts.map +1 -0
- package/dist/vue/utils/placeholder.js +17 -0
- package/dist/vue/utils/placeholder.js.map +1 -0
- package/package.json +10 -2
- package/src/core/initFormData.ts +208 -0
- package/src/core/projection.ts +33 -22
- package/src/core/refs.ts +166 -0
- package/src/core/resolveScope.ts +23 -8
- package/src/core/transforms.ts +33 -6
- package/src/core/types.ts +1 -0
- package/src/index.ts +14 -2
- package/src/no-eval-ajv.ts +381 -0
- package/src/vue/components/ProviderAutocomplete.vue +9 -7
- package/src/vue/components/ProviderMultiSelect.vue +20 -15
- package/src/vue/components/ProviderSelect.vue +21 -14
- 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 +8 -3
- package/src/vue/composables/useProjection.ts +172 -1
- package/src/vue/composables/useProvider.ts +28 -11
- package/src/vue/index.ts +28 -9
- package/src/vue/primevue/JfBoolean.vue +10 -5
- package/src/vue/primevue/JfEnum.vue +23 -14
- package/src/vue/primevue/JfEnumArray.vue +22 -17
- package/src/vue/primevue/JfNumber.vue +20 -12
- package/src/vue/primevue/JfText.vue +31 -16
- package/src/vue/primevue/JfTextArea.vue +15 -13
- package/src/vue/primevue/index.ts +104 -23
- package/src/vue/styles.css +26 -1
- package/src/vue/utils/autoSelect.ts +2 -2
- package/src/vue/utils/placeholder.ts +42 -0
|
@@ -3,8 +3,10 @@ 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 { useDeriveInitialValue } from "../composables/useDeriveInitialValue";
|
|
6
7
|
import { useProjection } from "../composables/useProjection";
|
|
7
8
|
import { shouldAutoSelect } from "../utils/autoSelect";
|
|
9
|
+
import { resolvePlaceholder } from "../utils/placeholder";
|
|
8
10
|
import Dropdown from "primevue/dropdown";
|
|
9
11
|
|
|
10
12
|
const props = defineProps<{
|
|
@@ -13,7 +15,11 @@ const props = defineProps<{
|
|
|
13
15
|
path: string;
|
|
14
16
|
}>();
|
|
15
17
|
const { control, handleChange: rawHandleChange } = useJsonFormsControl(props);
|
|
16
|
-
const {
|
|
18
|
+
const {
|
|
19
|
+
projectedData,
|
|
20
|
+
projectedLabel,
|
|
21
|
+
handleProjectedChange: handleChange,
|
|
22
|
+
} = useProjection(control, rawHandleChange);
|
|
17
23
|
|
|
18
24
|
const binding = computed(() => {
|
|
19
25
|
const provider = control.value.uischema?.options?.provider;
|
|
@@ -41,11 +47,14 @@ const rootData = computed(() => injectedFormData.value || {});
|
|
|
41
47
|
const { items, loading, error } = useProvider(binding, {
|
|
42
48
|
data: rootData, // Pass the reactive reference
|
|
43
49
|
path: control.value.path,
|
|
44
|
-
dependsOnValues: depValues
|
|
50
|
+
dependsOnValues: depValues,
|
|
45
51
|
});
|
|
46
52
|
|
|
47
53
|
// Provider will automatically reload when rootData changes due to reactive cache key
|
|
48
54
|
|
|
55
|
+
// deriveInitialValue — async API-based initial value seeding
|
|
56
|
+
useDeriveInitialValue({ control, handleChange });
|
|
57
|
+
|
|
49
58
|
// Auto-select when provider returns only one item (enabled by default)
|
|
50
59
|
watch(
|
|
51
60
|
[items, loading],
|
|
@@ -62,7 +71,7 @@ watch(
|
|
|
62
71
|
handleChange(control.value.path, valueToSelect);
|
|
63
72
|
}
|
|
64
73
|
},
|
|
65
|
-
{ immediate: true }
|
|
74
|
+
{ immediate: true },
|
|
66
75
|
);
|
|
67
76
|
|
|
68
77
|
const value = computed({
|
|
@@ -72,25 +81,23 @@ const value = computed({
|
|
|
72
81
|
|
|
73
82
|
const placeholder = computed(() => {
|
|
74
83
|
if (loading.value) return "Loading…";
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
84
|
+
return resolvePlaceholder(
|
|
85
|
+
control.value.uischema,
|
|
86
|
+
projectedLabel.value,
|
|
87
|
+
"select",
|
|
88
|
+
);
|
|
80
89
|
});
|
|
81
90
|
</script>
|
|
82
91
|
|
|
83
92
|
<template>
|
|
84
|
-
<div class="
|
|
85
|
-
<label v-if="
|
|
86
|
-
|
|
87
|
-
}}</label>
|
|
88
|
-
<div v-if="control.description" class="text-color-secondary text-left">
|
|
93
|
+
<div class="jf-control">
|
|
94
|
+
<label v-if="projectedLabel" class="jf-label">{{ projectedLabel }}</label>
|
|
95
|
+
<div v-if="control.description" class="jf-description">
|
|
89
96
|
{{ control.description }}
|
|
90
97
|
</div>
|
|
91
98
|
<Dropdown
|
|
92
99
|
v-model="value"
|
|
93
|
-
class="w-full"
|
|
100
|
+
class="w-full!"
|
|
94
101
|
:options="items"
|
|
95
102
|
option-label="label"
|
|
96
103
|
option-value="value"
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
} from "vue";
|
|
9
9
|
import type { ConnectorDataLayer } from "../../core/types";
|
|
10
10
|
|
|
11
|
-
const DATA_LAYER_KEY = Symbol("dataLayer");
|
|
11
|
+
export const DATA_LAYER_KEY = Symbol("dataLayer");
|
|
12
12
|
|
|
13
13
|
export interface DataLayer {
|
|
14
14
|
push(data: Partial<ConnectorDataLayer>): void;
|
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
computed,
|
|
3
|
+
ref,
|
|
4
|
+
watch,
|
|
5
|
+
unref,
|
|
6
|
+
inject,
|
|
7
|
+
type Ref,
|
|
8
|
+
type ComputedRef,
|
|
9
|
+
} from "vue";
|
|
2
10
|
import { type ControlElement } from "@jsonforms/core";
|
|
3
11
|
import { useDataLayer } from "./useDataLayer";
|
|
4
12
|
|
|
@@ -14,7 +22,11 @@ interface DeriveOptions {
|
|
|
14
22
|
data?: Ref<unknown> | ComputedRef<unknown>;
|
|
15
23
|
}
|
|
16
24
|
|
|
17
|
-
export function useDerive({
|
|
25
|
+
export function useDerive({
|
|
26
|
+
control,
|
|
27
|
+
handleChange,
|
|
28
|
+
data: dataOverride,
|
|
29
|
+
}: DeriveOptions) {
|
|
18
30
|
// Get the root form data from JSONForms context
|
|
19
31
|
const injectedFormData = inject<{ value: unknown }>("formData", {
|
|
20
32
|
value: {},
|
|
@@ -36,6 +48,14 @@ export function useDerive({ control, handleChange, data: dataOverride }: DeriveO
|
|
|
36
48
|
};
|
|
37
49
|
});
|
|
38
50
|
|
|
51
|
+
// Track the last resolved source value so we only update the field
|
|
52
|
+
// when the source itself changes, not on every form data mutation.
|
|
53
|
+
const lastDerivedValue = ref<unknown>(undefined);
|
|
54
|
+
// First watch fire is special: a pre-populated field (edit flow seeded
|
|
55
|
+
// from a saved connection) must not be clobbered by whatever the source
|
|
56
|
+
// resolves to on mount. We record the source value but skip handleChange.
|
|
57
|
+
let isFirstRun = true;
|
|
58
|
+
|
|
39
59
|
// Watch for changes in form data and dataLayer and update derived field
|
|
40
60
|
watch(
|
|
41
61
|
[rootData, dataLayerData, deriveConfig],
|
|
@@ -50,7 +70,30 @@ export function useDerive({ control, handleChange, data: dataOverride }: DeriveO
|
|
|
50
70
|
data,
|
|
51
71
|
extData,
|
|
52
72
|
);
|
|
53
|
-
const compareData = dataOverride
|
|
73
|
+
const compareData = dataOverride
|
|
74
|
+
? unref(dataOverride)
|
|
75
|
+
: control.value.data;
|
|
76
|
+
|
|
77
|
+
if (isFirstRun) {
|
|
78
|
+
isFirstRun = false;
|
|
79
|
+
lastDerivedValue.value = derivedValue;
|
|
80
|
+
// On mount, only populate the field if it's empty.
|
|
81
|
+
// A non-empty seed represents user intent (edit flow) and must
|
|
82
|
+
// be preserved. Subsequent source changes will still propagate.
|
|
83
|
+
const isFieldEmpty =
|
|
84
|
+
compareData === undefined ||
|
|
85
|
+
compareData === null ||
|
|
86
|
+
compareData === "";
|
|
87
|
+
if (isFieldEmpty && derivedValue !== compareData) {
|
|
88
|
+
handleChange(control.value.path, derivedValue);
|
|
89
|
+
}
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Only update if the SOURCE value changed, not just the field value
|
|
94
|
+
if (derivedValue === lastDerivedValue.value) return;
|
|
95
|
+
lastDerivedValue.value = derivedValue;
|
|
96
|
+
|
|
54
97
|
if (derivedValue !== compareData) {
|
|
55
98
|
handleChange(control.value.path, derivedValue);
|
|
56
99
|
}
|
|
@@ -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
|
+
}
|
|
@@ -1,10 +1,15 @@
|
|
|
1
|
-
import { ref, computed, type Ref } from "vue";
|
|
1
|
+
import { ref, computed, type Ref, type ComputedRef } from "vue";
|
|
2
2
|
|
|
3
|
-
export function useDirtyValidation(
|
|
3
|
+
export function useDirtyValidation(
|
|
4
|
+
control: Ref<{ errors: string }>,
|
|
5
|
+
errorsOverride?: Ref<string> | ComputedRef<string>,
|
|
6
|
+
) {
|
|
4
7
|
const hasInteracted = ref(false);
|
|
5
8
|
|
|
6
9
|
const showErrors = computed(
|
|
7
|
-
() =>
|
|
10
|
+
() =>
|
|
11
|
+
hasInteracted.value &&
|
|
12
|
+
!!(errorsOverride ? errorsOverride.value : control.value.errors),
|
|
8
13
|
);
|
|
9
14
|
|
|
10
15
|
const markDirty = () => {
|
|
@@ -1,18 +1,122 @@
|
|
|
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,
|
|
5
5
|
getProjectedSchema,
|
|
6
|
+
parseProjectionPath,
|
|
6
7
|
} from "../../core/projection";
|
|
8
|
+
import { deref } from "../../core/refs";
|
|
7
9
|
|
|
8
10
|
interface ProjectionControl {
|
|
9
11
|
data: unknown;
|
|
10
12
|
path: string;
|
|
13
|
+
errors: string;
|
|
14
|
+
label?: string;
|
|
15
|
+
required?: boolean;
|
|
11
16
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
12
17
|
schema: Record<string, any>;
|
|
13
18
|
uischema: { options?: { projection?: string; [key: string]: unknown } };
|
|
14
19
|
}
|
|
15
20
|
|
|
21
|
+
// Minimal AJV ErrorObject shape for filtering
|
|
22
|
+
interface ErrorLike {
|
|
23
|
+
instancePath?: string;
|
|
24
|
+
keyword?: string;
|
|
25
|
+
message?: string;
|
|
26
|
+
params?: { missingProperty?: string };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Resolve the display label for a control.
|
|
31
|
+
* Priority: uischema options.label → schemaTitle (projected sub-schema) → control.label.
|
|
32
|
+
* Appends a trailing asterisk when the control is for a required property.
|
|
33
|
+
*/
|
|
34
|
+
function resolveLabel(
|
|
35
|
+
ctrl: ProjectionControl,
|
|
36
|
+
schemaTitle?: string,
|
|
37
|
+
required?: boolean,
|
|
38
|
+
): string {
|
|
39
|
+
const base =
|
|
40
|
+
(ctrl.uischema?.options?.label as string) ??
|
|
41
|
+
schemaTitle ??
|
|
42
|
+
ctrl.label ??
|
|
43
|
+
"";
|
|
44
|
+
if (!base) return base;
|
|
45
|
+
return required ? `${base} *` : base;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Determine whether the leaf of a projection path is listed in its parent
|
|
50
|
+
* schema's `required` array. Numeric leaf segments (array indices) are not
|
|
51
|
+
* considered "required properties".
|
|
52
|
+
*
|
|
53
|
+
* Dereferences `$ref` at every step against `root` (defaulting to `schema`)
|
|
54
|
+
* so that schemas like `items: { $ref: '#/$defs/Foo' }` resolve correctly.
|
|
55
|
+
*/
|
|
56
|
+
function isProjectedFieldRequired(
|
|
57
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
58
|
+
schema: Record<string, any>,
|
|
59
|
+
path: string,
|
|
60
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
61
|
+
root?: Record<string, any>,
|
|
62
|
+
): boolean {
|
|
63
|
+
const segments = parseProjectionPath(path);
|
|
64
|
+
if (segments.length === 0) return false;
|
|
65
|
+
const rootSchema = root ?? schema;
|
|
66
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
67
|
+
let current: Record<string, any> | undefined = deref(schema, rootSchema);
|
|
68
|
+
for (let i = 0; i < segments.length - 1; i++) {
|
|
69
|
+
const seg = segments[i]!;
|
|
70
|
+
if (typeof seg === "number") {
|
|
71
|
+
current = deref(current?.items, rootSchema);
|
|
72
|
+
} else {
|
|
73
|
+
current = deref(current?.properties?.[seg], rootSchema);
|
|
74
|
+
}
|
|
75
|
+
if (!current) return false;
|
|
76
|
+
}
|
|
77
|
+
const last = segments[segments.length - 1];
|
|
78
|
+
if (typeof last !== "string") return false;
|
|
79
|
+
current = deref(current, rootSchema);
|
|
80
|
+
return Array.isArray(current?.required) && current.required.includes(last);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Normalize AJV error message fragments into user-friendly text.
|
|
85
|
+
* e.g. "is a required property" → "is required"
|
|
86
|
+
*/
|
|
87
|
+
function normalizeErrors(errors: string): string {
|
|
88
|
+
if (!errors) return errors;
|
|
89
|
+
return errors
|
|
90
|
+
.replace(/is a required property/g, "is required")
|
|
91
|
+
.replace(/must have required property '[^']*'/g, "is required");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Prefix each error message line with the field label so that AJV fragments
|
|
96
|
+
* like "is required" become "Name is required".
|
|
97
|
+
*/
|
|
98
|
+
function prefixErrors(label: string, errors: string): string {
|
|
99
|
+
if (!label || !errors) return errors;
|
|
100
|
+
return errors
|
|
101
|
+
.split("\n")
|
|
102
|
+
.map((line) => `${label} ${line}`)
|
|
103
|
+
.join("\n");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Convert an AJV ErrorObject's instancePath to a dot-separated control path.
|
|
108
|
+
* Replicates the logic from @jsonforms/core getControlPath.
|
|
109
|
+
*/
|
|
110
|
+
function getErrorPath(error: ErrorLike): string {
|
|
111
|
+
let p = (error.instancePath || "").replace(/\//g, ".").replace(/^\./, "");
|
|
112
|
+
if (error.keyword === "required" && error.params?.missingProperty) {
|
|
113
|
+
p = p
|
|
114
|
+
? p + "." + error.params.missingProperty
|
|
115
|
+
: error.params.missingProperty;
|
|
116
|
+
}
|
|
117
|
+
return p;
|
|
118
|
+
}
|
|
119
|
+
|
|
16
120
|
export interface ProjectionResult {
|
|
17
121
|
/** The value at the projected path (for rendering) */
|
|
18
122
|
projectedData: ComputedRef<unknown>;
|
|
@@ -23,6 +127,10 @@ export interface ProjectionResult {
|
|
|
23
127
|
handleProjectedChange: (path: string, value: unknown) => void;
|
|
24
128
|
/** Whether projection is active */
|
|
25
129
|
hasProjection: boolean;
|
|
130
|
+
/** Resolved display label (options.label → projected schema title → control.label) */
|
|
131
|
+
projectedLabel: ComputedRef<string>;
|
|
132
|
+
/** Error string combining base-path and projected sub-path errors */
|
|
133
|
+
projectedErrors: ComputedRef<string>;
|
|
26
134
|
}
|
|
27
135
|
|
|
28
136
|
/**
|
|
@@ -44,14 +152,37 @@ export function useProjection(
|
|
|
44
152
|
| undefined;
|
|
45
153
|
|
|
46
154
|
if (!projection) {
|
|
155
|
+
const label = computed(() =>
|
|
156
|
+
resolveLabel(control.value, undefined, control.value.required),
|
|
157
|
+
);
|
|
47
158
|
return {
|
|
48
159
|
projectedData: computed(() => control.value.data),
|
|
49
160
|
projectedSchema: computed(() => control.value.schema),
|
|
50
161
|
handleProjectedChange: handleChange,
|
|
51
162
|
hasProjection: false,
|
|
163
|
+
projectedLabel: label,
|
|
164
|
+
projectedErrors: computed(() =>
|
|
165
|
+
prefixErrors(
|
|
166
|
+
label.value.replace(/\*$/, "").trim(),
|
|
167
|
+
normalizeErrors(control.value.errors),
|
|
168
|
+
),
|
|
169
|
+
),
|
|
52
170
|
};
|
|
53
171
|
}
|
|
54
172
|
|
|
173
|
+
// Inject JSONForms state to access raw AJV errors for projected sub-paths,
|
|
174
|
+
// and the root schema so that `$ref` resolution inside the projected slice
|
|
175
|
+
// can find `$defs` (which live at the root, not on the control's schema).
|
|
176
|
+
const jsonforms = inject<{
|
|
177
|
+
core?: {
|
|
178
|
+
errors?: ErrorLike[];
|
|
179
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
180
|
+
schema?: Record<string, any>;
|
|
181
|
+
};
|
|
182
|
+
} | null>("jsonforms", null);
|
|
183
|
+
|
|
184
|
+
const fullProjectedPath = control.value.path + "." + projection;
|
|
185
|
+
|
|
55
186
|
const projectedData = computed(() =>
|
|
56
187
|
getProjectedValue(control.value.data, projection),
|
|
57
188
|
);
|
|
@@ -60,6 +191,44 @@ export function useProjection(
|
|
|
60
191
|
getProjectedSchema(control.value.schema, projection),
|
|
61
192
|
);
|
|
62
193
|
|
|
194
|
+
const projectedRequired = computed(() =>
|
|
195
|
+
isProjectedFieldRequired(
|
|
196
|
+
control.value.schema,
|
|
197
|
+
projection,
|
|
198
|
+
jsonforms?.core?.schema ?? control.value.schema,
|
|
199
|
+
),
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
const label = computed(() =>
|
|
203
|
+
resolveLabel(
|
|
204
|
+
control.value,
|
|
205
|
+
projectedSchema.value?.title,
|
|
206
|
+
projectedRequired.value,
|
|
207
|
+
),
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
const projectedErrors = computed(() => {
|
|
211
|
+
const baseErrors = normalizeErrors(control.value.errors || "");
|
|
212
|
+
|
|
213
|
+
const rawErrors = jsonforms?.core?.errors ?? ([] as ErrorLike[]);
|
|
214
|
+
const matching = rawErrors.filter(
|
|
215
|
+
(err) => getErrorPath(err) === fullProjectedPath,
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
let errStr: string;
|
|
219
|
+
if (matching.length === 0) {
|
|
220
|
+
errStr = baseErrors;
|
|
221
|
+
} else {
|
|
222
|
+
const projMsg = matching
|
|
223
|
+
.map((e) => (e.keyword === "required" ? "is required" : e.message))
|
|
224
|
+
.filter(Boolean)
|
|
225
|
+
.join("\n");
|
|
226
|
+
errStr = [baseErrors, projMsg].filter(Boolean).join("\n");
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return prefixErrors(label.value.replace(/\*$/, "").trim(), errStr);
|
|
230
|
+
});
|
|
231
|
+
|
|
63
232
|
const handleProjectedChange = (path: string, value: unknown) => {
|
|
64
233
|
const fullValue = setProjectedValue(control.value.data, projection, value);
|
|
65
234
|
handleChange(path, fullValue);
|
|
@@ -70,5 +239,7 @@ export function useProjection(
|
|
|
70
239
|
projectedSchema,
|
|
71
240
|
handleProjectedChange,
|
|
72
241
|
hasProjection: true,
|
|
242
|
+
projectedLabel: label,
|
|
243
|
+
projectedErrors,
|
|
73
244
|
};
|
|
74
245
|
}
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
} from "vue";
|
|
11
11
|
import { cache as globalCache } from "../../core/cache";
|
|
12
12
|
import { registry as globalRegistry } from "../../core/registry";
|
|
13
|
+
import { renderObj } from "../../core/templating";
|
|
13
14
|
import type {
|
|
14
15
|
AuthConfig,
|
|
15
16
|
ProviderBinding,
|
|
@@ -25,8 +26,11 @@ export function useProvider(
|
|
|
25
26
|
ctxBits: {
|
|
26
27
|
data: unknown | Ref<unknown> | ComputedRef<unknown>;
|
|
27
28
|
path: string;
|
|
28
|
-
dependsOnValues?: unknown[]
|
|
29
|
-
uiQuery?:
|
|
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
|
});
|