@narrative.io/jsonforms-provider-protocols 3.0.0-beta.1 → 3.0.0-beta.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/dist/core/initFormData.d.ts +10 -0
  2. package/dist/core/initFormData.d.ts.map +1 -0
  3. package/dist/core/initFormData.js +99 -0
  4. package/dist/core/initFormData.js.map +1 -0
  5. package/dist/core/projection.d.ts.map +1 -1
  6. package/dist/core/projection.js.map +1 -1
  7. package/dist/core/transforms.d.ts.map +1 -1
  8. package/dist/core/transforms.js +3 -1
  9. package/dist/core/transforms.js.map +1 -1
  10. package/dist/core/types.d.ts +1 -0
  11. package/dist/core/types.d.ts.map +1 -1
  12. package/dist/index.d.ts +1 -0
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +2 -0
  15. package/dist/index.js.map +1 -1
  16. package/dist/jsonforms-provider-protocols.css +2 -2
  17. package/dist/vue/components/ProviderAutocomplete.vue.d.ts.map +1 -1
  18. package/dist/vue/components/ProviderAutocomplete.vue.js +8 -5
  19. package/dist/vue/components/ProviderAutocomplete.vue.js.map +1 -1
  20. package/dist/vue/components/ProviderMultiSelect.vue.d.ts.map +1 -1
  21. package/dist/vue/components/ProviderMultiSelect.vue.js +1 -1
  22. package/dist/vue/components/ProviderMultiSelect.vue2.js +9 -6
  23. package/dist/vue/components/ProviderMultiSelect.vue2.js.map +1 -1
  24. package/dist/vue/components/ProviderSelect.vue.d.ts.map +1 -1
  25. package/dist/vue/components/ProviderSelect.vue.js +1 -1
  26. package/dist/vue/components/ProviderSelect.vue2.js +11 -6
  27. package/dist/vue/components/ProviderSelect.vue2.js.map +1 -1
  28. package/dist/vue/composables/useDataLayer.d.ts +1 -0
  29. package/dist/vue/composables/useDataLayer.d.ts.map +1 -1
  30. package/dist/vue/composables/useDataLayer.js +1 -0
  31. package/dist/vue/composables/useDataLayer.js.map +1 -1
  32. package/dist/vue/composables/useDerive.d.ts +1 -1
  33. package/dist/vue/composables/useDerive.d.ts.map +1 -1
  34. package/dist/vue/composables/useDerive.js +19 -2
  35. package/dist/vue/composables/useDerive.js.map +1 -1
  36. package/dist/vue/composables/useDeriveInitialValue.d.ts +36 -0
  37. package/dist/vue/composables/useDeriveInitialValue.d.ts.map +1 -0
  38. package/dist/vue/composables/useDeriveInitialValue.js +125 -0
  39. package/dist/vue/composables/useDeriveInitialValue.js.map +1 -0
  40. package/dist/vue/composables/useDirtyValidation.d.ts +9 -0
  41. package/dist/vue/composables/useDirtyValidation.d.ts.map +1 -0
  42. package/dist/vue/composables/useDirtyValidation.js +15 -0
  43. package/dist/vue/composables/useDirtyValidation.js.map +1 -0
  44. package/dist/vue/composables/useProjection.d.ts +6 -0
  45. package/dist/vue/composables/useProjection.d.ts.map +1 -1
  46. package/dist/vue/composables/useProjection.js +54 -3
  47. package/dist/vue/composables/useProjection.js.map +1 -1
  48. package/dist/vue/composables/useProvider.d.ts +2 -2
  49. package/dist/vue/composables/useProvider.d.ts.map +1 -1
  50. package/dist/vue/composables/useProvider.js +14 -10
  51. package/dist/vue/composables/useProvider.js.map +1 -1
  52. package/dist/vue/index.d.ts +3 -0
  53. package/dist/vue/index.d.ts.map +1 -1
  54. package/dist/vue/index.js +32 -10
  55. package/dist/vue/index.js.map +1 -1
  56. package/dist/vue/primevue/JfBoolean.vue.d.ts.map +1 -1
  57. package/dist/vue/primevue/JfBoolean.vue.js +26 -9
  58. package/dist/vue/primevue/JfBoolean.vue.js.map +1 -1
  59. package/dist/vue/primevue/JfEnum.vue.d.ts.map +1 -1
  60. package/dist/vue/primevue/JfEnum.vue.js +21 -17
  61. package/dist/vue/primevue/JfEnum.vue.js.map +1 -1
  62. package/dist/vue/primevue/JfEnumArray.vue.d.ts.map +1 -1
  63. package/dist/vue/primevue/JfEnumArray.vue.js +22 -12
  64. package/dist/vue/primevue/JfEnumArray.vue.js.map +1 -1
  65. package/dist/vue/primevue/JfNumber.vue.d.ts.map +1 -1
  66. package/dist/vue/primevue/JfNumber.vue.js +21 -17
  67. package/dist/vue/primevue/JfNumber.vue.js.map +1 -1
  68. package/dist/vue/primevue/JfText.vue.d.ts.map +1 -1
  69. package/dist/vue/primevue/JfText.vue.js +29 -26
  70. package/dist/vue/primevue/JfText.vue.js.map +1 -1
  71. package/dist/vue/primevue/JfTextArea.vue.d.ts.map +1 -1
  72. package/dist/vue/primevue/JfTextArea.vue.js +22 -13
  73. package/dist/vue/primevue/JfTextArea.vue.js.map +1 -1
  74. package/dist/vue/primevue/index.d.ts.map +1 -1
  75. package/dist/vue/primevue/index.js +93 -16
  76. package/dist/vue/primevue/index.js.map +1 -1
  77. package/dist/vue/utils/autoSelect.js.map +1 -1
  78. package/package.json +3 -1
  79. package/src/core/initFormData.ts +189 -0
  80. package/src/core/projection.ts +5 -5
  81. package/src/core/transforms.ts +33 -6
  82. package/src/core/types.ts +1 -0
  83. package/src/index.ts +1 -0
  84. package/src/vue/components/ProviderAutocomplete.vue +8 -5
  85. package/src/vue/components/ProviderMultiSelect.vue +13 -8
  86. package/src/vue/components/ProviderSelect.vue +14 -7
  87. package/src/vue/composables/useDataLayer.ts +1 -1
  88. package/src/vue/composables/useDerive.ts +46 -3
  89. package/src/vue/composables/useDeriveInitialValue.ts +195 -0
  90. package/src/vue/composables/useDirtyValidation.ts +20 -0
  91. package/src/vue/composables/useProjection.ts +108 -1
  92. package/src/vue/composables/useProvider.ts +28 -11
  93. package/src/vue/index.ts +29 -9
  94. package/src/vue/primevue/JfBoolean.vue +19 -6
  95. package/src/vue/primevue/JfEnum.vue +23 -21
  96. package/src/vue/primevue/JfEnumArray.vue +25 -15
  97. package/src/vue/primevue/JfNumber.vue +22 -20
  98. package/src/vue/primevue/JfText.vue +26 -24
  99. package/src/vue/primevue/JfTextArea.vue +22 -15
  100. package/src/vue/primevue/index.ts +104 -23
  101. package/src/vue/styles.css +26 -1
  102. package/src/vue/utils/autoSelect.ts +2 -2
@@ -0,0 +1,189 @@
1
+ /**
2
+ * Initialize a form data object from a JSON Schema.
3
+ * Resolves $ref, const, default, oneOf/discriminator, and typed empty values.
4
+ *
5
+ * @param schema - The full JSON Schema (must include $defs if $refs are used)
6
+ * @param seed - Optional existing data to merge (seed values take priority)
7
+ * @returns A data object with all schema-defined fields initialized
8
+ */
9
+ export function initFormDataFromSchema(
10
+ schema: Record<string, unknown>,
11
+ seed?: Record<string, unknown>,
12
+ ): Record<string, unknown> {
13
+ const result = initProperty(schema, schema, seed) as Record<string, unknown>;
14
+
15
+ // If result is an object and seed has extra keys not in schema, preserve them
16
+ if (
17
+ result &&
18
+ typeof result === "object" &&
19
+ !Array.isArray(result) &&
20
+ seed &&
21
+ typeof seed === "object"
22
+ ) {
23
+ const schemaKeys = new Set(
24
+ Object.keys(
25
+ (resolveRef(schema, schema) as Record<string, unknown>)?.properties ??
26
+ {},
27
+ ),
28
+ );
29
+ for (const key of Object.keys(seed)) {
30
+ if (!schemaKeys.has(key) && !(key in result)) {
31
+ result[key] = seed[key];
32
+ }
33
+ }
34
+ }
35
+
36
+ return result ?? {};
37
+ }
38
+
39
+ /**
40
+ * Resolve a $ref pointer against the root schema's $defs.
41
+ * Supports nested $ref chains.
42
+ */
43
+ function resolveRef(
44
+ property: Record<string, unknown>,
45
+ root: Record<string, unknown>,
46
+ seen?: Set<string>,
47
+ ): Record<string, unknown> {
48
+ if (!property || typeof property !== "object") return property;
49
+
50
+ const ref = property.$ref as string | undefined;
51
+ if (!ref) return property;
52
+
53
+ // Guard against circular refs
54
+ const visited = seen ?? new Set<string>();
55
+ if (visited.has(ref)) return property;
56
+ visited.add(ref);
57
+
58
+ const resolved = resolvePointer(root, ref);
59
+ if (!resolved) return property;
60
+
61
+ // Continue resolving if the result itself has a $ref
62
+ return resolveRef(resolved as Record<string, unknown>, root, visited);
63
+ }
64
+
65
+ /**
66
+ * Resolve a JSON pointer like "#/$defs/Price" against an object.
67
+ */
68
+ function resolvePointer(
69
+ obj: Record<string, unknown>,
70
+ pointer: string,
71
+ ): unknown {
72
+ if (!pointer.startsWith("#/")) return undefined;
73
+ const parts = pointer.slice(2).split("/");
74
+ let current: unknown = obj;
75
+ for (const part of parts) {
76
+ if (current && typeof current === "object" && part in current) {
77
+ current = (current as Record<string, unknown>)[part];
78
+ } else {
79
+ return undefined;
80
+ }
81
+ }
82
+ return current;
83
+ }
84
+
85
+ /**
86
+ * Initialize a single property value based on its schema definition.
87
+ */
88
+ function initProperty(
89
+ property: Record<string, unknown>,
90
+ root: Record<string, unknown>,
91
+ seed?: unknown,
92
+ ): unknown {
93
+ if (!property || typeof property !== "object") return null;
94
+
95
+ // Resolve $ref first
96
+ const resolved = resolveRef(property, root);
97
+
98
+ // Handle oneOf with discriminator — pick first variant
99
+ if (Array.isArray(resolved.oneOf)) {
100
+ return initOneOf(resolved, root, seed);
101
+ }
102
+
103
+ // Priority 1: seed wins (for object types, we merge recursively below)
104
+ // For non-object types, return seed directly if present
105
+ const type = resolved.type as string | undefined;
106
+ if (seed !== undefined && seed !== null && type !== "object") {
107
+ return seed;
108
+ }
109
+
110
+ // Priority 2: const
111
+ if ("const" in resolved) {
112
+ return resolved.const;
113
+ }
114
+
115
+ // Priority 3: single-value enum
116
+ if (
117
+ Array.isArray(resolved.enum) &&
118
+ (resolved.enum as unknown[]).length === 1
119
+ ) {
120
+ return (resolved.enum as unknown[])[0];
121
+ }
122
+
123
+ // Priority 4: default
124
+ if ("default" in resolved) {
125
+ return resolved.default;
126
+ }
127
+
128
+ // Priority 5: typed empty values
129
+ switch (type) {
130
+ case "object":
131
+ return initObject(resolved, root, seed as Record<string, unknown>);
132
+ case "array":
133
+ return seed !== undefined && seed !== null ? seed : [];
134
+ case "string":
135
+ return "";
136
+ case "boolean":
137
+ return false;
138
+ case "number":
139
+ case "integer":
140
+ return null;
141
+ default:
142
+ return null;
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Initialize an object type by recursing into its properties.
148
+ */
149
+ function initObject(
150
+ schema: Record<string, unknown>,
151
+ root: Record<string, unknown>,
152
+ seed?: Record<string, unknown>,
153
+ ): Record<string, unknown> {
154
+ const properties = schema.properties as
155
+ | Record<string, Record<string, unknown>>
156
+ | undefined;
157
+ if (!properties) {
158
+ // Object with no defined properties — return seed or empty object
159
+ return seed && typeof seed === "object" ? { ...seed } : {};
160
+ }
161
+
162
+ const result: Record<string, unknown> = {};
163
+
164
+ for (const [key, propSchema] of Object.entries(properties)) {
165
+ const seedValue = seed && typeof seed === "object" ? seed[key] : undefined;
166
+ result[key] = initProperty(propSchema, root, seedValue);
167
+ }
168
+
169
+ return result;
170
+ }
171
+
172
+ /**
173
+ * Handle oneOf schemas — pick the first variant and initialize it.
174
+ */
175
+ function initOneOf(
176
+ schema: Record<string, unknown>,
177
+ root: Record<string, unknown>,
178
+ seed?: unknown,
179
+ ): unknown {
180
+ const variants = schema.oneOf as Record<string, unknown>[];
181
+ if (!variants || variants.length === 0) return null;
182
+
183
+ const first = variants[0];
184
+ if (!first) return null;
185
+
186
+ // Pick first variant, resolve its $ref if needed
187
+ const firstVariant = resolveRef(first, root);
188
+ return initProperty(firstVariant, root, seed);
189
+ }
@@ -26,10 +26,7 @@ export function parseProjectionPath(path: string): ProjectionSegment[] {
26
26
  * Read a value from `data` by following the projection path.
27
27
  * Returns `undefined` if any segment along the path is missing.
28
28
  */
29
- export function getProjectedValue(
30
- data: unknown,
31
- path: string,
32
- ): unknown {
29
+ export function getProjectedValue(data: unknown, path: string): unknown {
33
30
  const segments = parseProjectionPath(path);
34
31
  let current: unknown = data;
35
32
 
@@ -85,7 +82,10 @@ function setAtPath(
85
82
  } else {
86
83
  // Object key — ensure we have an object
87
84
  const obj: Record<string, unknown> =
88
- current !== null && current !== undefined && typeof current === "object" && !Array.isArray(current)
85
+ current !== null &&
86
+ current !== undefined &&
87
+ typeof current === "object" &&
88
+ !Array.isArray(current)
89
89
  ? { ...(current as Record<string, unknown>) }
90
90
  : {};
91
91
  obj[seg] = setAtPath(obj[seg], segments, index + 1, value);
@@ -14,7 +14,16 @@ export interface FlattenTransform extends Transform {
14
14
  labelFormat?: string; // Optional format string like "{parent.name} → {name}"
15
15
  }
16
16
 
17
- export type FilterOperator = "eq" | "neq" | "empty" | "notEmpty" | "gt" | "gte" | "lt" | "lte" | "contains";
17
+ export type FilterOperator =
18
+ | "eq"
19
+ | "neq"
20
+ | "empty"
21
+ | "notEmpty"
22
+ | "gt"
23
+ | "gte"
24
+ | "lt"
25
+ | "lte"
26
+ | "contains";
18
27
 
19
28
  export interface FilterCondition {
20
29
  key: string;
@@ -153,13 +162,29 @@ function evaluateCondition(
153
162
  case "notEmpty":
154
163
  return !isEmpty(value);
155
164
  case "gt":
156
- return typeof value === "number" && condition.values !== undefined && value > (condition.values[0] as number);
165
+ return (
166
+ typeof value === "number" &&
167
+ condition.values !== undefined &&
168
+ value > (condition.values[0] as number)
169
+ );
157
170
  case "gte":
158
- return typeof value === "number" && condition.values !== undefined && value >= (condition.values[0] as number);
171
+ return (
172
+ typeof value === "number" &&
173
+ condition.values !== undefined &&
174
+ value >= (condition.values[0] as number)
175
+ );
159
176
  case "lt":
160
- return typeof value === "number" && condition.values !== undefined && value < (condition.values[0] as number);
177
+ return (
178
+ typeof value === "number" &&
179
+ condition.values !== undefined &&
180
+ value < (condition.values[0] as number)
181
+ );
161
182
  case "lte":
162
- return typeof value === "number" && condition.values !== undefined && value <= (condition.values[0] as number);
183
+ return (
184
+ typeof value === "number" &&
185
+ condition.values !== undefined &&
186
+ value <= (condition.values[0] as number)
187
+ );
163
188
  case "contains":
164
189
  if (typeof value === "string" && condition.values) {
165
190
  return condition.values.some((v) => value.includes(String(v)));
@@ -208,7 +233,9 @@ function filterTransform(items: unknown[], config: Transform): unknown[] {
208
233
  return items.filter((item) => {
209
234
  if (typeof item !== "object" || item === null) return false;
210
235
  const itemObj = item as Record<string, unknown>;
211
- return conditions.every((condition) => evaluateCondition(itemObj, condition));
236
+ return conditions.every((condition) =>
237
+ evaluateCondition(itemObj, condition),
238
+ );
212
239
  });
213
240
  }
214
241
 
package/src/core/types.ts CHANGED
@@ -51,4 +51,5 @@ export interface ConnectorDataLayer {
51
51
  dataset_id?: number;
52
52
  profile_id?: string;
53
53
  profile_name?: string;
54
+ datasetWriteMode?: "append" | "overwrite";
54
55
  }
package/src/index.ts CHANGED
@@ -14,6 +14,7 @@ export * from "./core/projection";
14
14
  export * from "./core/resolveScope";
15
15
  // Core exports
16
16
  export * from "./core/types";
17
+ export { initFormDataFromSchema } from "./core/initFormData";
17
18
 
18
19
  // Protocol exports
19
20
  export { RestApiProtocol } from "./protocols/rest_api";
@@ -12,7 +12,10 @@ const props = defineProps<{
12
12
  path: string;
13
13
  }>();
14
14
  const { control, handleChange: rawHandleChange } = useJsonFormsControl(props);
15
- const { projectedData, handleProjectedChange: handleChange } = useProjection(control, rawHandleChange);
15
+ const { projectedData, handleProjectedChange: handleChange } = useProjection(
16
+ control,
17
+ rawHandleChange,
18
+ );
16
19
 
17
20
  const binding = computed(() => {
18
21
  const provider = control.value.uischema?.options?.provider;
@@ -53,16 +56,16 @@ const onSelect = (event: { value?: { value?: unknown } | unknown }) => {
53
56
  </script>
54
57
 
55
58
  <template>
56
- <div class="flex flex-column gap-1">
57
- <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">{{
58
61
  control.schema.title
59
62
  }}</label>
60
- <div v-if="control.description" class="text-color-secondary text-left">
63
+ <div v-if="control.description" class="jf-description">
61
64
  {{ control.description }}
62
65
  </div>
63
66
  <AutoComplete
64
67
  v-model="value"
65
- class="w-full"
68
+ class="w-full!"
66
69
  :suggestions="items"
67
70
  option-label="label"
68
71
  :placeholder="placeholder"
@@ -13,7 +13,10 @@ const props = defineProps<{
13
13
  path: string;
14
14
  }>();
15
15
  const { control, handleChange: rawHandleChange } = useJsonFormsControl(props);
16
- const { projectedData, handleProjectedChange: handleChange } = useProjection(control, rawHandleChange);
16
+ const { projectedData, handleProjectedChange: handleChange } = useProjection(
17
+ control,
18
+ rawHandleChange,
19
+ );
17
20
 
18
21
  const binding = computed(() => {
19
22
  const provider = control.value.uischema?.options?.provider;
@@ -41,7 +44,7 @@ const rootData = computed(() => injectedFormData.value || {});
41
44
  const { items, loading, error } = useProvider(binding, {
42
45
  data: rootData, // Pass the reactive reference
43
46
  path: control.value.path,
44
- dependsOnValues: depValues.value,
47
+ dependsOnValues: depValues,
45
48
  });
46
49
 
47
50
  // Provider will automatically reload when rootData changes due to reactive cache key
@@ -55,14 +58,16 @@ watch(
55
58
  control.value.uischema?.options?.autoSelectSingle === true,
56
59
  isLoading,
57
60
  items: newItems,
58
- currentValue: Array.isArray(projectedData.value) ? projectedData.value : [],
61
+ currentValue: Array.isArray(projectedData.value)
62
+ ? projectedData.value
63
+ : [],
59
64
  });
60
65
 
61
66
  if (valueToSelect !== null) {
62
67
  handleChange(control.value.path, valueToSelect);
63
68
  }
64
69
  },
65
- { immediate: true }
70
+ { immediate: true },
66
71
  );
67
72
 
68
73
  // order-insensitive shallow equality for primitive arrays
@@ -98,16 +103,16 @@ const placeholder = computed(() => {
98
103
  </script>
99
104
 
100
105
  <template>
101
- <div class="flex flex-column gap-2">
102
- <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">{{
103
108
  control.schema.title
104
109
  }}</label>
105
- <div v-if="control.description" class="text-color-secondary text-left">
110
+ <div v-if="control.description" class="jf-description">
106
111
  {{ control.description }}
107
112
  </div>
108
113
  <MultiSelect
109
114
  v-model="value"
110
- class="w-full"
115
+ class="w-full!"
111
116
  :options="items"
112
117
  option-label="label"
113
118
  option-value="value"
@@ -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 { useDeriveInitialValue } from "../composables/useDeriveInitialValue";
6
7
  import { useProjection } from "../composables/useProjection";
7
8
  import { shouldAutoSelect } from "../utils/autoSelect";
8
9
  import Dropdown from "primevue/dropdown";
@@ -13,7 +14,10 @@ const props = defineProps<{
13
14
  path: string;
14
15
  }>();
15
16
  const { control, handleChange: rawHandleChange } = useJsonFormsControl(props);
16
- const { projectedData, handleProjectedChange: handleChange } = useProjection(control, rawHandleChange);
17
+ const { projectedData, handleProjectedChange: handleChange } = useProjection(
18
+ control,
19
+ rawHandleChange,
20
+ );
17
21
 
18
22
  const binding = computed(() => {
19
23
  const provider = control.value.uischema?.options?.provider;
@@ -41,11 +45,14 @@ const rootData = computed(() => injectedFormData.value || {});
41
45
  const { items, loading, error } = useProvider(binding, {
42
46
  data: rootData, // Pass the reactive reference
43
47
  path: control.value.path,
44
- dependsOnValues: depValues.value,
48
+ dependsOnValues: depValues,
45
49
  });
46
50
 
47
51
  // Provider will automatically reload when rootData changes due to reactive cache key
48
52
 
53
+ // deriveInitialValue — async API-based initial value seeding
54
+ useDeriveInitialValue({ control, handleChange });
55
+
49
56
  // Auto-select when provider returns only one item (enabled by default)
50
57
  watch(
51
58
  [items, loading],
@@ -62,7 +69,7 @@ watch(
62
69
  handleChange(control.value.path, valueToSelect);
63
70
  }
64
71
  },
65
- { immediate: true }
72
+ { immediate: true },
66
73
  );
67
74
 
68
75
  const value = computed({
@@ -81,16 +88,16 @@ const placeholder = computed(() => {
81
88
  </script>
82
89
 
83
90
  <template>
84
- <div class="flex flex-column gap-2">
85
- <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">{{
86
93
  control.schema.title
87
94
  }}</label>
88
- <div v-if="control.description" class="text-color-secondary text-left">
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 { computed, watch, unref, inject, type Ref, type ComputedRef } 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";
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({ control, handleChange, data: dataOverride }: DeriveOptions) {
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 ? unref(dataOverride) : 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
+
54
97
  if (derivedValue !== compareData) {
55
98
  handleChange(control.value.path, derivedValue);
56
99
  }