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