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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/README.md +101 -29
  2. package/dist/core/initFormData.d.ts +10 -0
  3. package/dist/core/initFormData.d.ts.map +1 -0
  4. package/dist/core/initFormData.js +99 -0
  5. package/dist/core/initFormData.js.map +1 -0
  6. package/dist/core/projection.d.ts +32 -0
  7. package/dist/core/projection.d.ts.map +1 -0
  8. package/dist/core/projection.js +74 -0
  9. package/dist/core/projection.js.map +1 -0
  10. package/dist/core/resolveScope.d.ts +11 -0
  11. package/dist/core/resolveScope.d.ts.map +1 -0
  12. package/dist/core/resolveScope.js +22 -0
  13. package/dist/core/resolveScope.js.map +1 -0
  14. package/dist/core/transforms.d.ts +8 -10
  15. package/dist/core/transforms.d.ts.map +1 -1
  16. package/dist/core/transforms.js +58 -13
  17. package/dist/core/transforms.js.map +1 -1
  18. package/dist/core/types.d.ts +8 -0
  19. package/dist/core/types.d.ts.map +1 -1
  20. package/dist/index.d.ts +5 -1
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +14 -3
  23. package/dist/index.js.map +1 -1
  24. package/dist/jsonforms-provider-protocols.css +2 -2
  25. package/dist/vue/components/ProviderAutocomplete.vue.d.ts.map +1 -1
  26. package/dist/vue/components/ProviderAutocomplete.vue.js +10 -5
  27. package/dist/vue/components/ProviderAutocomplete.vue.js.map +1 -1
  28. package/dist/vue/components/ProviderMultiSelect.vue.d.ts.map +1 -1
  29. package/dist/vue/components/ProviderMultiSelect.vue.js +1 -1
  30. package/dist/vue/components/ProviderMultiSelect.vue2.js +12 -7
  31. package/dist/vue/components/ProviderMultiSelect.vue2.js.map +1 -1
  32. package/dist/vue/components/ProviderSelect.vue.d.ts.map +1 -1
  33. package/dist/vue/components/ProviderSelect.vue.js +1 -1
  34. package/dist/vue/components/ProviderSelect.vue2.js +13 -6
  35. package/dist/vue/components/ProviderSelect.vue2.js.map +1 -1
  36. package/dist/vue/composables/useDataLayer.d.ts +10 -0
  37. package/dist/vue/composables/useDataLayer.d.ts.map +1 -0
  38. package/dist/vue/composables/useDataLayer.js +26 -0
  39. package/dist/vue/composables/useDataLayer.js.map +1 -0
  40. package/dist/vue/composables/useDerive.d.ts +5 -2
  41. package/dist/vue/composables/useDerive.d.ts.map +1 -1
  42. package/dist/vue/composables/useDerive.js +29 -12
  43. package/dist/vue/composables/useDerive.js.map +1 -1
  44. package/dist/vue/composables/useDeriveInitialValue.d.ts +36 -0
  45. package/dist/vue/composables/useDeriveInitialValue.d.ts.map +1 -0
  46. package/dist/vue/composables/useDeriveInitialValue.js +125 -0
  47. package/dist/vue/composables/useDeriveInitialValue.js.map +1 -0
  48. package/dist/vue/composables/useDirtyValidation.d.ts +9 -0
  49. package/dist/vue/composables/useDirtyValidation.d.ts.map +1 -0
  50. package/dist/vue/composables/useDirtyValidation.js +15 -0
  51. package/dist/vue/composables/useDirtyValidation.js.map +1 -0
  52. package/dist/vue/composables/useProjection.d.ts +41 -0
  53. package/dist/vue/composables/useProjection.d.ts.map +1 -0
  54. package/dist/vue/composables/useProjection.js +84 -0
  55. package/dist/vue/composables/useProjection.js.map +1 -0
  56. package/dist/vue/index.d.ts +7 -0
  57. package/dist/vue/index.d.ts.map +1 -1
  58. package/dist/vue/index.js +35 -27
  59. package/dist/vue/index.js.map +1 -1
  60. package/dist/vue/primevue/JfBoolean.vue.d.ts +9 -0
  61. package/dist/vue/primevue/JfBoolean.vue.d.ts.map +1 -1
  62. package/dist/vue/primevue/JfBoolean.vue.js +35 -13
  63. package/dist/vue/primevue/JfBoolean.vue.js.map +1 -1
  64. package/dist/vue/primevue/JfEnum.vue.d.ts +9 -0
  65. package/dist/vue/primevue/JfEnum.vue.d.ts.map +1 -1
  66. package/dist/vue/primevue/JfEnum.vue.js +31 -22
  67. package/dist/vue/primevue/JfEnum.vue.js.map +1 -1
  68. package/dist/vue/primevue/JfEnumArray.vue.d.ts +9 -0
  69. package/dist/vue/primevue/JfEnumArray.vue.d.ts.map +1 -1
  70. package/dist/vue/primevue/JfEnumArray.vue.js +33 -18
  71. package/dist/vue/primevue/JfEnumArray.vue.js.map +1 -1
  72. package/dist/vue/primevue/JfNumber.vue.d.ts +9 -0
  73. package/dist/vue/primevue/JfNumber.vue.d.ts.map +1 -1
  74. package/dist/vue/primevue/JfNumber.vue.js +31 -22
  75. package/dist/vue/primevue/JfNumber.vue.js.map +1 -1
  76. package/dist/vue/primevue/JfText.vue.d.ts +9 -0
  77. package/dist/vue/primevue/JfText.vue.d.ts.map +1 -1
  78. package/dist/vue/primevue/JfText.vue.js +40 -32
  79. package/dist/vue/primevue/JfText.vue.js.map +1 -1
  80. package/dist/vue/primevue/JfTextArea.vue.d.ts +9 -0
  81. package/dist/vue/primevue/JfTextArea.vue.d.ts.map +1 -1
  82. package/dist/vue/primevue/JfTextArea.vue.js +32 -18
  83. package/dist/vue/primevue/JfTextArea.vue.js.map +1 -1
  84. package/dist/vue/primevue/index.d.ts.map +1 -1
  85. package/dist/vue/primevue/index.js +100 -8
  86. package/dist/vue/primevue/index.js.map +1 -1
  87. package/package.json +3 -1
  88. package/src/core/initFormData.ts +189 -0
  89. package/src/core/projection.ts +136 -0
  90. package/src/core/resolveScope.ts +39 -0
  91. package/src/core/transforms.ts +118 -26
  92. package/src/core/types.ts +9 -0
  93. package/src/index.ts +7 -1
  94. package/src/vue/components/ProviderAutocomplete.vue +10 -5
  95. package/src/vue/components/ProviderMultiSelect.vue +14 -7
  96. package/src/vue/components/ProviderSelect.vue +15 -6
  97. package/src/vue/composables/useDataLayer.ts +43 -0
  98. package/src/vue/composables/useDerive.ts +62 -16
  99. package/src/vue/composables/useDeriveInitialValue.ts +195 -0
  100. package/src/vue/composables/useDirtyValidation.ts +20 -0
  101. package/src/vue/composables/useProjection.ts +181 -0
  102. package/src/vue/index.ts +35 -41
  103. package/src/vue/primevue/JfBoolean.vue +25 -7
  104. package/src/vue/primevue/JfEnum.vue +29 -22
  105. package/src/vue/primevue/JfEnumArray.vue +31 -16
  106. package/src/vue/primevue/JfNumber.vue +29 -22
  107. package/src/vue/primevue/JfText.vue +34 -27
  108. package/src/vue/primevue/JfTextArea.vue +29 -17
  109. package/src/vue/primevue/index.ts +114 -8
  110. package/src/vue/styles.css +26 -1
@@ -14,30 +14,38 @@ export interface FlattenTransform extends Transform {
14
14
  labelFormat?: string; // Optional format string like "{parent.name} → {name}"
15
15
  }
16
16
 
17
+ export type FilterOperator =
18
+ | "eq"
19
+ | "neq"
20
+ | "empty"
21
+ | "notEmpty"
22
+ | "gt"
23
+ | "gte"
24
+ | "lt"
25
+ | "lte"
26
+ | "contains";
27
+
28
+ export interface FilterCondition {
29
+ key: string;
30
+ operator?: FilterOperator; // Defaults to "eq" when values is provided, "exists" behavior when neither
31
+ values?: unknown[];
32
+ }
33
+
17
34
  export interface FilterTransform extends Transform {
18
35
  name: "filter";
19
- key: string; // The key to check
20
- values?: unknown[]; // Optional array of values to match against
36
+ key?: string; // Legacy: single key to check
37
+ values?: unknown[]; // Legacy: single values array
38
+ conditions?: FilterCondition[]; // Multi-condition filter (AND logic)
21
39
  }
22
40
 
23
41
  export type TransformStep = FlattenTransform | FilterTransform;
24
42
 
25
43
  export type TransformPipeline = TransformStep[];
26
44
 
27
- /**
28
- * Registry of transform functions
29
- */
30
45
  type TransformFunction = (items: unknown[], config: Transform) => unknown[];
31
46
 
32
47
  const transformRegistry: Record<string, TransformFunction> = {};
33
48
 
34
- /**
35
- * Register a transform function
36
- */
37
- export function registerTransform(name: string, fn: TransformFunction): void {
38
- transformRegistry[name] = fn;
39
- }
40
-
41
49
  /**
42
50
  * Apply a pipeline of transforms to data
43
51
  */
@@ -124,29 +132,113 @@ function flattenTransform(items: unknown[], config: Transform): unknown[] {
124
132
  return flattened;
125
133
  }
126
134
 
135
+ function isEmpty(value: unknown): boolean {
136
+ if (value === null || value === undefined) return true;
137
+ if (Array.isArray(value)) return value.length === 0;
138
+ if (typeof value === "string") return value.length === 0;
139
+ return false;
140
+ }
141
+
142
+ function evaluateCondition(
143
+ itemObj: Record<string, unknown>,
144
+ condition: FilterCondition,
145
+ ): boolean {
146
+ const value = itemObj[condition.key];
147
+ const operator = condition.operator ?? (condition.values ? "eq" : "eq");
148
+
149
+ switch (operator) {
150
+ case "eq":
151
+ if (!condition.values || condition.values.length === 0) {
152
+ return condition.key in itemObj;
153
+ }
154
+ return condition.values.includes(value);
155
+ case "neq":
156
+ if (!condition.values || condition.values.length === 0) {
157
+ return !(condition.key in itemObj);
158
+ }
159
+ return !condition.values.includes(value);
160
+ case "empty":
161
+ return isEmpty(value);
162
+ case "notEmpty":
163
+ return !isEmpty(value);
164
+ case "gt":
165
+ return (
166
+ typeof value === "number" &&
167
+ condition.values !== undefined &&
168
+ value > (condition.values[0] as number)
169
+ );
170
+ case "gte":
171
+ return (
172
+ typeof value === "number" &&
173
+ condition.values !== undefined &&
174
+ value >= (condition.values[0] as number)
175
+ );
176
+ case "lt":
177
+ return (
178
+ typeof value === "number" &&
179
+ condition.values !== undefined &&
180
+ value < (condition.values[0] as number)
181
+ );
182
+ case "lte":
183
+ return (
184
+ typeof value === "number" &&
185
+ condition.values !== undefined &&
186
+ value <= (condition.values[0] as number)
187
+ );
188
+ case "contains":
189
+ if (typeof value === "string" && condition.values) {
190
+ return condition.values.some((v) => value.includes(String(v)));
191
+ }
192
+ if (Array.isArray(value) && condition.values) {
193
+ return condition.values.some((v) => value.includes(v));
194
+ }
195
+ return false;
196
+ default:
197
+ return false;
198
+ }
199
+ }
200
+
127
201
  /**
128
- * Filter transform - filters items based on a key and optional values
202
+ * Filter transform - filters items based on conditions
203
+ *
204
+ * Supports legacy single key/values syntax and new multi-condition syntax.
205
+ * When using conditions, all conditions must match (AND logic).
206
+ *
207
+ * Operators:
208
+ * eq - item[key] matches one of values (default)
209
+ * neq - item[key] does NOT match any of values
210
+ * empty - item[key] is null, undefined, empty array, or empty string
211
+ * notEmpty - inverse of empty
212
+ * gt - item[key] > values[0]
213
+ * gte - item[key] >= values[0]
214
+ * lt - item[key] < values[0]
215
+ * lte - item[key] <= values[0]
216
+ * contains - string includes substring, or array includes value
129
217
  */
130
218
  function filterTransform(items: unknown[], config: Transform): unknown[] {
131
219
  const filterConfig = config as FilterTransform;
132
- const { key, values } = filterConfig;
220
+
221
+ // Build conditions array from either new or legacy syntax
222
+ let conditions: FilterCondition[];
223
+
224
+ if (filterConfig.conditions) {
225
+ conditions = filterConfig.conditions;
226
+ } else if (filterConfig.key) {
227
+ // Legacy single key/values syntax
228
+ conditions = [{ key: filterConfig.key, values: filterConfig.values }];
229
+ } else {
230
+ return items;
231
+ }
133
232
 
134
233
  return items.filter((item) => {
135
234
  if (typeof item !== "object" || item === null) return false;
136
-
137
235
  const itemObj = item as Record<string, unknown>;
138
-
139
- // If no values array provided, just check if the key exists
140
- if (!values || values.length === 0) {
141
- return key in itemObj;
142
- }
143
-
144
- // If values array provided, check if item[key] matches any of the values
145
- const itemValue = itemObj[key];
146
- return values.includes(itemValue);
236
+ return conditions.every((condition) =>
237
+ evaluateCondition(itemObj, condition),
238
+ );
147
239
  });
148
240
  }
149
241
 
150
242
  // Register built-in transforms
151
- registerTransform("flatten", flattenTransform);
152
- registerTransform("filter", filterTransform);
243
+ transformRegistry["flatten"] = flattenTransform;
244
+ transformRegistry["filter"] = filterTransform;
package/src/core/types.ts CHANGED
@@ -44,3 +44,12 @@ export interface AuthConfig {
44
44
  token?: string | (() => string); // Generic token auth
45
45
  [key: string]: unknown; // Custom auth fields
46
46
  }
47
+
48
+ export interface ConnectorDataLayer {
49
+ dataset_name?: string;
50
+ dataset_description?: string;
51
+ dataset_id?: number;
52
+ profile_id?: string;
53
+ profile_name?: string;
54
+ datasetWriteMode?: "append" | "overwrite";
55
+ }
package/src/index.ts CHANGED
@@ -3,15 +3,18 @@ import { cache as globalCache } from "./core/cache";
3
3
  import { registry as globalRegistry } from "./core/registry";
4
4
  import type { Protocol, AuthConfig } from "./core/types";
5
5
 
6
- console.log("[jsonforms-provider-protocols] loaded v2.10.0");
6
+ console.log("[jsonforms-provider-protocols] loaded v2.10.0 (symlink)");
7
7
 
8
8
  export { cache } from "./core/cache";
9
9
  export * from "./core/jsonpath";
10
10
  export { registry } from "./core/registry";
11
11
  export * from "./core/templating";
12
12
  export * from "./core/transforms";
13
+ export * from "./core/projection";
14
+ export * from "./core/resolveScope";
13
15
  // Core exports
14
16
  export * from "./core/types";
17
+ export { initFormDataFromSchema } from "./core/initFormData";
15
18
 
16
19
  // Protocol exports
17
20
  export { RestApiProtocol } from "./protocols/rest_api";
@@ -24,6 +27,8 @@ export {
24
27
  ProviderSelect,
25
28
  ProviderMultiSelect,
26
29
  useProvider,
30
+ createDataLayer,
31
+ useDataLayer,
27
32
  JfText,
28
33
  JfTextArea,
29
34
  JfNumber,
@@ -31,6 +36,7 @@ export {
31
36
  JfEnumArray,
32
37
  JfBoolean,
33
38
  } from "./vue";
39
+ export type { DataLayer } from "./vue";
34
40
 
35
41
  export interface ProviderConfig {
36
42
  protocols?: Protocol[];
@@ -3,6 +3,7 @@ import type { ControlElement, JsonSchema } from "@jsonforms/core";
3
3
  import { useJsonFormsControl } from "@jsonforms/vue";
4
4
  import { computed, ref, watch } from "vue";
5
5
  import { useProvider } from "../composables/useProvider";
6
+ import { useProjection } from "../composables/useProjection";
6
7
  import AutoComplete from "primevue/autocomplete";
7
8
 
8
9
  const props = defineProps<{
@@ -10,7 +11,11 @@ const props = defineProps<{
10
11
  schema: JsonSchema;
11
12
  path: string;
12
13
  }>();
13
- const { control, handleChange } = useJsonFormsControl(props);
14
+ const { control, handleChange: rawHandleChange } = useJsonFormsControl(props);
15
+ const { projectedData, handleProjectedChange: handleChange } = useProjection(
16
+ control,
17
+ rawHandleChange,
18
+ );
14
19
 
15
20
  const binding = computed(() => {
16
21
  const provider = control.value.uischema?.options?.provider;
@@ -33,7 +38,7 @@ watch(query, () => {
33
38
  });
34
39
 
35
40
  const value = computed({
36
- get: () => control.value.data,
41
+ get: () => projectedData.value,
37
42
  set: (v) => handleChange(control.value.path, v),
38
43
  });
39
44
 
@@ -51,11 +56,11 @@ const onSelect = (event: { value?: { value?: unknown } | unknown }) => {
51
56
  </script>
52
57
 
53
58
  <template>
54
- <div class="flex flex-col gap-1">
55
- <label v-if="control.schema.title" class="text-color text-left">{{
59
+ <div class="jf-control">
60
+ <label v-if="control.schema.title" class="jf-label">{{
56
61
  control.schema.title
57
62
  }}</label>
58
- <div v-if="control.description" class="text-color-secondary text-left">
63
+ <div v-if="control.description" class="jf-description">
59
64
  {{ control.description }}
60
65
  </div>
61
66
  <AutoComplete
@@ -3,6 +3,7 @@ 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 { useProjection } from "../composables/useProjection";
6
7
  import { shouldAutoSelectMulti } from "../utils/autoSelect";
7
8
  import MultiSelect from "primevue/multiselect";
8
9
 
@@ -11,7 +12,11 @@ const props = defineProps<{
11
12
  schema: JsonSchema;
12
13
  path: string;
13
14
  }>();
14
- const { control, handleChange } = useJsonFormsControl(props);
15
+ const { control, handleChange: rawHandleChange } = useJsonFormsControl(props);
16
+ const { projectedData, handleProjectedChange: handleChange } = useProjection(
17
+ control,
18
+ rawHandleChange,
19
+ );
15
20
 
16
21
  const binding = computed(() => {
17
22
  const provider = control.value.uischema?.options?.provider;
@@ -53,7 +58,9 @@ watch(
53
58
  control.value.uischema?.options?.autoSelectSingle === true,
54
59
  isLoading,
55
60
  items: newItems,
56
- currentValue: Array.isArray(control.value.data) ? control.value.data : [],
61
+ currentValue: Array.isArray(projectedData.value)
62
+ ? projectedData.value
63
+ : [],
57
64
  });
58
65
 
59
66
  if (valueToSelect !== null) {
@@ -74,13 +81,13 @@ const sameSet = (a: unknown[], b: unknown[]) => {
74
81
  // v-model with guard to avoid recursive updates
75
82
  const value = computed({
76
83
  get() {
77
- const curr = Array.isArray(control.value.data) ? control.value.data : [];
84
+ const curr = Array.isArray(projectedData.value) ? projectedData.value : [];
78
85
  // return a fresh copy so MultiSelect can't mutate JSONForms' array in place
79
86
  return [...curr];
80
87
  },
81
88
  set(val) {
82
89
  const next = Array.isArray(val) ? [...val] : [];
83
- const curr = Array.isArray(control.value.data) ? control.value.data : [];
90
+ const curr = Array.isArray(projectedData.value) ? projectedData.value : [];
84
91
  if (!sameSet(curr, next)) handleChange(control.value.path, next);
85
92
  },
86
93
  });
@@ -96,11 +103,11 @@ const placeholder = computed(() => {
96
103
  </script>
97
104
 
98
105
  <template>
99
- <div class="flex flex-col gap-2">
100
- <label v-if="control.schema.title" class="text-color text-left">{{
106
+ <div class="jf-control">
107
+ <label v-if="control.schema.title" class="jf-label">{{
101
108
  control.schema.title
102
109
  }}</label>
103
- <div v-if="control.description" class="text-color-secondary text-left">
110
+ <div v-if="control.description" class="jf-description">
104
111
  {{ control.description }}
105
112
  </div>
106
113
  <MultiSelect
@@ -3,6 +3,8 @@ 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";
7
9
  import Dropdown from "primevue/dropdown";
8
10
 
@@ -11,7 +13,11 @@ const props = defineProps<{
11
13
  schema: JsonSchema;
12
14
  path: string;
13
15
  }>();
14
- const { control, handleChange } = useJsonFormsControl(props);
16
+ const { control, handleChange: rawHandleChange } = useJsonFormsControl(props);
17
+ const { projectedData, handleProjectedChange: handleChange } = useProjection(
18
+ control,
19
+ rawHandleChange,
20
+ );
15
21
 
16
22
  const binding = computed(() => {
17
23
  const provider = control.value.uischema?.options?.provider;
@@ -44,6 +50,9 @@ const { items, loading, error } = useProvider(binding, {
44
50
 
45
51
  // Provider will automatically reload when rootData changes due to reactive cache key
46
52
 
53
+ // deriveInitialValue — async API-based initial value seeding
54
+ useDeriveInitialValue({ control, handleChange });
55
+
47
56
  // Auto-select when provider returns only one item (enabled by default)
48
57
  watch(
49
58
  [items, loading],
@@ -53,7 +62,7 @@ watch(
53
62
  control.value.uischema?.options?.autoSelectSingle !== false,
54
63
  isLoading,
55
64
  items: newItems,
56
- currentValue: control.value.data,
65
+ currentValue: projectedData.value,
57
66
  });
58
67
 
59
68
  if (valueToSelect !== null) {
@@ -64,7 +73,7 @@ watch(
64
73
  );
65
74
 
66
75
  const value = computed({
67
- get: () => control.value.data,
76
+ get: () => projectedData.value,
68
77
  set: (v) => handleChange(control.value.path, v),
69
78
  });
70
79
 
@@ -79,11 +88,11 @@ const placeholder = computed(() => {
79
88
  </script>
80
89
 
81
90
  <template>
82
- <div class="flex flex-col gap-2">
83
- <label v-if="control.schema.title" class="text-color text-left">{{
91
+ <div class="jf-control">
92
+ <label v-if="control.schema.title" class="jf-label">{{
84
93
  control.schema.title
85
94
  }}</label>
86
- <div v-if="control.description" class="text-color-secondary text-left">
95
+ <div v-if="control.description" class="jf-description">
87
96
  {{ control.description }}
88
97
  </div>
89
98
  <Dropdown
@@ -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 { computed, watch, inject, type Ref } from "vue";
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({ control, handleChange }: DeriveOptions) {
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 external data from context if available
21
- const injectedExternalData = inject<{ value: unknown }>("externalData", {
22
- value: {},
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
- // Watch for changes in form data and external data and update derived field
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, externalData, deriveConfig],
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
- if (derivedValue !== control.value.data) {
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
- externalData?: unknown,
114
+ dataLayerData?: unknown,
69
115
  ): unknown {
70
- // Handle externalData() syntax
71
- if (expression.startsWith("externalData(") && expression.endsWith(")")) {
72
- const propertyPath = expression.slice(13, -1); // Remove "externalData(" and ")"
73
- return resolvePropertyPath(propertyPath, externalData);
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 externalData() calls
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
  }