@narrative.io/jsonforms-provider-protocols 2.11.0-beta.0 → 2.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +193 -33
- 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 +36 -0
- package/dist/core/projection.d.ts.map +1 -0
- package/dist/core/projection.js +77 -0
- package/dist/core/projection.js.map +1 -0
- 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 +17 -0
- package/dist/core/resolveScope.d.ts.map +1 -0
- package/dist/core/resolveScope.js +28 -0
- package/dist/core/resolveScope.js.map +1 -0
- package/dist/core/seedProjectionTargets.d.ts +60 -0
- package/dist/core/seedProjectionTargets.d.ts.map +1 -0
- package/dist/core/seedProjectionTargets.js +52 -0
- package/dist/core/seedProjectionTargets.js.map +1 -0
- package/dist/core/transforms.d.ts +8 -10
- package/dist/core/transforms.d.ts.map +1 -1
- package/dist/core/transforms.js +58 -13
- package/dist/core/transforms.js.map +1 -1
- package/dist/core/types.d.ts +8 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/index.d.ts +9 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +21 -3
- package/dist/index.js.map +1 -1
- package/dist/jsonforms-provider-protocols.css +6 -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 -4
- 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 +19 -9
- package/dist/vue/components/ProviderMultiSelect.vue2.js.map +1 -1
- package/dist/vue/components/ProviderObjectMultiSelect.vue.d.ts +9 -0
- package/dist/vue/components/ProviderObjectMultiSelect.vue.d.ts.map +1 -0
- package/dist/vue/components/ProviderObjectMultiSelect.vue.js +8 -0
- package/dist/vue/components/ProviderObjectMultiSelect.vue.js.map +1 -0
- package/dist/vue/components/ProviderObjectMultiSelect.vue2.js +142 -0
- package/dist/vue/components/ProviderObjectMultiSelect.vue2.js.map +1 -0
- 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 +20 -8
- package/dist/vue/components/ProviderSelect.vue2.js.map +1 -1
- package/dist/vue/composables/useDataLayer.d.ts +10 -0
- package/dist/vue/composables/useDataLayer.d.ts.map +1 -0
- package/dist/vue/composables/useDataLayer.js +26 -0
- package/dist/vue/composables/useDataLayer.js.map +1 -0
- package/dist/vue/composables/useDerive.d.ts +5 -2
- package/dist/vue/composables/useDerive.d.ts.map +1 -1
- package/dist/vue/composables/useDerive.js +29 -12
- 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 +42 -0
- package/dist/vue/composables/useProjection.d.ts.map +1 -0
- package/dist/vue/composables/useProjection.js +116 -0
- package/dist/vue/composables/useProjection.js.map +1 -0
- 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 +9 -1
- package/dist/vue/index.d.ts.map +1 -1
- package/dist/vue/index.js +72 -34
- package/dist/vue/index.js.map +1 -1
- package/dist/vue/primevue/JfBoolean.vue.d.ts +9 -0
- package/dist/vue/primevue/JfBoolean.vue.d.ts.map +1 -1
- package/dist/vue/primevue/JfBoolean.vue.js +42 -21
- package/dist/vue/primevue/JfBoolean.vue.js.map +1 -1
- package/dist/vue/primevue/JfEnum.vue.d.ts +9 -0
- package/dist/vue/primevue/JfEnum.vue.d.ts.map +1 -1
- package/dist/vue/primevue/JfEnum.vue.js +35 -21
- package/dist/vue/primevue/JfEnum.vue.js.map +1 -1
- package/dist/vue/primevue/JfEnumArray.vue.d.ts +9 -0
- package/dist/vue/primevue/JfEnumArray.vue.d.ts.map +1 -1
- package/dist/vue/primevue/JfEnumArray.vue.js +37 -17
- package/dist/vue/primevue/JfEnumArray.vue.js.map +1 -1
- package/dist/vue/primevue/JfNumber.vue.d.ts +9 -0
- package/dist/vue/primevue/JfNumber.vue.d.ts.map +1 -1
- package/dist/vue/primevue/JfNumber.vue.js +30 -20
- package/dist/vue/primevue/JfNumber.vue.js.map +1 -1
- package/dist/vue/primevue/JfText.vue.d.ts +9 -0
- package/dist/vue/primevue/JfText.vue.d.ts.map +1 -1
- package/dist/vue/primevue/JfText.vue.js +48 -32
- package/dist/vue/primevue/JfText.vue.js.map +1 -1
- package/dist/vue/primevue/JfTextArea.vue.d.ts +9 -0
- package/dist/vue/primevue/JfTextArea.vue.d.ts.map +1 -1
- package/dist/vue/primevue/JfTextArea.vue.js +31 -16
- 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 +74 -7
- package/dist/vue/primevue/index.js.map +1 -1
- package/dist/vue/utils/autoSelect.js.map +1 -1
- package/dist/vue/utils/objectMultiSelect.d.ts +68 -0
- package/dist/vue/utils/objectMultiSelect.d.ts.map +1 -0
- package/dist/vue/utils/objectMultiSelect.js +72 -0
- package/dist/vue/utils/objectMultiSelect.js.map +1 -0
- 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 +147 -0
- package/src/core/refs.ts +166 -0
- package/src/core/resolveScope.ts +54 -0
- package/src/core/seedProjectionTargets.ts +144 -0
- package/src/core/transforms.ts +118 -26
- package/src/core/types.ts +9 -0
- package/src/index.ts +22 -2
- package/src/no-eval-ajv.ts +381 -0
- package/src/vue/components/ProviderAutocomplete.vue +10 -6
- package/src/vue/components/ProviderMultiSelect.vue +22 -15
- package/src/vue/components/ProviderObjectMultiSelect.vue +169 -0
- package/src/vue/components/ProviderSelect.vue +23 -14
- package/src/vue/composables/useDataLayer.ts +43 -0
- package/src/vue/composables/useDerive.ts +62 -16
- package/src/vue/composables/useDeriveInitialValue.ts +195 -0
- package/src/vue/composables/useDirtyValidation.ts +20 -0
- package/src/vue/composables/useProjection.ts +245 -0
- package/src/vue/composables/useProvider.ts +28 -11
- package/src/vue/index.ts +83 -47
- package/src/vue/primevue/JfBoolean.vue +35 -12
- package/src/vue/primevue/JfEnum.vue +34 -25
- package/src/vue/primevue/JfEnumArray.vue +36 -19
- package/src/vue/primevue/JfNumber.vue +30 -22
- package/src/vue/primevue/JfText.vue +46 -31
- package/src/vue/primevue/JfTextArea.vue +30 -19
- package/src/vue/primevue/index.ts +88 -7
- package/src/vue/utils/autoSelect.ts +2 -2
- package/src/vue/utils/objectMultiSelect.ts +171 -0
- package/src/vue/utils/placeholder.ts +42 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { ControlElement, JsonSchema } from "@jsonforms/core";
|
|
3
|
+
import { useJsonFormsControl } from "@jsonforms/vue";
|
|
4
|
+
import { computed, inject, watch } from "vue";
|
|
5
|
+
import { useProvider } from "../composables/useProvider";
|
|
6
|
+
import { useProjection } from "../composables/useProjection";
|
|
7
|
+
import { resolvePlaceholder } from "../utils/placeholder";
|
|
8
|
+
import {
|
|
9
|
+
fromMultiSelectShape,
|
|
10
|
+
resolveItemsSchema,
|
|
11
|
+
resolveObjectKeys,
|
|
12
|
+
sameObjectSet,
|
|
13
|
+
toMultiSelectShape,
|
|
14
|
+
type MultiSelectOption,
|
|
15
|
+
type ObjectKeys,
|
|
16
|
+
} from "../utils/objectMultiSelect";
|
|
17
|
+
import MultiSelect from "primevue/multiselect";
|
|
18
|
+
|
|
19
|
+
const props = defineProps<{
|
|
20
|
+
uischema: ControlElement;
|
|
21
|
+
schema: JsonSchema;
|
|
22
|
+
path: string;
|
|
23
|
+
}>();
|
|
24
|
+
const { control, handleChange: rawHandleChange } = useJsonFormsControl(props);
|
|
25
|
+
const {
|
|
26
|
+
projectedData,
|
|
27
|
+
projectedLabel,
|
|
28
|
+
handleProjectedChange: handleChange,
|
|
29
|
+
} = useProjection(control, rawHandleChange);
|
|
30
|
+
|
|
31
|
+
// Pull root schema so item-schema $refs resolve against the consumer's $defs.
|
|
32
|
+
const jsonforms = inject<{
|
|
33
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
34
|
+
core?: { schema?: Record<string, any> };
|
|
35
|
+
} | null>("jsonforms", null);
|
|
36
|
+
const rootSchema = computed(
|
|
37
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
38
|
+
() => (jsonforms?.core?.schema ?? control.value.schema) as Record<string, any>,
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const itemsSchema = computed(() =>
|
|
42
|
+
resolveItemsSchema(
|
|
43
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
44
|
+
control.value.schema as Record<string, any>,
|
|
45
|
+
rootSchema.value,
|
|
46
|
+
),
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
const objectKeys = computed<ObjectKeys>(() => {
|
|
50
|
+
const resolved = resolveObjectKeys(
|
|
51
|
+
control.value.uischema?.options as
|
|
52
|
+
| { objectKeys?: { value?: unknown; label?: unknown } }
|
|
53
|
+
| undefined,
|
|
54
|
+
itemsSchema.value,
|
|
55
|
+
);
|
|
56
|
+
if (!resolved) {
|
|
57
|
+
throw new Error(
|
|
58
|
+
"[ProviderObjectMultiSelect] objectKeys could not be resolved. " +
|
|
59
|
+
"Specify `uischema.options.objectKeys = { value, label }` or declare " +
|
|
60
|
+
"exactly two `required` properties on the array's items schema so " +
|
|
61
|
+
"they can be inferred.",
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
return resolved;
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const binding = computed(() => {
|
|
68
|
+
const provider = control.value.uischema?.options?.provider;
|
|
69
|
+
if (provider && typeof provider === "object" && !provider.load) {
|
|
70
|
+
return { ...provider, load: "mount" };
|
|
71
|
+
}
|
|
72
|
+
return provider;
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const deps = computed(
|
|
76
|
+
() =>
|
|
77
|
+
((
|
|
78
|
+
(control.value.schema as Record<string, unknown>)?.[
|
|
79
|
+
"x-provider"
|
|
80
|
+
] as Record<string, unknown>
|
|
81
|
+
)?.dependsOn as string[]) ?? [],
|
|
82
|
+
);
|
|
83
|
+
const depValues = computed(() => deps.value.map(() => null));
|
|
84
|
+
|
|
85
|
+
const injectedFormData = inject<{ value: unknown }>("formData", { value: {} });
|
|
86
|
+
const rootData = computed(() => injectedFormData.value || {});
|
|
87
|
+
|
|
88
|
+
const { items, loading, error } = useProvider(binding, {
|
|
89
|
+
data: rootData,
|
|
90
|
+
path: control.value.path,
|
|
91
|
+
dependsOnValues: depValues,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Auto-select when provider returns only one item — paired-object variant.
|
|
95
|
+
watch(
|
|
96
|
+
[items, loading],
|
|
97
|
+
([newItems, isLoading]) => {
|
|
98
|
+
if (
|
|
99
|
+
!control.value.uischema?.options?.autoSelectSingle ||
|
|
100
|
+
isLoading ||
|
|
101
|
+
newItems.length !== 1
|
|
102
|
+
) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const single = newItems[0]!;
|
|
106
|
+
const current = Array.isArray(projectedData.value) ? projectedData.value : [];
|
|
107
|
+
if (current.length === 0) {
|
|
108
|
+
handleChange(control.value.path, [
|
|
109
|
+
{
|
|
110
|
+
[objectKeys.value.value]: single.value,
|
|
111
|
+
[objectKeys.value.label]: single.label,
|
|
112
|
+
},
|
|
113
|
+
]);
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
{ immediate: true },
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
const value = computed<MultiSelectOption[]>({
|
|
120
|
+
get() {
|
|
121
|
+
return toMultiSelectShape(projectedData.value, objectKeys.value);
|
|
122
|
+
},
|
|
123
|
+
set(val) {
|
|
124
|
+
const next = fromMultiSelectShape(val, objectKeys.value);
|
|
125
|
+
const curr = Array.isArray(projectedData.value) ? projectedData.value : [];
|
|
126
|
+
if (!sameObjectSet(curr, next, objectKeys.value.value)) {
|
|
127
|
+
handleChange(control.value.path, next);
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const placeholder = computed(() => {
|
|
133
|
+
if (loading.value) return "Loading…";
|
|
134
|
+
return resolvePlaceholder(
|
|
135
|
+
control.value.uischema,
|
|
136
|
+
projectedLabel.value,
|
|
137
|
+
"select",
|
|
138
|
+
);
|
|
139
|
+
});
|
|
140
|
+
</script>
|
|
141
|
+
|
|
142
|
+
<template>
|
|
143
|
+
<div class="jf-control">
|
|
144
|
+
<label v-if="projectedLabel" class="jf-label">{{ projectedLabel }}</label>
|
|
145
|
+
<div v-if="control.description" class="jf-description">
|
|
146
|
+
{{ control.description }}
|
|
147
|
+
</div>
|
|
148
|
+
<MultiSelect
|
|
149
|
+
v-model="value"
|
|
150
|
+
class="w-full!"
|
|
151
|
+
:options="items"
|
|
152
|
+
option-label="label"
|
|
153
|
+
data-key="value"
|
|
154
|
+
display="chip"
|
|
155
|
+
:placeholder="placeholder"
|
|
156
|
+
:disabled="!control.enabled || loading"
|
|
157
|
+
:show-clear="true"
|
|
158
|
+
/>
|
|
159
|
+
<small v-if="error" class="p-error" role="alert"
|
|
160
|
+
>Failed to load: {{ error }}</small
|
|
161
|
+
>
|
|
162
|
+
</div>
|
|
163
|
+
</template>
|
|
164
|
+
|
|
165
|
+
<style scoped>
|
|
166
|
+
:deep(.p-multiselect-label) {
|
|
167
|
+
text-align: left;
|
|
168
|
+
}
|
|
169
|
+
</style>
|
|
@@ -3,7 +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";
|
|
7
|
+
import { useProjection } from "../composables/useProjection";
|
|
6
8
|
import { shouldAutoSelect } from "../utils/autoSelect";
|
|
9
|
+
import { resolvePlaceholder } from "../utils/placeholder";
|
|
7
10
|
import Dropdown from "primevue/dropdown";
|
|
8
11
|
|
|
9
12
|
const props = defineProps<{
|
|
@@ -11,7 +14,12 @@ const props = defineProps<{
|
|
|
11
14
|
schema: JsonSchema;
|
|
12
15
|
path: string;
|
|
13
16
|
}>();
|
|
14
|
-
const { control, handleChange } = useJsonFormsControl(props);
|
|
17
|
+
const { control, handleChange: rawHandleChange } = useJsonFormsControl(props);
|
|
18
|
+
const {
|
|
19
|
+
projectedData,
|
|
20
|
+
projectedLabel,
|
|
21
|
+
handleProjectedChange: handleChange,
|
|
22
|
+
} = useProjection(control, rawHandleChange);
|
|
15
23
|
|
|
16
24
|
const binding = computed(() => {
|
|
17
25
|
const provider = control.value.uischema?.options?.provider;
|
|
@@ -39,11 +47,14 @@ const rootData = computed(() => injectedFormData.value || {});
|
|
|
39
47
|
const { items, loading, error } = useProvider(binding, {
|
|
40
48
|
data: rootData, // Pass the reactive reference
|
|
41
49
|
path: control.value.path,
|
|
42
|
-
dependsOnValues: depValues
|
|
50
|
+
dependsOnValues: depValues,
|
|
43
51
|
});
|
|
44
52
|
|
|
45
53
|
// Provider will automatically reload when rootData changes due to reactive cache key
|
|
46
54
|
|
|
55
|
+
// deriveInitialValue — async API-based initial value seeding
|
|
56
|
+
useDeriveInitialValue({ control, handleChange });
|
|
57
|
+
|
|
47
58
|
// Auto-select when provider returns only one item (enabled by default)
|
|
48
59
|
watch(
|
|
49
60
|
[items, loading],
|
|
@@ -53,42 +64,40 @@ watch(
|
|
|
53
64
|
control.value.uischema?.options?.autoSelectSingle !== false,
|
|
54
65
|
isLoading,
|
|
55
66
|
items: newItems,
|
|
56
|
-
currentValue:
|
|
67
|
+
currentValue: projectedData.value,
|
|
57
68
|
});
|
|
58
69
|
|
|
59
70
|
if (valueToSelect !== null) {
|
|
60
71
|
handleChange(control.value.path, valueToSelect);
|
|
61
72
|
}
|
|
62
73
|
},
|
|
63
|
-
{ immediate: true }
|
|
74
|
+
{ immediate: true },
|
|
64
75
|
);
|
|
65
76
|
|
|
66
77
|
const value = computed({
|
|
67
|
-
get: () =>
|
|
78
|
+
get: () => projectedData.value,
|
|
68
79
|
set: (v) => handleChange(control.value.path, v),
|
|
69
80
|
});
|
|
70
81
|
|
|
71
82
|
const placeholder = computed(() => {
|
|
72
83
|
if (loading.value) return "Loading…";
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
84
|
+
return resolvePlaceholder(
|
|
85
|
+
control.value.uischema,
|
|
86
|
+
projectedLabel.value,
|
|
87
|
+
"select",
|
|
88
|
+
);
|
|
78
89
|
});
|
|
79
90
|
</script>
|
|
80
91
|
|
|
81
92
|
<template>
|
|
82
93
|
<div class="jf-control">
|
|
83
|
-
<label v-if="
|
|
84
|
-
control.schema.title
|
|
85
|
-
}}</label>
|
|
94
|
+
<label v-if="projectedLabel" class="jf-label">{{ projectedLabel }}</label>
|
|
86
95
|
<div v-if="control.description" class="jf-description">
|
|
87
96
|
{{ control.description }}
|
|
88
97
|
</div>
|
|
89
98
|
<Dropdown
|
|
90
99
|
v-model="value"
|
|
91
|
-
class="w-full"
|
|
100
|
+
class="w-full!"
|
|
92
101
|
:options="items"
|
|
93
102
|
option-label="label"
|
|
94
103
|
option-value="value"
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ref,
|
|
3
|
+
provide,
|
|
4
|
+
inject,
|
|
5
|
+
readonly,
|
|
6
|
+
type DeepReadonly,
|
|
7
|
+
type Ref,
|
|
8
|
+
} from "vue";
|
|
9
|
+
import type { ConnectorDataLayer } from "../../core/types";
|
|
10
|
+
|
|
11
|
+
export const DATA_LAYER_KEY = Symbol("dataLayer");
|
|
12
|
+
|
|
13
|
+
export interface DataLayer {
|
|
14
|
+
push(data: Partial<ConnectorDataLayer>): void;
|
|
15
|
+
state: DeepReadonly<Ref<ConnectorDataLayer>>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function createDataLayer(): DataLayer {
|
|
19
|
+
const state = ref<ConnectorDataLayer>({});
|
|
20
|
+
|
|
21
|
+
const dataLayer: DataLayer = {
|
|
22
|
+
push(data: Partial<ConnectorDataLayer>) {
|
|
23
|
+
state.value = { ...state.value, ...data };
|
|
24
|
+
},
|
|
25
|
+
state: readonly(state) as DeepReadonly<Ref<ConnectorDataLayer>>,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
provide(DATA_LAYER_KEY, dataLayer);
|
|
29
|
+
|
|
30
|
+
return dataLayer;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function useDataLayer(): DeepReadonly<Ref<ConnectorDataLayer>> {
|
|
34
|
+
const dataLayer = inject<DataLayer | null>(DATA_LAYER_KEY, null);
|
|
35
|
+
if (dataLayer) {
|
|
36
|
+
return dataLayer.state;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// No dataLayer provided — return empty reactive ref
|
|
40
|
+
return readonly(ref<ConnectorDataLayer>({})) as DeepReadonly<
|
|
41
|
+
Ref<ConnectorDataLayer>
|
|
42
|
+
>;
|
|
43
|
+
}
|
|
@@ -1,5 +1,14 @@
|
|
|
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";
|
|
11
|
+
import { useDataLayer } from "./useDataLayer";
|
|
3
12
|
|
|
4
13
|
interface DeriveOptions {
|
|
5
14
|
control: Ref<{
|
|
@@ -8,20 +17,25 @@ interface DeriveOptions {
|
|
|
8
17
|
data: unknown;
|
|
9
18
|
}>;
|
|
10
19
|
handleChange: (path: string, value: unknown) => void;
|
|
20
|
+
/** When projection is active, pass projectedData so the comparison
|
|
21
|
+
* matches the projected (unwrapped) value rather than raw scope data. */
|
|
22
|
+
data?: Ref<unknown> | ComputedRef<unknown>;
|
|
11
23
|
}
|
|
12
24
|
|
|
13
|
-
export function useDerive({
|
|
25
|
+
export function useDerive({
|
|
26
|
+
control,
|
|
27
|
+
handleChange,
|
|
28
|
+
data: dataOverride,
|
|
29
|
+
}: DeriveOptions) {
|
|
14
30
|
// Get the root form data from JSONForms context
|
|
15
31
|
const injectedFormData = inject<{ value: unknown }>("formData", {
|
|
16
32
|
value: {},
|
|
17
33
|
});
|
|
18
34
|
const rootData = computed(() => injectedFormData.value || {});
|
|
19
35
|
|
|
20
|
-
// Get
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
});
|
|
24
|
-
const externalData = computed(() => injectedExternalData.value || {});
|
|
36
|
+
// Get data from the dataLayer
|
|
37
|
+
const dataLayerState = useDataLayer();
|
|
38
|
+
const dataLayerData = computed(() => dataLayerState.value || {});
|
|
25
39
|
|
|
26
40
|
// Extract derive configuration from uischema options
|
|
27
41
|
const deriveConfig = computed(() => {
|
|
@@ -34,9 +48,17 @@ export function useDerive({ control, handleChange }: DeriveOptions) {
|
|
|
34
48
|
};
|
|
35
49
|
});
|
|
36
50
|
|
|
37
|
-
//
|
|
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
|
+
|
|
59
|
+
// Watch for changes in form data and dataLayer and update derived field
|
|
38
60
|
watch(
|
|
39
|
-
[rootData,
|
|
61
|
+
[rootData, dataLayerData, deriveConfig],
|
|
40
62
|
([data, extData, config]) => {
|
|
41
63
|
if (!config.expression || config.mode !== "follow") {
|
|
42
64
|
return;
|
|
@@ -48,7 +70,31 @@ export function useDerive({ control, handleChange }: DeriveOptions) {
|
|
|
48
70
|
data,
|
|
49
71
|
extData,
|
|
50
72
|
);
|
|
51
|
-
|
|
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
|
+
|
|
97
|
+
if (derivedValue !== compareData) {
|
|
52
98
|
handleChange(control.value.path, derivedValue);
|
|
53
99
|
}
|
|
54
100
|
} catch (error) {
|
|
@@ -65,12 +111,12 @@ export function useDerive({ control, handleChange }: DeriveOptions) {
|
|
|
65
111
|
function resolveDeriveExpression(
|
|
66
112
|
expression: string,
|
|
67
113
|
data: unknown,
|
|
68
|
-
|
|
114
|
+
dataLayerData?: unknown,
|
|
69
115
|
): unknown {
|
|
70
|
-
// Handle
|
|
71
|
-
if (expression.startsWith("
|
|
72
|
-
const propertyPath = expression.slice(
|
|
73
|
-
return resolvePropertyPath(propertyPath,
|
|
116
|
+
// Handle dataLayer() syntax
|
|
117
|
+
if (expression.startsWith("dataLayer(") && expression.endsWith(")")) {
|
|
118
|
+
const propertyPath = expression.slice(10, -1); // Remove "dataLayer(" and ")"
|
|
119
|
+
return resolvePropertyPath(propertyPath, dataLayerData);
|
|
74
120
|
}
|
|
75
121
|
|
|
76
122
|
// Handle simple property paths like "country.name"
|
|
@@ -82,7 +128,7 @@ function resolveDeriveExpression(
|
|
|
82
128
|
return resolvePropertyPath(expression, data);
|
|
83
129
|
}
|
|
84
130
|
|
|
85
|
-
// For now, we'll only support simple property paths and
|
|
131
|
+
// For now, we'll only support simple property paths and dataLayer() calls
|
|
86
132
|
// Complex expressions would require a safe expression evaluator
|
|
87
133
|
return resolvePropertyPath(expression, data);
|
|
88
134
|
}
|
|
@@ -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
|
+
}
|