@atscript/ui 0.1.102 → 0.1.104
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/index.cjs +731 -10
- package/dist/index.d.cts +272 -6
- package/dist/index.d.mts +272 -6
- package/dist/index.mjs +722 -9
- package/dist/plugin.cjs +2 -2
- package/dist/plugin.mjs +2 -2
- package/package.json +3 -3
package/dist/index.cjs
CHANGED
|
@@ -30,7 +30,7 @@ const UI_FORM_SUFFIX_ICON = "ui.form.suffix.icon";
|
|
|
30
30
|
const UI_TABLE_WIDTH = "ui.table.width";
|
|
31
31
|
const UI_TABLE_COMPONENT = "ui.table.component";
|
|
32
32
|
const UI_TABLE_SELECT_WITH = "ui.table.selectWith";
|
|
33
|
-
const
|
|
33
|
+
const UI_TABLE_EXCLUDE = "ui.table.exclude";
|
|
34
34
|
const UI_TABLE_ATTR = "ui.table.attr";
|
|
35
35
|
const UI_TABLE_CLASSES = "ui.table.classes";
|
|
36
36
|
const UI_TABLE_STYLES = "ui.table.styles";
|
|
@@ -811,6 +811,35 @@ function setByPath(obj, path, value) {
|
|
|
811
811
|
}
|
|
812
812
|
current[last] = value;
|
|
813
813
|
}
|
|
814
|
+
/**
|
|
815
|
+
* Deletes the own key at a dot-separated path (form-data wrapper aware — derefs
|
|
816
|
+
* `obj.value` first). Walks to the parent WITHOUT vivifying intermediate nodes:
|
|
817
|
+
* if any ancestor is missing, the call is a no-op (nothing to delete).
|
|
818
|
+
*
|
|
819
|
+
* Unlike `setByPath(obj, path, undefined)`, this leaves NO own key behind — the
|
|
820
|
+
* leaf reads as absent (`'k' in parent === false`), which keeps `deepEqual`
|
|
821
|
+
* structural comparisons in sync (a present `undefined` own-key and an absent
|
|
822
|
+
* key are NOT structurally equal under the own-key walk). Used by
|
|
823
|
+
* {@link applyFormChanges} to apply a clear-to-`undefined` change as a delete.
|
|
824
|
+
*
|
|
825
|
+
* Empty path clears the root domain value (`obj.value = undefined`).
|
|
826
|
+
*/
|
|
827
|
+
function deleteByPath(obj, path) {
|
|
828
|
+
if (!path) {
|
|
829
|
+
obj.value = void 0;
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
const keys = path.split(".");
|
|
833
|
+
const last = keys.pop();
|
|
834
|
+
if (last === void 0) return;
|
|
835
|
+
let current = obj.value;
|
|
836
|
+
for (const key of keys) {
|
|
837
|
+
if (current === null || current === void 0 || typeof current !== "object") return;
|
|
838
|
+
current = current[key];
|
|
839
|
+
}
|
|
840
|
+
if (current === null || current === void 0 || typeof current !== "object") return;
|
|
841
|
+
delete current[last];
|
|
842
|
+
}
|
|
814
843
|
function parseStaticDefault(raw, prop) {
|
|
815
844
|
if (typeof raw !== "string") return raw;
|
|
816
845
|
if (prop.type.kind === "" && prop.type.designType === "string") return raw;
|
|
@@ -884,8 +913,11 @@ function detectUnionVariant(value, variants) {
|
|
|
884
913
|
const disc = getVariantsDiscriminator(variants);
|
|
885
914
|
if (disc && value !== null && typeof value === "object") {
|
|
886
915
|
const tag = value[disc.propertyName];
|
|
887
|
-
const
|
|
888
|
-
if (
|
|
916
|
+
const key = String(tag);
|
|
917
|
+
if (Object.prototype.hasOwnProperty.call(disc.indexMapping, key)) {
|
|
918
|
+
const idx = disc.indexMapping[key];
|
|
919
|
+
if (idx !== void 0) return idx;
|
|
920
|
+
}
|
|
889
921
|
}
|
|
890
922
|
for (let i = 0; i < variants.length; i++) try {
|
|
891
923
|
if (getVariantValidator(variants[i]).validate(value, true)) return i;
|
|
@@ -893,6 +925,35 @@ function detectUnionVariant(value, variants) {
|
|
|
893
925
|
return 0;
|
|
894
926
|
}
|
|
895
927
|
//#endregion
|
|
928
|
+
//#region src/form/clone.ts
|
|
929
|
+
/**
|
|
930
|
+
* Structural deep clone of plain JSON-ish data (objects / arrays / primitives /
|
|
931
|
+
* `Date`). Walks OWN-ENUMERABLE keys only (matches the own-key discipline in
|
|
932
|
+
* `diff.ts` — never copies an accidental prototype) and copies leaves by value.
|
|
933
|
+
*
|
|
934
|
+
* `structuredClone` is deliberately NOT used: it throws on functions and on Vue
|
|
935
|
+
* reactive proxies. The optional `unwrap` hook lets a framework caller
|
|
936
|
+
* de-proxy each value first (vue-form passes `toRaw`); the core omits it.
|
|
937
|
+
*
|
|
938
|
+
* The SINGLE deep-clone primitive for the form engine — used by
|
|
939
|
+
* `applyFormChanges`, `buildFormRebase`, and vue-form's baseline snapshot. Do
|
|
940
|
+
* not reimplement structural cloning elsewhere.
|
|
941
|
+
*/
|
|
942
|
+
function deepClone(value, unwrap) {
|
|
943
|
+
const v = unwrap ? unwrap(value) : value;
|
|
944
|
+
if (v === null || typeof v !== "object") return v;
|
|
945
|
+
if (v instanceof Date) return new Date(v.getTime());
|
|
946
|
+
if (Array.isArray(v)) {
|
|
947
|
+
const out = [];
|
|
948
|
+
for (let i = 0; i < v.length; i++) out.push(deepClone(v[i], unwrap));
|
|
949
|
+
return out;
|
|
950
|
+
}
|
|
951
|
+
const src = v;
|
|
952
|
+
const out = {};
|
|
953
|
+
for (const k of Object.keys(src)) out[k] = deepClone(src[k], unwrap);
|
|
954
|
+
return out;
|
|
955
|
+
}
|
|
956
|
+
//#endregion
|
|
896
957
|
//#region src/form/validate.ts
|
|
897
958
|
let defaultValidatorPlugins = [];
|
|
898
959
|
/** Replace the default validator plugins applied to every form/field validator. */
|
|
@@ -948,6 +1009,661 @@ function createFieldValidator(prop, opts) {
|
|
|
948
1009
|
};
|
|
949
1010
|
}
|
|
950
1011
|
//#endregion
|
|
1012
|
+
//#region src/form/diff.ts
|
|
1013
|
+
/**
|
|
1014
|
+
* Diffs a form's `current` data against its `baseline` snapshot, producing both
|
|
1015
|
+
* a changed-fields list and an `@atscript/db` patch object.
|
|
1016
|
+
*
|
|
1017
|
+
* Both `baseline` and `current` are the WRAPPED form-data container
|
|
1018
|
+
* (`{ value: domainData }`) so this reuses {@link getByPath}.
|
|
1019
|
+
*
|
|
1020
|
+
* Revert-aware: a value edited back to its baseline produces no change and no
|
|
1021
|
+
* patch entry.
|
|
1022
|
+
*
|
|
1023
|
+
* Snapshot contract: the result is NOT a deep copy. `$insert` items, `$replace`
|
|
1024
|
+
* arrays, scalar leaf values, and `changes[].before/after` all hold live
|
|
1025
|
+
* references into `baseline` / `current`. Callers that keep editing the form
|
|
1026
|
+
* after building the patch must snapshot first (e.g. build the patch at submit
|
|
1027
|
+
* time on a frozen clone). This is the common Vue v-model flow.
|
|
1028
|
+
*/
|
|
1029
|
+
function buildFormDiff(def, baseline, current, opts) {
|
|
1030
|
+
const changes = [];
|
|
1031
|
+
const patch = {};
|
|
1032
|
+
const versionColumn = findVersionColumn(def);
|
|
1033
|
+
diffFields(def.fields, "", baseline, current, changes, patch, versionColumn, def.flatMap);
|
|
1034
|
+
if ((opts?.cas ?? true) && versionColumn && Object.keys(patch).length > 0) {
|
|
1035
|
+
const baselineVersion = getByPath(baseline, versionColumn);
|
|
1036
|
+
if (typeof baselineVersion === "number" && Number.isInteger(baselineVersion)) patch.$cas = { [versionColumn]: baselineVersion };
|
|
1037
|
+
}
|
|
1038
|
+
return {
|
|
1039
|
+
isDirty: changes.length > 0,
|
|
1040
|
+
changes,
|
|
1041
|
+
patch
|
|
1042
|
+
};
|
|
1043
|
+
}
|
|
1044
|
+
/**
|
|
1045
|
+
* Diffs a list of sibling fields. `prefix` is the dot-path of the parent
|
|
1046
|
+
* context relative to the form root (used only for the change list `path`);
|
|
1047
|
+
* patch entries are written into the local `patch` object so callers can place
|
|
1048
|
+
* the whole sub-object as a nested partial.
|
|
1049
|
+
*
|
|
1050
|
+
* `versionColumn` is the top-level `@db.column.version` field name (or
|
|
1051
|
+
* undefined). When set, the matching top-level field is skipped entirely — it
|
|
1052
|
+
* is server-managed and may only be round-tripped via `$cas`.
|
|
1053
|
+
*
|
|
1054
|
+
* `inlineFlatMap` is the form's `flatMap`, supplied only at the top-level walk.
|
|
1055
|
+
* It lets {@link diffScalarField} read `@db.patch.strategy` off the object
|
|
1056
|
+
* ancestors of an INLINED leaf (a dotted-path leaf with no `FormObjectFieldDef`
|
|
1057
|
+
* node — `createFormDef` dissolves unlabelled objects into dot-paths). At a
|
|
1058
|
+
* default (replace) ancestor the whole sub-object must be emitted. It is
|
|
1059
|
+
* intentionally NOT propagated into `diffObjectField` recursion, where the
|
|
1060
|
+
* strategy decision is already made per structured object.
|
|
1061
|
+
*/
|
|
1062
|
+
function diffFields(fields, prefix, baseline, current, changes, patch, versionColumn, inlineFlatMap) {
|
|
1063
|
+
for (const field of fields) {
|
|
1064
|
+
if (field.path === "" && fields.length === 1) {
|
|
1065
|
+
diffLeafRoot(field, baseline, current, changes, patch);
|
|
1066
|
+
continue;
|
|
1067
|
+
}
|
|
1068
|
+
if (field.phantom) continue;
|
|
1069
|
+
if (versionColumn !== void 0 && !prefix && field.path === versionColumn) continue;
|
|
1070
|
+
const fullPath = prefix ? `${prefix}.${field.path}` : field.path;
|
|
1071
|
+
if (isArrayField(field)) {
|
|
1072
|
+
diffArrayField(field, fullPath, baseline, current, changes, patch);
|
|
1073
|
+
continue;
|
|
1074
|
+
}
|
|
1075
|
+
if (isObjectField(field)) {
|
|
1076
|
+
diffObjectField(field, fullPath, baseline, current, changes, patch);
|
|
1077
|
+
continue;
|
|
1078
|
+
}
|
|
1079
|
+
diffScalarField(field, fullPath, baseline, current, changes, patch, inlineFlatMap);
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
/** Single-leaf root form (non-object root). Whole value is the patch. */
|
|
1083
|
+
function diffLeafRoot(field, baseline, current, changes, patch) {
|
|
1084
|
+
const before = getByPath(baseline, "");
|
|
1085
|
+
const after = getByPath(current, "");
|
|
1086
|
+
if (deepEqual(before, after)) return;
|
|
1087
|
+
if (isArrayField(field)) {
|
|
1088
|
+
const arrayPatch = diffArray(field, before, after);
|
|
1089
|
+
if (arrayPatch === void 0) return;
|
|
1090
|
+
changes.push({
|
|
1091
|
+
path: "",
|
|
1092
|
+
kind: "array",
|
|
1093
|
+
before,
|
|
1094
|
+
after
|
|
1095
|
+
});
|
|
1096
|
+
patch.value = arrayPatch;
|
|
1097
|
+
} else {
|
|
1098
|
+
changes.push({
|
|
1099
|
+
path: "",
|
|
1100
|
+
kind: "set",
|
|
1101
|
+
before,
|
|
1102
|
+
after
|
|
1103
|
+
});
|
|
1104
|
+
patch.value = after === void 0 ? null : after;
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
/** Scalar / union / tuple / ref field — whole-value compare; clear → null. */
|
|
1108
|
+
function diffScalarField(field, fullPath, baseline, current, changes, patch, inlineFlatMap) {
|
|
1109
|
+
const before = getByPath(baseline, fullPath);
|
|
1110
|
+
const after = getByPath(current, fullPath);
|
|
1111
|
+
if (deepEqual(before, after)) return;
|
|
1112
|
+
changes.push({
|
|
1113
|
+
path: fullPath,
|
|
1114
|
+
kind: "set",
|
|
1115
|
+
before,
|
|
1116
|
+
after
|
|
1117
|
+
});
|
|
1118
|
+
if (inlineFlatMap && field.path.includes(".")) {
|
|
1119
|
+
const cutoff = inlinedReplaceCutoff(field.path, inlineFlatMap);
|
|
1120
|
+
if (cutoff !== void 0) {
|
|
1121
|
+
const prefixLen = fullPath.length - field.path.length;
|
|
1122
|
+
const sub = getByPath(current, prefixLen > 0 ? fullPath.slice(0, prefixLen) + cutoff : cutoff);
|
|
1123
|
+
setPatchLeaf(patch, cutoff, sub === void 0 ? null : sub);
|
|
1124
|
+
return;
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
setPatchLeaf(patch, field.path, after === void 0 ? null : after);
|
|
1128
|
+
}
|
|
1129
|
+
/**
|
|
1130
|
+
* For a dotted INLINED leaf path, returns the path of the shallowest object
|
|
1131
|
+
* ancestor whose `@db.patch.strategy` is the default (`replace`), or undefined
|
|
1132
|
+
* when every ancestor is `merge` (then the leaf partial is correct). At a
|
|
1133
|
+
* replace ancestor the whole sub-object must be present in the patch.
|
|
1134
|
+
*
|
|
1135
|
+
* Walks ancestor segments (`a`, `a.b`, … but not the leaf itself); the first
|
|
1136
|
+
* one that is an object AND not merge is the cutoff. merge does NOT propagate,
|
|
1137
|
+
* so a default-replace level below a merge level still cuts off there.
|
|
1138
|
+
*/
|
|
1139
|
+
function inlinedReplaceCutoff(leafPath, flatMap) {
|
|
1140
|
+
const segs = leafPath.split(".");
|
|
1141
|
+
let acc = "";
|
|
1142
|
+
for (let i = 0; i < segs.length - 1; i++) {
|
|
1143
|
+
acc = acc ? `${acc}.${segs[i]}` : segs[i];
|
|
1144
|
+
const prop = flatMap.get(acc);
|
|
1145
|
+
if (!prop || prop.type.kind !== "object") continue;
|
|
1146
|
+
if (getFieldMeta(prop, "db.patch.strategy") !== "merge") return acc;
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
/**
|
|
1150
|
+
* Inlined-or-structured object field — recurse, emit a nested partial OR the
|
|
1151
|
+
* whole sub-object depending on the field's `@db.patch.strategy`.
|
|
1152
|
+
*
|
|
1153
|
+
* atscript-db's DEFAULT nested-object patch strategy is `replace` (strict —
|
|
1154
|
+
* every required child must be present, else 400; omitted optionals are
|
|
1155
|
+
* null-filled). A changed-leaves-only partial is a valid patch ONLY when the
|
|
1156
|
+
* object field carries `@db.patch.strategy 'merge'`. For the default (replace)
|
|
1157
|
+
* case we therefore emit the WHOLE current sub-object so the validator passes
|
|
1158
|
+
* and no optional leaf is silently nulled. `merge` does NOT propagate, so a
|
|
1159
|
+
* descendant object without its own `merge` again emits its full sub-object
|
|
1160
|
+
* (handled by recursion — `diffFields` re-enters this function per child).
|
|
1161
|
+
*
|
|
1162
|
+
* Wholesale-clear: if the sub-object was a defined object in `baseline` but is
|
|
1163
|
+
* now undefined/null, emit `field: null` (object removed) instead of a partial
|
|
1164
|
+
* of nulled leaves.
|
|
1165
|
+
*/
|
|
1166
|
+
function diffObjectField(field, fullPath, baseline, current, changes, patch) {
|
|
1167
|
+
const beforeObj = getByPath(baseline, fullPath);
|
|
1168
|
+
const afterObj = getByPath(current, fullPath);
|
|
1169
|
+
const nested = {};
|
|
1170
|
+
diffFields(field.objectDef.fields, fullPath, baseline, current, changes, nested, void 0, field.objectDef.flatMap);
|
|
1171
|
+
if ((afterObj === void 0 || afterObj === null) && isPlainObject(beforeObj)) {
|
|
1172
|
+
setPatchLeaf(patch, field.path, null);
|
|
1173
|
+
return;
|
|
1174
|
+
}
|
|
1175
|
+
if (Object.keys(nested).length === 0) return;
|
|
1176
|
+
if (getFieldMeta(field.prop, "db.patch.strategy") === "merge") setPatchLeaf(patch, field.path, nested);
|
|
1177
|
+
else setPatchLeaf(patch, field.path, afterObj === void 0 ? null : afterObj);
|
|
1178
|
+
}
|
|
1179
|
+
/** Array field — keyed → $update/$insert/$remove; unkeyed → $replace. */
|
|
1180
|
+
function diffArrayField(field, fullPath, baseline, current, changes, patch) {
|
|
1181
|
+
const before = getByPath(baseline, fullPath);
|
|
1182
|
+
const after = getByPath(current, fullPath);
|
|
1183
|
+
if (deepEqual(before, after)) return;
|
|
1184
|
+
const arrayPatch = diffArray(field, before, after);
|
|
1185
|
+
if (arrayPatch === void 0) return;
|
|
1186
|
+
changes.push({
|
|
1187
|
+
path: fullPath,
|
|
1188
|
+
kind: "array",
|
|
1189
|
+
before,
|
|
1190
|
+
after
|
|
1191
|
+
});
|
|
1192
|
+
setPatchLeaf(patch, field.path, arrayPatch);
|
|
1193
|
+
}
|
|
1194
|
+
/**
|
|
1195
|
+
* Produces a `TArrayPatch` value for one array field, or `undefined` when no
|
|
1196
|
+
* real op results (the caller then skips the field entirely).
|
|
1197
|
+
*
|
|
1198
|
+
* - Keyed arrays (item object has `@expect.array.key`): emit `$update`
|
|
1199
|
+
* (key + changed leaves), `$insert` (wholly-new items, whole), `$remove`
|
|
1200
|
+
* (key only). Reorder-only (same key membership, same content, different
|
|
1201
|
+
* order) → `$replace` (key-ops can't express a pure reorder).
|
|
1202
|
+
* Ambiguous keys (duplicate or missing key values) → `$replace` (the only
|
|
1203
|
+
* faithful op when key identity is unreliable).
|
|
1204
|
+
* - Unkeyed object arrays / primitive arrays: `$replace` with the whole array.
|
|
1205
|
+
* Primitive arrays with `@expect.array.uniqueItems` use by-value
|
|
1206
|
+
* `$insert` / `$remove` (set semantics).
|
|
1207
|
+
*
|
|
1208
|
+
* Deliberate `$insert`-not-`$upsert`: wholly-new keyed items use `$insert`
|
|
1209
|
+
* (pure append) rather than `$upsert`. This is safe because `$insert` is only
|
|
1210
|
+
* ever used for keys ABSENT from baseline; existing keys go through `$update`.
|
|
1211
|
+
* `$upsert` would dedupe-by-key, which we don't need given that invariant.
|
|
1212
|
+
*/
|
|
1213
|
+
function diffArray(field, before, after) {
|
|
1214
|
+
const beforeArr = Array.isArray(before) ? before : [];
|
|
1215
|
+
const afterArr = Array.isArray(after) ? after : [];
|
|
1216
|
+
const keyProps = getArrayKeyProps(field.itemType);
|
|
1217
|
+
if (keyProps.length > 0) return diffKeyedArray(beforeArr, afterArr, keyProps);
|
|
1218
|
+
if (getFieldMeta(field.prop, "expect.array.uniqueItems") !== void 0 && isPrimitiveItem(field)) {
|
|
1219
|
+
const setPatch = diffUniqueArray(beforeArr, afterArr);
|
|
1220
|
+
if (setPatch) return setPatch;
|
|
1221
|
+
}
|
|
1222
|
+
return { $replace: afterArr };
|
|
1223
|
+
}
|
|
1224
|
+
/**
|
|
1225
|
+
* Keyed array diff. Reorder-only (same membership) falls back to $replace.
|
|
1226
|
+
*
|
|
1227
|
+
* Returns `undefined` when no $update/$insert/$remove op is produced (so the
|
|
1228
|
+
* caller skips the field rather than emitting a malformed empty `{}`).
|
|
1229
|
+
*
|
|
1230
|
+
* Ambiguity fallback: if either side has DUPLICATE key buckets, or ANY element
|
|
1231
|
+
* is missing all of its key values, key identity is unreliable — fall back to
|
|
1232
|
+
* `{ $replace: after }`, the only faithful op (last-write-wins collapse would
|
|
1233
|
+
* silently drop items, and a key-less `$update` is unmatchable by the DB).
|
|
1234
|
+
*/
|
|
1235
|
+
function diffKeyedArray(before, after, keyProps) {
|
|
1236
|
+
if (hasKeylessItem(before, keyProps) || hasKeylessItem(after, keyProps)) return { $replace: after };
|
|
1237
|
+
const beforeByKey = /* @__PURE__ */ new Map();
|
|
1238
|
+
for (const el of before) if (isPlainObject(el)) beforeByKey.set(keyOf(el, keyProps), el);
|
|
1239
|
+
const afterByKey = /* @__PURE__ */ new Map();
|
|
1240
|
+
for (const el of after) if (isPlainObject(el)) afterByKey.set(keyOf(el, keyProps), el);
|
|
1241
|
+
if (beforeByKey.size !== before.length || afterByKey.size !== after.length) return { $replace: after };
|
|
1242
|
+
if (beforeByKey.size === afterByKey.size) {
|
|
1243
|
+
let sameMembershipAndContent = true;
|
|
1244
|
+
for (const [k, el] of afterByKey) {
|
|
1245
|
+
const prev = beforeByKey.get(k);
|
|
1246
|
+
if (prev === void 0 || !deepEqual(prev, el)) {
|
|
1247
|
+
sameMembershipAndContent = false;
|
|
1248
|
+
break;
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
if (sameMembershipAndContent) return { $replace: after };
|
|
1252
|
+
}
|
|
1253
|
+
const $insert = [];
|
|
1254
|
+
const $update = [];
|
|
1255
|
+
const $remove = [];
|
|
1256
|
+
for (const [k, el] of afterByKey) {
|
|
1257
|
+
const prev = beforeByKey.get(k);
|
|
1258
|
+
if (prev === void 0) {
|
|
1259
|
+
$insert.push(el);
|
|
1260
|
+
continue;
|
|
1261
|
+
}
|
|
1262
|
+
if (!deepEqual(prev, el)) {
|
|
1263
|
+
const partial = buildKeyedUpdate(prev, el, keyProps);
|
|
1264
|
+
if (partial) $update.push(partial);
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
for (const [k, el] of beforeByKey) if (!afterByKey.has(k)) $remove.push(pickKeys(el, keyProps));
|
|
1268
|
+
return arrayOps({
|
|
1269
|
+
$update,
|
|
1270
|
+
$insert,
|
|
1271
|
+
$remove
|
|
1272
|
+
});
|
|
1273
|
+
}
|
|
1274
|
+
/** Builds a `$update` partial: key fields + changed leaves only. */
|
|
1275
|
+
function buildKeyedUpdate(prev, next, keyProps) {
|
|
1276
|
+
const partial = {};
|
|
1277
|
+
for (const k of keyProps) partial[k] = next[k];
|
|
1278
|
+
let changed = false;
|
|
1279
|
+
const allKeys = new Set([...Object.keys(prev), ...Object.keys(next)]);
|
|
1280
|
+
for (const k of allKeys) {
|
|
1281
|
+
if (keyProps.includes(k)) continue;
|
|
1282
|
+
const a = prev[k];
|
|
1283
|
+
const b = next[k];
|
|
1284
|
+
if (!deepEqual(a, b)) {
|
|
1285
|
+
partial[k] = b === void 0 ? null : b;
|
|
1286
|
+
changed = true;
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
return changed ? partial : void 0;
|
|
1290
|
+
}
|
|
1291
|
+
/** Primitive uniqueItems set diff. Returns undefined if neither side differs. */
|
|
1292
|
+
function diffUniqueArray(before, after) {
|
|
1293
|
+
const beforeSet = new Set(before.map((v) => stableKey(v)));
|
|
1294
|
+
const afterSet = new Set(after.map((v) => stableKey(v)));
|
|
1295
|
+
const $insert = [];
|
|
1296
|
+
const $remove = [];
|
|
1297
|
+
const seenInsert = /* @__PURE__ */ new Set();
|
|
1298
|
+
const seenRemove = /* @__PURE__ */ new Set();
|
|
1299
|
+
for (const v of after) {
|
|
1300
|
+
const k = stableKey(v);
|
|
1301
|
+
if (!beforeSet.has(k) && !seenInsert.has(k)) {
|
|
1302
|
+
$insert.push(v);
|
|
1303
|
+
seenInsert.add(k);
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
for (const v of before) {
|
|
1307
|
+
const k = stableKey(v);
|
|
1308
|
+
if (!afterSet.has(k) && !seenRemove.has(k)) {
|
|
1309
|
+
$remove.push(v);
|
|
1310
|
+
seenRemove.add(k);
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
return arrayOps({
|
|
1314
|
+
$insert,
|
|
1315
|
+
$remove
|
|
1316
|
+
});
|
|
1317
|
+
}
|
|
1318
|
+
/**
|
|
1319
|
+
* Assembles a `TArrayPatch` from named op-arrays, dropping empty ones. Returns
|
|
1320
|
+
* `undefined` when no op carries any item, so the caller skips the field rather
|
|
1321
|
+
* than emitting a malformed empty `{}`.
|
|
1322
|
+
*/
|
|
1323
|
+
function arrayOps(ops) {
|
|
1324
|
+
const result = {};
|
|
1325
|
+
for (const k in ops) if (ops[k].length > 0) result[k] = ops[k];
|
|
1326
|
+
return Object.keys(result).length > 0 ? result : void 0;
|
|
1327
|
+
}
|
|
1328
|
+
/** Reads `@expect.array.key` props from an array item's object type. */
|
|
1329
|
+
function getArrayKeyProps(itemType) {
|
|
1330
|
+
if (itemType.type.kind !== "object") return [];
|
|
1331
|
+
const props = itemType.type.props;
|
|
1332
|
+
const keys = [];
|
|
1333
|
+
for (const [name, prop] of props.entries()) if (getFieldMeta(prop, "expect.array.key") !== void 0) keys.push(name);
|
|
1334
|
+
return keys;
|
|
1335
|
+
}
|
|
1336
|
+
/** True when the array's item type is a primitive (designType, kind === ''). */
|
|
1337
|
+
function isPrimitiveItem(field) {
|
|
1338
|
+
return field.itemType.type.kind === "";
|
|
1339
|
+
}
|
|
1340
|
+
/** Finds the form's `@db.column.version` column name, if any (top-level only). */
|
|
1341
|
+
function findVersionColumn(def) {
|
|
1342
|
+
for (const [path, prop] of def.flatMap.entries()) {
|
|
1343
|
+
if (!path || path.includes(".")) continue;
|
|
1344
|
+
if (getFieldMeta(prop, "db.column.version") !== void 0) return path;
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
/** True when any element lacks ALL of its key values (un-keyable identity). */
|
|
1348
|
+
function hasKeylessItem(arr, keyProps) {
|
|
1349
|
+
for (const el of arr) {
|
|
1350
|
+
if (!isPlainObject(el)) return true;
|
|
1351
|
+
let hasAnyKey = false;
|
|
1352
|
+
for (const k of keyProps) {
|
|
1353
|
+
const v = el[k];
|
|
1354
|
+
if (v !== void 0 && v !== null) {
|
|
1355
|
+
hasAnyKey = true;
|
|
1356
|
+
break;
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
if (!hasAnyKey) return true;
|
|
1360
|
+
}
|
|
1361
|
+
return false;
|
|
1362
|
+
}
|
|
1363
|
+
/** Composite key string for a keyed-array element. */
|
|
1364
|
+
function keyOf(el, keyProps) {
|
|
1365
|
+
if (keyProps.length === 1) return stableKey(el[keyProps[0]]);
|
|
1366
|
+
return keyProps.map((k) => stableKey(el[k])).join(" ");
|
|
1367
|
+
}
|
|
1368
|
+
/** Picks only the key fields from an element (for $remove). */
|
|
1369
|
+
function pickKeys(el, keyProps) {
|
|
1370
|
+
const out = {};
|
|
1371
|
+
for (const k of keyProps) out[k] = el[k];
|
|
1372
|
+
return out;
|
|
1373
|
+
}
|
|
1374
|
+
/** Writes a value into a (possibly dotted) leaf path on a local patch object. */
|
|
1375
|
+
function setPatchLeaf(patch, path, value) {
|
|
1376
|
+
if (!path.includes(".")) {
|
|
1377
|
+
patch[path] = value;
|
|
1378
|
+
return;
|
|
1379
|
+
}
|
|
1380
|
+
const keys = path.split(".");
|
|
1381
|
+
const last = keys.pop();
|
|
1382
|
+
let cur = patch;
|
|
1383
|
+
for (const k of keys) {
|
|
1384
|
+
let next = cur[k];
|
|
1385
|
+
if (next === void 0 || next === null || typeof next !== "object") {
|
|
1386
|
+
next = {};
|
|
1387
|
+
cur[k] = next;
|
|
1388
|
+
}
|
|
1389
|
+
cur = next;
|
|
1390
|
+
}
|
|
1391
|
+
cur[last] = value;
|
|
1392
|
+
}
|
|
1393
|
+
function isPlainObject(v) {
|
|
1394
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1395
|
+
}
|
|
1396
|
+
/**
|
|
1397
|
+
* Stable string key for primitives / values (used for set membership).
|
|
1398
|
+
*
|
|
1399
|
+
* Key equality is TYPE-STRICT: a number `1` and a string `'1'` produce
|
|
1400
|
+
* distinct keys, so a keyed-array item whose key changes JS representation
|
|
1401
|
+
* between baseline and current is treated as a remove + insert. Callers that
|
|
1402
|
+
* round-trip keys with loose typing should normalise the key type first.
|
|
1403
|
+
*/
|
|
1404
|
+
function stableKey(v) {
|
|
1405
|
+
if (typeof v === "string") return `s:${v}`;
|
|
1406
|
+
if (typeof v === "number" || typeof v === "boolean") return `p:${String(v)}`;
|
|
1407
|
+
if (v === null) return "null";
|
|
1408
|
+
if (v === void 0) return "undef";
|
|
1409
|
+
return `j:${JSON.stringify(v)}`;
|
|
1410
|
+
}
|
|
1411
|
+
/**
|
|
1412
|
+
* Structural deep equality (order-sensitive for arrays). `NaN` equals `NaN`
|
|
1413
|
+
* (revert-aware for NaN scalars) while `0` / `-0` stay equal (matches DB
|
|
1414
|
+
* intent — `===` treats them equal, only NaN is special-cased).
|
|
1415
|
+
*
|
|
1416
|
+
* The single comparator shared across the form engine: diff, conflict
|
|
1417
|
+
* detection ({@link buildFormRebase}), and apply all route through this — never
|
|
1418
|
+
* reimplement equality elsewhere.
|
|
1419
|
+
*/
|
|
1420
|
+
function deepEqual(a, b) {
|
|
1421
|
+
if (a === b) return true;
|
|
1422
|
+
if (typeof a === "number" && typeof b === "number") return Number.isNaN(a) && Number.isNaN(b);
|
|
1423
|
+
if (a === null || b === null || a === void 0 || b === void 0) return false;
|
|
1424
|
+
if (typeof a !== "object" || typeof b !== "object") return false;
|
|
1425
|
+
const aIsArr = Array.isArray(a);
|
|
1426
|
+
const bIsArr = Array.isArray(b);
|
|
1427
|
+
if (aIsArr !== bIsArr) return false;
|
|
1428
|
+
if (aIsArr && bIsArr) {
|
|
1429
|
+
if (a.length !== b.length) return false;
|
|
1430
|
+
for (let i = 0; i < a.length; i++) if (!deepEqual(a[i], b[i])) return false;
|
|
1431
|
+
return true;
|
|
1432
|
+
}
|
|
1433
|
+
const ao = a;
|
|
1434
|
+
const bo = b;
|
|
1435
|
+
const aKeys = Object.keys(ao);
|
|
1436
|
+
const bKeys = Object.keys(bo);
|
|
1437
|
+
if (aKeys.length !== bKeys.length) return false;
|
|
1438
|
+
for (const k of aKeys) {
|
|
1439
|
+
if (!Object.prototype.hasOwnProperty.call(bo, k)) return false;
|
|
1440
|
+
if (!deepEqual(ao[k], bo[k])) return false;
|
|
1441
|
+
}
|
|
1442
|
+
return true;
|
|
1443
|
+
}
|
|
1444
|
+
//#endregion
|
|
1445
|
+
//#region src/form/dirty.ts
|
|
1446
|
+
/**
|
|
1447
|
+
* True when the field at dot-path `path` is dirty given a {@link FormFieldChange}
|
|
1448
|
+
* list (as produced by {@link buildFormDiff}).
|
|
1449
|
+
*
|
|
1450
|
+
* The change list is leaf-grained for scalars/objects but WHOLE-ARRAY for arrays,
|
|
1451
|
+
* so a field at `path` is dirty iff some change path equals `path` OR starts with
|
|
1452
|
+
* `path + "."`:
|
|
1453
|
+
*
|
|
1454
|
+
* - scalar / leaf field (incl. nested `address.city`) → exact match.
|
|
1455
|
+
* - object / section container → no entry at its own path, only its leaves →
|
|
1456
|
+
* matched by the PREFIX branch.
|
|
1457
|
+
* - whole-array field → one entry at the array root → exact match.
|
|
1458
|
+
* - a field rendered for an array-ITEM leaf (e.g. `items.0.qty`) → NOT detectable:
|
|
1459
|
+
* the array diff emits a single whole-array change at the array root, never
|
|
1460
|
+
* per-item leaf paths, so this correctly returns false (the array container
|
|
1461
|
+
* lights up instead). This is a known, documented limitation.
|
|
1462
|
+
*
|
|
1463
|
+
* The prefix uses `path + "."` so field `item` never matches a change at `items`
|
|
1464
|
+
* (no false positives).
|
|
1465
|
+
*
|
|
1466
|
+
* Empty `path` `''` is the wrapped form root — every change is nested under it,
|
|
1467
|
+
* so it is considered dirty iff there are ANY changes.
|
|
1468
|
+
*/
|
|
1469
|
+
function isPathDirty(changes, path) {
|
|
1470
|
+
if (path === "") return changes.length > 0;
|
|
1471
|
+
const prefix = `${path}.`;
|
|
1472
|
+
for (const change of changes) if (change.path === path || change.path.startsWith(prefix)) return true;
|
|
1473
|
+
return false;
|
|
1474
|
+
}
|
|
1475
|
+
/**
|
|
1476
|
+
* Precomputes the set of ALL dirty paths from a {@link FormFieldChange} list so
|
|
1477
|
+
* that membership is an O(1) `Set.has(path)` instead of {@link isPathDirty}'s
|
|
1478
|
+
* per-call O(changes) prefix scan. Callers that probe many fields against the
|
|
1479
|
+
* same change list (e.g. a form rendering one field per leaf) build this once
|
|
1480
|
+
* and query it per field.
|
|
1481
|
+
*
|
|
1482
|
+
* For each change path `C` it adds `C` AND every dot-prefix ancestor of `C`
|
|
1483
|
+
* (so `'address.city'` adds both `'address.city'` and `'address'`), matching
|
|
1484
|
+
* `isPathDirty`'s "exact OR `path + '.'` prefix" predicate — an ancestor
|
|
1485
|
+
* container is dirty exactly when some change is nested under it. The wrapped
|
|
1486
|
+
* root `''` is added iff there are ANY changes, mirroring `isPathDirty('')`.
|
|
1487
|
+
*
|
|
1488
|
+
* INVARIANT (locked, tested): for EVERY path `P`,
|
|
1489
|
+
* `collectDirtyPaths(changes).has(P) === isPathDirty(changes, P)`. This is a
|
|
1490
|
+
* precompute of the SAME predicate, not a second one — keep them in lockstep.
|
|
1491
|
+
*/
|
|
1492
|
+
function collectDirtyPaths(changes) {
|
|
1493
|
+
const dirty = /* @__PURE__ */ new Set();
|
|
1494
|
+
if (changes.length === 0) return dirty;
|
|
1495
|
+
dirty.add("");
|
|
1496
|
+
for (const change of changes) {
|
|
1497
|
+
const path = change.path;
|
|
1498
|
+
dirty.add(path);
|
|
1499
|
+
let dot = path.indexOf(".");
|
|
1500
|
+
while (dot !== -1) {
|
|
1501
|
+
dirty.add(path.slice(0, dot));
|
|
1502
|
+
dot = path.indexOf(".", dot + 1);
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
return dirty;
|
|
1506
|
+
}
|
|
1507
|
+
//#endregion
|
|
1508
|
+
//#region src/form/apply.ts
|
|
1509
|
+
/**
|
|
1510
|
+
* Applies a {@link FormFieldChange} list onto a WRAPPED form-data container
|
|
1511
|
+
* (`{ value: domainData }`), mutating it in place and returning the same
|
|
1512
|
+
* reference. The inverse direction of {@link buildFormDiff}: where the diff
|
|
1513
|
+
* READS `(baseline, current)` into changes, this WRITES changes onto data.
|
|
1514
|
+
*
|
|
1515
|
+
* IMPORTANT: pass a CLONE, never the live fetched row — every write mutates
|
|
1516
|
+
* `data` directly. Callers that need the original intact should
|
|
1517
|
+
* `deepClone(data)` first (see {@link deepClone}).
|
|
1518
|
+
*
|
|
1519
|
+
* Per-change semantics (the single place the apply rules live, so
|
|
1520
|
+
* {@link buildFormRebase} stays consistent):
|
|
1521
|
+
*
|
|
1522
|
+
* - `kind: 'set'`:
|
|
1523
|
+
* - `change.after === undefined` → DELETE the own key at `change.path` (walk
|
|
1524
|
+
* to parent, `delete`). A cleared field must read as ABSENT, not as a
|
|
1525
|
+
* present `undefined` own-key — otherwise a re-diff sees a structural
|
|
1526
|
+
* mismatch where the form intends "no value". `setByPath(…, undefined)`
|
|
1527
|
+
* leaves an own key behind, so we use {@link deleteByPath} instead.
|
|
1528
|
+
* - otherwise → `setByPath(data, change.path, change.after)`.
|
|
1529
|
+
* - `kind: 'array'`: whole-array set via `setByPath(data, change.path,
|
|
1530
|
+
* change.after)` (LOCKED Option A — no per-element merge; the diff already
|
|
1531
|
+
* carried the full after-array).
|
|
1532
|
+
*
|
|
1533
|
+
* The `def` is currently unused by the apply walk (paths fully describe the
|
|
1534
|
+
* write target) but is part of the signature for parity with
|
|
1535
|
+
* `buildFormDiff`/`buildFormRebase`, so the rebase engine threads one `def`
|
|
1536
|
+
* uniformly through diff + apply.
|
|
1537
|
+
*/
|
|
1538
|
+
function applyFormChanges(_def, data, changes) {
|
|
1539
|
+
for (const change of changes) if (change.kind === "set" && change.after === void 0) deleteByPath(data, change.path);
|
|
1540
|
+
else setByPath(data, change.path, change.after);
|
|
1541
|
+
return data;
|
|
1542
|
+
}
|
|
1543
|
+
//#endregion
|
|
1544
|
+
//#region src/form/rebase.ts
|
|
1545
|
+
/**
|
|
1546
|
+
* Pure 3-way rebase for a change-tracked form. Given the current baseline `B0`,
|
|
1547
|
+
* the live form `C`, and a fresh upstream `U`, produces the form rewritten as
|
|
1548
|
+
* `U` + the local diff (`C` vs `B0`) reapplied on top:
|
|
1549
|
+
*
|
|
1550
|
+
* - Fields the user never touched adopt upstream's value.
|
|
1551
|
+
* - Local edits survive (reapplied onto the upstream clone).
|
|
1552
|
+
* - Fields changed on BOTH sides to different values are conflicts, resolved by
|
|
1553
|
+
* `opts.conflict` (`'ours'` keeps local, `'theirs'` takes upstream).
|
|
1554
|
+
*
|
|
1555
|
+
* All inputs are WRAPPED form-data containers (`{ value: domainData }`). The
|
|
1556
|
+
* result `next` is a fresh container; no input is mutated.
|
|
1557
|
+
*
|
|
1558
|
+
* `diffOptions` are forwarded to BOTH internal `buildFormDiff` passes so the
|
|
1559
|
+
* same field exclusions apply (notably the `@db.column.version` column and the
|
|
1560
|
+
* `$cas` policy) on the local and upstream sides — keep them identical to the
|
|
1561
|
+
* options the caller uses for its own change tracking.
|
|
1562
|
+
*/
|
|
1563
|
+
function buildFormRebase(def, baseline, current, upstream, opts, diffOptions) {
|
|
1564
|
+
const conflictMode = opts?.conflict ?? "ours";
|
|
1565
|
+
const local = buildFormDiff(def, baseline, current, diffOptions).changes;
|
|
1566
|
+
const upstreamChanges = buildFormDiff(def, baseline, upstream, diffOptions).changes;
|
|
1567
|
+
const upstreamByPath = /* @__PURE__ */ new Map();
|
|
1568
|
+
for (const uc of upstreamChanges) upstreamByPath.set(uc.path, uc);
|
|
1569
|
+
const next = deepClone(upstream);
|
|
1570
|
+
const conflicts = [];
|
|
1571
|
+
for (const lc of local) {
|
|
1572
|
+
const clearedAncestor = findClearedAncestor(lc.path, baseline, upstream);
|
|
1573
|
+
if (clearedAncestor !== void 0) {
|
|
1574
|
+
conflicts.push(clearedAncestor);
|
|
1575
|
+
if (conflictMode === "ours") setByPath(next, clearedAncestor, deepClone(getByPath(current, clearedAncestor)));
|
|
1576
|
+
continue;
|
|
1577
|
+
}
|
|
1578
|
+
const uc = upstreamByPath.get(lc.path);
|
|
1579
|
+
if (uc !== void 0) {
|
|
1580
|
+
if (deepEqual(lc.after, uc.after)) continue;
|
|
1581
|
+
conflicts.push(lc.path);
|
|
1582
|
+
if (conflictMode === "ours") reapply(def, next, lc);
|
|
1583
|
+
continue;
|
|
1584
|
+
}
|
|
1585
|
+
reapply(def, next, lc);
|
|
1586
|
+
}
|
|
1587
|
+
const reapplied = buildFormDiff(def, upstream, deepClone(next), diffOptions).changes;
|
|
1588
|
+
return {
|
|
1589
|
+
next,
|
|
1590
|
+
conflicts: [...new Set(conflicts)],
|
|
1591
|
+
reapplied
|
|
1592
|
+
};
|
|
1593
|
+
}
|
|
1594
|
+
/**
|
|
1595
|
+
* Reapplies a single local change onto `next`, DEEP-CLONING its `after` value
|
|
1596
|
+
* first. `lc.after` is a LIVE reference into `current` (buildFormDiff holds live
|
|
1597
|
+
* refs), so for a `kind:'array'` or whole-object/union `set` change a raw apply
|
|
1598
|
+
* would make `next.value`'s node `===` `current.value`'s node — violating the
|
|
1599
|
+
* `FormRebaseResult.next` contract ("never aliases any input container"). The
|
|
1600
|
+
* ancestor-clear branch already deep-clones before writing; this keeps the two
|
|
1601
|
+
* leaf-reapply sites consistent.
|
|
1602
|
+
*/
|
|
1603
|
+
function reapply(def, next, lc) {
|
|
1604
|
+
applyFormChanges(def, next, [{
|
|
1605
|
+
...lc,
|
|
1606
|
+
after: deepClone(lc.after)
|
|
1607
|
+
}]);
|
|
1608
|
+
}
|
|
1609
|
+
/**
|
|
1610
|
+
* Returns the SHALLOWEST strict ancestor of `leafPath` that was an object/array
|
|
1611
|
+
* in `baseline` but is `null`/`undefined` in `upstream` (upstream cleared the
|
|
1612
|
+
* subtree), or `undefined` when no ancestor was cleared. The leaf path itself is
|
|
1613
|
+
* never considered an ancestor.
|
|
1614
|
+
*/
|
|
1615
|
+
function findClearedAncestor(leafPath, baseline, upstream) {
|
|
1616
|
+
if (!leafPath.includes(".")) return void 0;
|
|
1617
|
+
const segs = leafPath.split(".");
|
|
1618
|
+
let acc = "";
|
|
1619
|
+
for (let i = 0; i < segs.length - 1; i++) {
|
|
1620
|
+
acc = acc ? `${acc}.${segs[i]}` : segs[i];
|
|
1621
|
+
const base = getByPath(baseline, acc);
|
|
1622
|
+
if (typeof base !== "object" || base === null) continue;
|
|
1623
|
+
const up = getByPath(upstream, acc);
|
|
1624
|
+
if (up === null || up === void 0) return acc;
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
//#endregion
|
|
1628
|
+
//#region src/form/union-detect.ts
|
|
1629
|
+
/**
|
|
1630
|
+
* True when ANY union field in the form resolves to a DIFFERENT discriminated
|
|
1631
|
+
* variant between two wrapped data containers. A variant picker typically
|
|
1632
|
+
* detects its variant index once at setup and keys the variant subtree on it,
|
|
1633
|
+
* so a rebase that lands a different variant (via conflict OR an upstream-only
|
|
1634
|
+
* switch) needs a remount to re-detect. This walks union + nested-object fields
|
|
1635
|
+
* and compares `detectUnionVariant` at each union path.
|
|
1636
|
+
*
|
|
1637
|
+
* Scope note (pragmatic): walks standalone + nested-OBJECT union fields. Unions
|
|
1638
|
+
* nested INSIDE array items are not walked — an array renderer that keeps a
|
|
1639
|
+
* stable per-item key across in-place value mutations would not remount an
|
|
1640
|
+
* existing row's picker on an upstream-driven variant flip, but that collision
|
|
1641
|
+
* (a 3-way rebase landing a different union variant inside an unchanged array
|
|
1642
|
+
* row) is a rare edge. TODO: extend to array-item unions if a real consumer
|
|
1643
|
+
* hits a stuck picker inside an array row.
|
|
1644
|
+
*/
|
|
1645
|
+
function unionVariantChanged(def, before, after) {
|
|
1646
|
+
return walkUnionFields(def.fields, "", before, after);
|
|
1647
|
+
}
|
|
1648
|
+
function walkUnionFields(fields, prefix, before, after) {
|
|
1649
|
+
for (const field of fields) {
|
|
1650
|
+
if (field.phantom) continue;
|
|
1651
|
+
const fullPath = field.path ? prefix ? `${prefix}.${field.path}` : field.path : prefix;
|
|
1652
|
+
if (isUnionField(field)) {
|
|
1653
|
+
const variants = field.unionVariants;
|
|
1654
|
+
if (variants.length > 1) {
|
|
1655
|
+
if (detectUnionVariant(getByPath(before, fullPath), variants) !== detectUnionVariant(getByPath(after, fullPath), variants)) return true;
|
|
1656
|
+
}
|
|
1657
|
+
continue;
|
|
1658
|
+
}
|
|
1659
|
+
if (isObjectField(field)) {
|
|
1660
|
+
const objectDef = field.objectDef;
|
|
1661
|
+
if (walkUnionFields(objectDef.fields, fullPath, before, after)) return true;
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
return false;
|
|
1665
|
+
}
|
|
1666
|
+
//#endregion
|
|
951
1667
|
//#region src/form/error-utils.ts
|
|
952
1668
|
/**
|
|
953
1669
|
* Framework-agnostic helpers for working with form-error maps keyed by
|
|
@@ -1432,6 +2148,7 @@ function createTableDef(meta, preDeserializedType) {
|
|
|
1432
2148
|
const kind = prop.type.kind;
|
|
1433
2149
|
if (path.includes(".") || kind === "object" || kind === "array") continue;
|
|
1434
2150
|
}
|
|
2151
|
+
if (getFieldMeta(prop, "ui.table.exclude") !== void 0) continue;
|
|
1435
2152
|
const fieldMeta = meta.fields[path];
|
|
1436
2153
|
const options = extractLiteralOptions(prop);
|
|
1437
2154
|
const valueHelpInfo = extractValueHelp(prop);
|
|
@@ -1448,7 +2165,6 @@ function createTableDef(meta, preDeserializedType) {
|
|
|
1448
2165
|
sortable: fieldMeta?.sortable ?? false,
|
|
1449
2166
|
filterable: fieldMeta?.filterable ?? false,
|
|
1450
2167
|
nullable: prop.optional === true,
|
|
1451
|
-
visible: getFieldMeta(prop, UI_TABLE_HIDDEN) === void 0,
|
|
1452
2168
|
width: getFieldMeta(prop, UI_TABLE_WIDTH),
|
|
1453
2169
|
maxLen: maxLengthMeta?.length,
|
|
1454
2170
|
order: getFieldMeta(prop, "ui.table.order") ?? Infinity,
|
|
@@ -1464,6 +2180,7 @@ function createTableDef(meta, preDeserializedType) {
|
|
|
1464
2180
|
type,
|
|
1465
2181
|
columns,
|
|
1466
2182
|
flatMap,
|
|
2183
|
+
fetchableFields: new Set(Object.keys(meta.fields)),
|
|
1467
2184
|
primaryKeys: meta.primaryKeys,
|
|
1468
2185
|
preferredId: meta.preferredId ?? meta.primaryKeys,
|
|
1469
2186
|
versionColumn: meta.versionColumn,
|
|
@@ -1537,10 +2254,6 @@ function str(value) {
|
|
|
1537
2254
|
}
|
|
1538
2255
|
//#endregion
|
|
1539
2256
|
//#region src/table/column-resolver.ts
|
|
1540
|
-
/** Get visible columns only, already sorted by order. */
|
|
1541
|
-
function getVisibleColumns(def) {
|
|
1542
|
-
return def.columns.filter((c) => c.visible);
|
|
1543
|
-
}
|
|
1544
2257
|
/** Get sortable columns. */
|
|
1545
2258
|
function getSortableColumns(def) {
|
|
1546
2259
|
return def.columns.filter((c) => c.sortable);
|
|
@@ -1621,11 +2334,11 @@ exports.UI_FORM_VALIDATE = UI_FORM_VALIDATE;
|
|
|
1621
2334
|
exports.UI_TABLE_ATTR = UI_TABLE_ATTR;
|
|
1622
2335
|
exports.UI_TABLE_CLASSES = UI_TABLE_CLASSES;
|
|
1623
2336
|
exports.UI_TABLE_COMPONENT = UI_TABLE_COMPONENT;
|
|
2337
|
+
exports.UI_TABLE_EXCLUDE = UI_TABLE_EXCLUDE;
|
|
1624
2338
|
exports.UI_TABLE_FN_ATTR = UI_TABLE_FN_ATTR;
|
|
1625
2339
|
exports.UI_TABLE_FN_CLASSES = UI_TABLE_FN_CLASSES;
|
|
1626
2340
|
exports.UI_TABLE_FN_PREFIX = UI_TABLE_FN_PREFIX;
|
|
1627
2341
|
exports.UI_TABLE_FN_STYLES = UI_TABLE_FN_STYLES;
|
|
1628
|
-
exports.UI_TABLE_HIDDEN = UI_TABLE_HIDDEN;
|
|
1629
2342
|
exports.UI_TABLE_ORDER = UI_TABLE_ORDER;
|
|
1630
2343
|
exports.UI_TABLE_SELECT_WITH = UI_TABLE_SELECT_WITH;
|
|
1631
2344
|
exports.UI_TABLE_STYLES = UI_TABLE_STYLES;
|
|
@@ -1634,16 +2347,23 @@ exports.UI_TABLE_WIDTH = UI_TABLE_WIDTH;
|
|
|
1634
2347
|
exports.UI_TYPE = UI_TYPE;
|
|
1635
2348
|
exports.ValueHelpClient = ValueHelpClient;
|
|
1636
2349
|
exports.WF_ACTION_WITH_DATA = WF_ACTION_WITH_DATA;
|
|
2350
|
+
exports.applyFormChanges = applyFormChanges;
|
|
1637
2351
|
exports.asArray = asArray;
|
|
1638
2352
|
exports.buildDescendantErrorCounts = buildDescendantErrorCounts;
|
|
2353
|
+
exports.buildFormDiff = buildFormDiff;
|
|
2354
|
+
exports.buildFormRebase = buildFormRebase;
|
|
1639
2355
|
exports.buildGridClasses = buildGridClasses;
|
|
1640
2356
|
exports.buildUnionVariants = buildUnionVariants;
|
|
2357
|
+
exports.collectDirtyPaths = collectDirtyPaths;
|
|
1641
2358
|
exports.createFieldValidator = createFieldValidator;
|
|
1642
2359
|
exports.createFormData = createFormData;
|
|
1643
2360
|
exports.createFormDef = createFormDef;
|
|
1644
2361
|
exports.createFormValueResolver = createFormValueResolver;
|
|
1645
2362
|
exports.createTableDef = createTableDef;
|
|
2363
|
+
exports.deepClone = deepClone;
|
|
2364
|
+
exports.deepEqual = deepEqual;
|
|
1646
2365
|
exports.defaultResolver = defaultResolver;
|
|
2366
|
+
exports.deleteByPath = deleteByPath;
|
|
1647
2367
|
exports.detectUnionVariant = detectUnionVariant;
|
|
1648
2368
|
exports.enforceScale = enforceScale;
|
|
1649
2369
|
exports.extractLiteralOptions = extractLiteralOptions;
|
|
@@ -1665,11 +2385,11 @@ exports.getMetaEntry = getMetaEntry;
|
|
|
1665
2385
|
exports.getResolver = getResolver;
|
|
1666
2386
|
exports.getSortableColumns = getSortableColumns;
|
|
1667
2387
|
exports.getThousandsSeparator = getThousandsSeparator;
|
|
1668
|
-
exports.getVisibleColumns = getVisibleColumns;
|
|
1669
2388
|
exports.groupInteger = groupInteger;
|
|
1670
2389
|
exports.hasComputedAnnotations = hasComputedAnnotations;
|
|
1671
2390
|
exports.isArrayField = isArrayField;
|
|
1672
2391
|
exports.isObjectField = isObjectField;
|
|
2392
|
+
exports.isPathDirty = isPathDirty;
|
|
1673
2393
|
exports.isPureLiteralUnion = isPureLiteralUnion;
|
|
1674
2394
|
exports.isTupleField = isTupleField;
|
|
1675
2395
|
exports.isUnionField = isUnionField;
|
|
@@ -1700,4 +2420,5 @@ exports.setDefaultValidatorPlugins = setDefaultValidatorPlugins;
|
|
|
1700
2420
|
exports.setResolver = setResolver;
|
|
1701
2421
|
exports.splitDecimalString = splitDecimalString;
|
|
1702
2422
|
exports.str = str;
|
|
2423
|
+
exports.unionVariantChanged = unionVariantChanged;
|
|
1703
2424
|
exports.valueHelpDictPaths = valueHelpDictPaths;
|