@atscript/ui-table 0.1.139 → 0.1.141
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 +414 -63
- package/dist/index.d.cts +287 -20
- package/dist/index.d.mts +287 -20
- package/dist/index.mjs +407 -65
- package/package.json +8 -8
package/dist/index.cjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
let _uniqu_core = require("@uniqu/core");
|
|
3
|
+
let _uniqu_url_builder = require("@uniqu/url/builder");
|
|
3
4
|
let _atscript_db_client = require("@atscript/db-client");
|
|
4
5
|
let _atscript_ui = require("@atscript/ui");
|
|
5
|
-
let _uniqu_url_builder = require("@uniqu/url/builder");
|
|
6
6
|
let _uniqu_url = require("@uniqu/url");
|
|
7
7
|
//#region src/filters/filter-conditions.ts
|
|
8
8
|
/** Conditions that operate purely on nullability — value is ignored. */
|
|
@@ -509,8 +509,16 @@ const RANGE_OPS = {
|
|
|
509
509
|
$lt: "lt",
|
|
510
510
|
$lte: "lte"
|
|
511
511
|
};
|
|
512
|
-
/**
|
|
513
|
-
|
|
512
|
+
/**
|
|
513
|
+
* Field paths a Uniquery filter expression references, deduped, in order of
|
|
514
|
+
* appearance (logical operators are walked, operator keys skipped).
|
|
515
|
+
*
|
|
516
|
+
* @internal Exported for `@atscript/vue-table`.
|
|
517
|
+
*/
|
|
518
|
+
function filterExprFields(expr) {
|
|
519
|
+
return fieldsOf(expr, []);
|
|
520
|
+
}
|
|
521
|
+
function fieldsOf(expr, out) {
|
|
514
522
|
if (Array.isArray(expr)) {
|
|
515
523
|
for (const child of expr) fieldsOf(child, out);
|
|
516
524
|
return out;
|
|
@@ -520,8 +528,9 @@ function fieldsOf(expr, out = []) {
|
|
|
520
528
|
else if (!out.includes(key)) out.push(key);
|
|
521
529
|
return out;
|
|
522
530
|
}
|
|
531
|
+
/** @internal Module-shared with the URL parser; not a package export. */
|
|
523
532
|
function unsupported(reason, expr) {
|
|
524
|
-
const fields = fieldsOf(expr);
|
|
533
|
+
const fields = fieldsOf(expr, []);
|
|
525
534
|
return {
|
|
526
535
|
reason: fields.length > 1 ? "cross-field" : reason,
|
|
527
536
|
expr,
|
|
@@ -699,6 +708,7 @@ function invert(cond) {
|
|
|
699
708
|
* anything else would invert into an OR of ANDs, which the model cannot hold.
|
|
700
709
|
*/
|
|
701
710
|
function collectNot(child, out) {
|
|
711
|
+
if (isPlainObject(child) && Object.keys(child).length === 1 && "$not" in child) return collect(child.$not, out);
|
|
702
712
|
const expr = { $not: child };
|
|
703
713
|
const b = classifyBranch(child);
|
|
704
714
|
if (b && !("reason" in b) && (b.terms.length === 1 || b.terms.every(isNegative))) {
|
|
@@ -742,30 +752,27 @@ function warnUnsupported(issue) {
|
|
|
742
752
|
console.warn(`[ui-table] Filter left out (${issue.reason}): ${JSON.stringify(issue.expr)}. Field filters cannot express it, so the result is broader than the source filter.`);
|
|
743
753
|
}
|
|
744
754
|
/**
|
|
745
|
-
*
|
|
746
|
-
*
|
|
747
|
-
*
|
|
748
|
-
*
|
|
749
|
-
*
|
|
750
|
-
*
|
|
751
|
-
* - `$in` becomes equality conditions on the field, `$nin` inequality ones.
|
|
752
|
-
* - A same-field `$or` becomes that field's OR'd conditions.
|
|
753
|
-
* - A `$not` that inverts equality / emptiness becomes the inverse conditions.
|
|
754
|
-
* - Fields next to `$and` / `$or` / `$not` in one object are all kept.
|
|
755
|
-
* - Anything else (see {@link UnsupportedFilterReason}) is left out. Leaving
|
|
756
|
-
* an AND-ed piece out only ever widens the match, so the result selects a
|
|
757
|
-
* superset of `expr`. Each left-out piece goes to `onUnsupportedFilter`, or
|
|
758
|
-
* to a dev-mode `console.warn` when no handler is given.
|
|
755
|
+
* Split a Uniquery `FilterExpr` into what the table's field-filter model
|
|
756
|
+
* holds exactly (`filters`), what it cannot hold but carries as residual
|
|
757
|
+
* conditions (`residual`, with `carry`), what it leaves out (`unsupported`)
|
|
758
|
+
* and what names a field outside `knownFields` (`unknown`). Nothing is
|
|
759
|
+
* approximated: `filters AND residual` selects `expr` minus the
|
|
760
|
+
* `unsupported` and `unknown` pieces.
|
|
759
761
|
*
|
|
760
|
-
*
|
|
761
|
-
* silently: they are not this table's (a host page flag, a stale column). A
|
|
762
|
-
* piece that mixes known and unknown fields is reported.
|
|
762
|
+
* Never throws, never warns — the caller decides how to report.
|
|
763
763
|
*
|
|
764
|
-
*
|
|
764
|
+
* @since 0.1.140
|
|
765
765
|
*/
|
|
766
|
-
function
|
|
767
|
-
const
|
|
768
|
-
|
|
766
|
+
function decomposeUniqueryFilter(expr, opts = {}) {
|
|
767
|
+
const filters = {};
|
|
768
|
+
const result = {
|
|
769
|
+
filters,
|
|
770
|
+
residual: [],
|
|
771
|
+
unsupported: [],
|
|
772
|
+
unknown: []
|
|
773
|
+
};
|
|
774
|
+
if (!expr) return result;
|
|
775
|
+
const knownFields = opts.knownFields;
|
|
769
776
|
const known = knownFields == null ? null : knownFields instanceof Set ? knownFields : new Set(knownFields);
|
|
770
777
|
const isKnown = (field) => known === null || known.has(field);
|
|
771
778
|
const out = {
|
|
@@ -783,21 +790,170 @@ function uniqueryFilterToFieldFilters(expr, knownFields, onUnsupportedFilter = w
|
|
|
783
790
|
}];
|
|
784
791
|
}
|
|
785
792
|
const positives = /* @__PURE__ */ new Map();
|
|
793
|
+
const unknown = /* @__PURE__ */ new Map();
|
|
786
794
|
for (const term of out.terms) {
|
|
787
|
-
if (!isKnown(term.field))
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
795
|
+
if (!isKnown(term.field)) {
|
|
796
|
+
unknown.set(term.expr, [term.field]);
|
|
797
|
+
continue;
|
|
798
|
+
}
|
|
799
|
+
const list = filters[term.field] ??= [];
|
|
800
|
+
if (isNegative(term)) {
|
|
792
801
|
list.push(...term.conds);
|
|
793
|
-
|
|
802
|
+
continue;
|
|
803
|
+
}
|
|
804
|
+
const groups = positives.get(term.field);
|
|
805
|
+
if (!groups) {
|
|
806
|
+
positives.set(term.field, [term]);
|
|
807
|
+
list.push(...term.conds);
|
|
808
|
+
} else if (!groups.some((g) => sameConds(g.conds, term.conds))) groups.push(term);
|
|
809
|
+
}
|
|
810
|
+
const carried = [];
|
|
811
|
+
for (const [field, groups] of positives) {
|
|
812
|
+
if (groups.length < 2) continue;
|
|
813
|
+
const leftOut = opts.carry ? groups : groups.slice(1);
|
|
814
|
+
if (opts.carry) {
|
|
815
|
+
const kept = new Set(groups[0].conds);
|
|
816
|
+
const rest = filters[field].filter((c) => !kept.has(c));
|
|
817
|
+
if (rest.length > 0) filters[field] = rest;
|
|
818
|
+
else delete filters[field];
|
|
819
|
+
}
|
|
820
|
+
for (const term of leftOut) out.issues.push({
|
|
794
821
|
reason: "conjunction",
|
|
795
822
|
expr: term.expr,
|
|
796
|
-
fields: [
|
|
823
|
+
fields: [field]
|
|
797
824
|
});
|
|
798
825
|
}
|
|
799
|
-
for (const issue of out.issues)
|
|
800
|
-
|
|
826
|
+
for (const issue of out.issues) {
|
|
827
|
+
const fields = issue.fields;
|
|
828
|
+
const outside = fields.filter((f) => !isKnown(f));
|
|
829
|
+
if (outside.length > 0) unknown.set(issue.expr, outside);
|
|
830
|
+
else if (opts.carry && fields.length > 0) carried.push(issue.expr);
|
|
831
|
+
else result.unsupported.push(issue);
|
|
832
|
+
}
|
|
833
|
+
result.residual = normalizeResidualFilters(carried);
|
|
834
|
+
result.unknown = [...unknown].map(([expr, fields]) => ({
|
|
835
|
+
expr,
|
|
836
|
+
fields
|
|
837
|
+
}));
|
|
838
|
+
return result;
|
|
839
|
+
}
|
|
840
|
+
/**
|
|
841
|
+
* Canonical identity of a filter expression — its `@uniqu/url` spelling.
|
|
842
|
+
* Two expressions with the same key select the same rows.
|
|
843
|
+
*
|
|
844
|
+
* @since 0.1.140
|
|
845
|
+
*/
|
|
846
|
+
function filterExprKey(expr) {
|
|
847
|
+
try {
|
|
848
|
+
return (0, _uniqu_url_builder.buildUrl)({ filter: expr });
|
|
849
|
+
} catch {
|
|
850
|
+
return JSON.stringify(expr) ?? "";
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
/**
|
|
854
|
+
* A residual-condition list with empty expressions and duplicates (by
|
|
855
|
+
* {@link filterExprKey}) dropped, sorted by key. Sorted, not first-appearance:
|
|
856
|
+
* the URL parser moves \`$not\`-wrapped clauses (how \`mergeFilters\` spells
|
|
857
|
+
* a repeated same-field clause) ahead of plain ones, so appearance order would
|
|
858
|
+
* flip on every round trip.
|
|
859
|
+
*
|
|
860
|
+
* @internal Exported for `@atscript/vue-table`.
|
|
861
|
+
*/
|
|
862
|
+
function normalizeResidualFilters(exprs) {
|
|
863
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
864
|
+
for (const expr of exprs) {
|
|
865
|
+
if (!isPlainObject(expr)) continue;
|
|
866
|
+
const key = filterExprKey(expr);
|
|
867
|
+
if (key && !byKey.has(key)) byKey.set(key, expr);
|
|
868
|
+
}
|
|
869
|
+
return [...byKey.keys()].toSorted().map((key) => byKey.get(key));
|
|
870
|
+
}
|
|
871
|
+
/**
|
|
872
|
+
* Convert a Uniquery `FilterExpr` back into the UI's `FieldFilters` shape.
|
|
873
|
+
*
|
|
874
|
+
* Inverse of `filtersToUniqueryFilter`, and exact for everything that encoder
|
|
875
|
+
* produces. For any other input, each AND-ed piece is either converted exactly
|
|
876
|
+
* or left out whole and reported — never approximated:
|
|
877
|
+
*
|
|
878
|
+
* - `$in` becomes equality conditions on the field, `$nin` inequality ones.
|
|
879
|
+
* - A same-field `$or` becomes that field's OR'd conditions.
|
|
880
|
+
* - A `$not` that inverts equality / emptiness becomes the inverse conditions.
|
|
881
|
+
* - Fields next to `$and` / `$or` / `$not` in one object are all kept.
|
|
882
|
+
* - Anything else (see {@link UnsupportedFilterReason}) is left out. Leaving
|
|
883
|
+
* an AND-ed piece out only ever widens the match, so the result selects a
|
|
884
|
+
* superset of `expr`. Each left-out piece goes to `onUnsupportedFilter`, or
|
|
885
|
+
* to a dev-mode `console.warn` when no handler is given. To keep those
|
|
886
|
+
* pieces instead, use {@link decomposeUniqueryFilter} with `carry`.
|
|
887
|
+
*
|
|
888
|
+
* Pieces that name a field outside `knownFields` (when provided) are ignored
|
|
889
|
+
* silently: they are not this table's (a host page flag, a hidden or stale
|
|
890
|
+
* column). Since 0.1.141 that includes a piece mixing known and unknown
|
|
891
|
+
* fields — {@link decomposeUniqueryFilter} lists them as `unknown`.
|
|
892
|
+
*
|
|
893
|
+
* Returns `{}` for an empty/missing expression. Never throws.
|
|
894
|
+
*/
|
|
895
|
+
function uniqueryFilterToFieldFilters(expr, knownFields, onUnsupportedFilter = warnUnsupported) {
|
|
896
|
+
const { filters, unsupported } = decomposeUniqueryFilter(expr, { knownFields });
|
|
897
|
+
for (const issue of unsupported) onUnsupportedFilter(issue);
|
|
898
|
+
return filters;
|
|
899
|
+
}
|
|
900
|
+
//#endregion
|
|
901
|
+
//#region src/filters/format-filter-expr.ts
|
|
902
|
+
function show(v) {
|
|
903
|
+
if (v instanceof Date) return v.toISOString();
|
|
904
|
+
if (typeof v === "object" && v !== null) return JSON.stringify(v) ?? "";
|
|
905
|
+
return String(v);
|
|
906
|
+
}
|
|
907
|
+
/** One comparison, worded like the filter chips (the decoder's operator table). */
|
|
908
|
+
function leaf(label, op, value) {
|
|
909
|
+
if (op === "$eq" && value instanceof RegExp) op = "$regex";
|
|
910
|
+
if ((op === "$in" || op === "$nin") && Array.isArray(value)) return `${label} ${op === "$in" ? "is one of" : "is none of"} ${value.map(show).join(", ")}`;
|
|
911
|
+
const conds = decodeOperator(op, value);
|
|
912
|
+
if (conds?.length === 1) return filterTokenLabel(label, conds, label);
|
|
913
|
+
return (0, _uniqu_url_builder.buildUrl)({ filter: { [label]: { [op]: value } } }) || `${label}${op}`;
|
|
914
|
+
}
|
|
915
|
+
function join(children, kind) {
|
|
916
|
+
const parts = children.filter((c) => c.s);
|
|
917
|
+
if (parts.length === 0) return {
|
|
918
|
+
s: "",
|
|
919
|
+
kind: "leaf"
|
|
920
|
+
};
|
|
921
|
+
if (parts.length === 1) return parts[0];
|
|
922
|
+
const wrap = kind === "and" ? "or" : "and";
|
|
923
|
+
return {
|
|
924
|
+
s: parts.map((c) => c.kind === wrap ? `(${c.s})` : c.s).join(` ${kind} `),
|
|
925
|
+
kind
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
/**
|
|
929
|
+
* Human-readable rendering of a Uniquery filter expression, worded like the
|
|
930
|
+
* filter-field chips: `(Status equals shipped and Total greater than 500) or
|
|
931
|
+
* (Status equals pending and Total less or equal 50)`. `and` binds tighter
|
|
932
|
+
* than `or`; groups are parenthesized only where needed. Operators without a
|
|
933
|
+
* wording fall back to their `@uniqu/url` spelling.
|
|
934
|
+
*
|
|
935
|
+
* @param labelOf — display label for a field path (e.g. the column label);
|
|
936
|
+
* the path itself when omitted or when it returns `undefined`.
|
|
937
|
+
* @since 0.1.140
|
|
938
|
+
*/
|
|
939
|
+
function formatFilterExpr(expr, labelOf) {
|
|
940
|
+
const visitor = {
|
|
941
|
+
comparison: (field, op, value) => ({
|
|
942
|
+
s: leaf(labelOf?.(field) ?? field, op, value),
|
|
943
|
+
kind: "leaf"
|
|
944
|
+
}),
|
|
945
|
+
and: (children) => join(children, "and"),
|
|
946
|
+
or: (children) => join(children, "or"),
|
|
947
|
+
not: (child) => ({
|
|
948
|
+
s: child.s ? `not (${child.s})` : "",
|
|
949
|
+
kind: "leaf"
|
|
950
|
+
})
|
|
951
|
+
};
|
|
952
|
+
try {
|
|
953
|
+
return (0, _uniqu_core.walkFilter)(expr, visitor)?.s ?? "";
|
|
954
|
+
} catch {
|
|
955
|
+
return JSON.stringify(expr) ?? "";
|
|
956
|
+
}
|
|
801
957
|
}
|
|
802
958
|
//#endregion
|
|
803
959
|
//#region src/filters/date-shortcuts.ts
|
|
@@ -1002,6 +1158,137 @@ function resolveSystemPresets(input) {
|
|
|
1002
1158
|
}, ...named];
|
|
1003
1159
|
}
|
|
1004
1160
|
//#endregion
|
|
1161
|
+
//#region src/presets/prune-preset-snapshot.ts
|
|
1162
|
+
/** `[kept, dropped]` in one pass. */
|
|
1163
|
+
function partition(list, keep) {
|
|
1164
|
+
const kept = [];
|
|
1165
|
+
const dropped = [];
|
|
1166
|
+
for (const x of list) (keep(x) ? kept : dropped).push(x);
|
|
1167
|
+
return [kept, dropped];
|
|
1168
|
+
}
|
|
1169
|
+
/** The columns aspect, an empty width map spelled by omission — the way capture writes it. */
|
|
1170
|
+
function columnsAspect(columnNames, columnWidths) {
|
|
1171
|
+
return columnWidths && Object.keys(columnWidths).length > 0 ? {
|
|
1172
|
+
columnNames,
|
|
1173
|
+
columnWidths
|
|
1174
|
+
} : { columnNames };
|
|
1175
|
+
}
|
|
1176
|
+
/**
|
|
1177
|
+
* Remove every entry of `snapshot` that names a field outside `known`, per
|
|
1178
|
+
* aspect:
|
|
1179
|
+
*
|
|
1180
|
+
* - `columns.columnNames` — unknown names go, the rest keep their order. When
|
|
1181
|
+
* none survives, it falls back to every column, never an empty grid.
|
|
1182
|
+
* - `columns.columnWidths` — unknown keys go.
|
|
1183
|
+
* - `filters` (displayed inputs) and `filterOps` — entries on a field outside
|
|
1184
|
+
* `known.server` go. A field's conditions are one AND-ed conjunct, so
|
|
1185
|
+
* dropping one only broadens the result.
|
|
1186
|
+
* - `sorters` — unknown entries go, the rest keep their priority.
|
|
1187
|
+
* - `itemsPerPage` — untouched.
|
|
1188
|
+
*
|
|
1189
|
+
* `dropped` is `null` when nothing user-visible went (a width-only drop is
|
|
1190
|
+
* hygiene). The input is never mutated.
|
|
1191
|
+
*
|
|
1192
|
+
* @since 0.1.141
|
|
1193
|
+
*/
|
|
1194
|
+
function prunePresetSnapshot(snapshot, known) {
|
|
1195
|
+
const isColumn = (p) => known.columns.has(p);
|
|
1196
|
+
const isServer = (p) => known.server.has(p);
|
|
1197
|
+
const out = { ...snapshot };
|
|
1198
|
+
const dropped = {
|
|
1199
|
+
fields: [],
|
|
1200
|
+
columns: [],
|
|
1201
|
+
filterFields: [],
|
|
1202
|
+
filters: {},
|
|
1203
|
+
residual: [],
|
|
1204
|
+
sorters: []
|
|
1205
|
+
};
|
|
1206
|
+
if (snapshot.columns) {
|
|
1207
|
+
const [names, gone] = partition(snapshot.columns.columnNames, isColumn);
|
|
1208
|
+
const [widths] = partition(Object.entries(snapshot.columns.columnWidths ?? {}), ([p]) => isColumn(p));
|
|
1209
|
+
dropped.columns = gone;
|
|
1210
|
+
out.columns = columnsAspect(names.length > 0 || gone.length === 0 ? names : [...known.columns], Object.fromEntries(widths));
|
|
1211
|
+
}
|
|
1212
|
+
if (snapshot.filters) [out.filters, dropped.filterFields] = partition(snapshot.filters, isServer);
|
|
1213
|
+
if (snapshot.filterOps) {
|
|
1214
|
+
const [kept, gone] = partition(Object.entries(snapshot.filterOps), ([p]) => isServer(p));
|
|
1215
|
+
out.filterOps = Object.fromEntries(kept);
|
|
1216
|
+
dropped.filters = Object.fromEntries(gone);
|
|
1217
|
+
}
|
|
1218
|
+
if (snapshot.sorters) [out.sorters, dropped.sorters] = partition(snapshot.sorters, (s) => isColumn(s.field));
|
|
1219
|
+
dropped.fields = [...new Set([
|
|
1220
|
+
...dropped.columns,
|
|
1221
|
+
...dropped.filterFields,
|
|
1222
|
+
...Object.keys(dropped.filters),
|
|
1223
|
+
...dropped.sorters.map((s) => s.field)
|
|
1224
|
+
])];
|
|
1225
|
+
return {
|
|
1226
|
+
snapshot: out,
|
|
1227
|
+
dropped: dropped.fields.length > 0 ? dropped : null
|
|
1228
|
+
};
|
|
1229
|
+
}
|
|
1230
|
+
/**
|
|
1231
|
+
* Split residual filter conditions, in one pass, into the ones that name
|
|
1232
|
+
* only `known` fields and the ones that do not. A condition naming any
|
|
1233
|
+
* unknown field is dropped WHOLE — never pruned inside an `$or` (that would
|
|
1234
|
+
* narrow the result) or a `$not` (that would invert it). Each condition is an
|
|
1235
|
+
* AND-ed conjunct, so dropping one only broadens the result.
|
|
1236
|
+
*
|
|
1237
|
+
* `dropped` holds the conditions in `residual` and the unknown paths in
|
|
1238
|
+
* `fields`; `null` when every condition is kept.
|
|
1239
|
+
*
|
|
1240
|
+
* @since 0.1.141
|
|
1241
|
+
*/
|
|
1242
|
+
function pruneResidualFilters(exprs, known) {
|
|
1243
|
+
const kept = [];
|
|
1244
|
+
const residual = [];
|
|
1245
|
+
const fields = /* @__PURE__ */ new Set();
|
|
1246
|
+
for (const expr of exprs) {
|
|
1247
|
+
const unknown = filterExprFields(expr).filter((p) => !known.has(p));
|
|
1248
|
+
if (unknown.length === 0) {
|
|
1249
|
+
kept.push(expr);
|
|
1250
|
+
continue;
|
|
1251
|
+
}
|
|
1252
|
+
residual.push(expr);
|
|
1253
|
+
for (const p of unknown) fields.add(p);
|
|
1254
|
+
}
|
|
1255
|
+
if (residual.length === 0) return {
|
|
1256
|
+
kept,
|
|
1257
|
+
dropped: null
|
|
1258
|
+
};
|
|
1259
|
+
return {
|
|
1260
|
+
kept,
|
|
1261
|
+
dropped: {
|
|
1262
|
+
fields: [...fields],
|
|
1263
|
+
columns: [],
|
|
1264
|
+
filterFields: [],
|
|
1265
|
+
filters: {},
|
|
1266
|
+
residual,
|
|
1267
|
+
sorters: []
|
|
1268
|
+
}
|
|
1269
|
+
};
|
|
1270
|
+
}
|
|
1271
|
+
/**
|
|
1272
|
+
* Append what a prune dropped back onto a snapshot captured from (pruned)
|
|
1273
|
+
* table state, so overwriting a preset with a narrower view does not destroy
|
|
1274
|
+
* the parts the saver cannot see. Column names, filter inputs and sorters go
|
|
1275
|
+
* after the captured ones; field filters are added back. Only aspects
|
|
1276
|
+
* `captured` carries are touched; widths of hidden columns are not kept.
|
|
1277
|
+
*
|
|
1278
|
+
* @since 0.1.141
|
|
1279
|
+
*/
|
|
1280
|
+
function restoreDroppedEntries(captured, dropped) {
|
|
1281
|
+
const out = { ...captured };
|
|
1282
|
+
if (captured.columns) out.columns = columnsAspect([...captured.columns.columnNames, ...dropped.columns], captured.columns.columnWidths);
|
|
1283
|
+
if (captured.filters) out.filters = [...captured.filters, ...dropped.filterFields];
|
|
1284
|
+
if (captured.filterOps) out.filterOps = {
|
|
1285
|
+
...captured.filterOps,
|
|
1286
|
+
...dropped.filters
|
|
1287
|
+
};
|
|
1288
|
+
if (captured.sorters) out.sorters = [...captured.sorters, ...dropped.sorters];
|
|
1289
|
+
return out;
|
|
1290
|
+
}
|
|
1291
|
+
//#endregion
|
|
1005
1292
|
//#region src/presets/preset-dirty.ts
|
|
1006
1293
|
/**
|
|
1007
1294
|
* JSON-stringify with object keys sorted alphabetically at every depth so
|
|
@@ -1394,8 +1681,9 @@ function mergeSorters(forceSorters, userSorters) {
|
|
|
1394
1681
|
//#endregion
|
|
1395
1682
|
//#region src/query/merge-filters.ts
|
|
1396
1683
|
/**
|
|
1397
|
-
* AND-merge
|
|
1398
|
-
* `@uniqu/url`'s `mergeConjunction` parser collapse.
|
|
1684
|
+
* AND-merge filter expressions (`undefined` ones skipped), producing a wire
|
|
1685
|
+
* shape that survives `@uniqu/url`'s `mergeConjunction` parser collapse.
|
|
1686
|
+
* Variadic since 0.1.140.
|
|
1399
1687
|
*
|
|
1400
1688
|
* The collapse problem: when two `$and` siblings target the same field
|
|
1401
1689
|
* with the same op (e.g. `{status: 'cancelled'}` AND `{status: 'shipped'}`),
|
|
@@ -1408,10 +1696,10 @@ function mergeSorters(forceSorters, userSorters) {
|
|
|
1408
1696
|
* and `!!p ≡ p` is a semantic identity, so the server evaluator sees the
|
|
1409
1697
|
* same AND. Non-colliding merges produce the canonical `$and` shape.
|
|
1410
1698
|
*/
|
|
1411
|
-
function mergeFilters(
|
|
1412
|
-
|
|
1413
|
-
if (
|
|
1414
|
-
return makeParserSafeAnd(
|
|
1699
|
+
function mergeFilters(...exprs) {
|
|
1700
|
+
const list = exprs.filter((e) => !!e);
|
|
1701
|
+
if (list.length <= 1) return list[0];
|
|
1702
|
+
return makeParserSafeAnd(list);
|
|
1415
1703
|
}
|
|
1416
1704
|
/** Op-set for a field value: primitives are `$eq`, op-bags expose their keys. */
|
|
1417
1705
|
function getOpsForFieldValue(value) {
|
|
@@ -1479,12 +1767,12 @@ function makeParserSafeAnd(children) {
|
|
|
1479
1767
|
* Build a Uniquery object from table UI state.
|
|
1480
1768
|
*
|
|
1481
1769
|
* Pure function — no framework dependencies.
|
|
1482
|
-
* Combines user filters
|
|
1770
|
+
* Combines user filters (field filters, then residual conditions) with force
|
|
1771
|
+
* filters, merges sorters,
|
|
1483
1772
|
* projects visible columns, and applies pagination.
|
|
1484
1773
|
*/
|
|
1485
1774
|
function buildTableQuery(opts) {
|
|
1486
|
-
const
|
|
1487
|
-
const filter = mergeFilters(opts.forceFilters, userFilter);
|
|
1775
|
+
const filter = mergeFilters(opts.forceFilters, filtersToUniqueryFilter(opts.filters), ...opts.residualFilters ?? []);
|
|
1488
1776
|
const userSorters = opts.ignoreSorters ? [] : opts.sorters;
|
|
1489
1777
|
const sorters = opts.forceSorters?.length ? mergeSorters(opts.forceSorters, userSorters) : userSorters;
|
|
1490
1778
|
const $sort = {};
|
|
@@ -1544,6 +1832,17 @@ function gateOwns(gate, path) {
|
|
|
1544
1832
|
if (gate === "none") return false;
|
|
1545
1833
|
return gate.has(path);
|
|
1546
1834
|
}
|
|
1835
|
+
/**
|
|
1836
|
+
* Does a URL under `gate` own the residual condition `expr`? Only when it
|
|
1837
|
+
* owns every field the condition references — a condition that touches a
|
|
1838
|
+
* private field stays private as a whole.
|
|
1839
|
+
*
|
|
1840
|
+
* @internal Exported for `@atscript/vue-table`.
|
|
1841
|
+
*/
|
|
1842
|
+
function residualGateOwns(gate, expr) {
|
|
1843
|
+
const fields = filterExprFields(expr);
|
|
1844
|
+
return fields.length > 0 && fields.every((path) => gateOwns(gate, path));
|
|
1845
|
+
}
|
|
1547
1846
|
function pickFilterPaths(filters, gate) {
|
|
1548
1847
|
const out = {};
|
|
1549
1848
|
for (const path in filters) if (gateOwns(gate, path)) out[path] = filters[path];
|
|
@@ -1570,6 +1869,7 @@ function stateToUrlQueryString(state, defaults) {
|
|
|
1570
1869
|
visibleColumnPaths: [],
|
|
1571
1870
|
sorters: sortersGate === "all" ? state.sorters : state.sorters.filter((s) => gateOwns(sortersGate, s.field)),
|
|
1572
1871
|
filters,
|
|
1872
|
+
residualFilters: defaults.sync?.residual === false || !state.residualFilters?.length ? void 0 : state.residualFilters.filter((expr) => residualGateOwns(filtersGate, expr)),
|
|
1573
1873
|
search: searchOff ? void 0 : state.searchTerm || void 0
|
|
1574
1874
|
});
|
|
1575
1875
|
if (!searchOff && state.searchTerm && state.ignoreSorters !== void 0 && state.ignoreSorters !== (defaults.defaultIgnoreSorters ?? false)) query.controls.$relevance = state.ignoreSorters ? 1 : 0;
|
|
@@ -1592,8 +1892,25 @@ const CONSUMED_CONTROLS = new Set([
|
|
|
1592
1892
|
"$skip",
|
|
1593
1893
|
URL_SNAPSHOT_KEY
|
|
1594
1894
|
]);
|
|
1595
|
-
|
|
1596
|
-
|
|
1895
|
+
function toSet(paths) {
|
|
1896
|
+
if (!paths) return null;
|
|
1897
|
+
return paths instanceof Set ? paths : new Set(paths);
|
|
1898
|
+
}
|
|
1899
|
+
/**
|
|
1900
|
+
* `field=value` (or `field=null`) — the one filter shape a host page flag
|
|
1901
|
+
* shares, so a bare one on an unknown field is not reported as dropped.
|
|
1902
|
+
*/
|
|
1903
|
+
function isBareEquality(expr) {
|
|
1904
|
+
const keys = Object.keys(expr);
|
|
1905
|
+
if (keys.length !== 1 || keys[0].startsWith("$")) return false;
|
|
1906
|
+
const v = expr[keys[0]];
|
|
1907
|
+
return v === null || typeof v !== "object";
|
|
1908
|
+
}
|
|
1909
|
+
/**
|
|
1910
|
+
* Characters that can only appear in a uniqu filter key, never in a page flag:
|
|
1911
|
+
* comparison operators, `^` (OR), `( )` (groups) and `{ }` (`$in` lists).
|
|
1912
|
+
*/
|
|
1913
|
+
const FILTER_OPERATOR_CHAR = /[<>!~()^{}]/;
|
|
1597
1914
|
/**
|
|
1598
1915
|
* Whether {@link urlQueryStringToState} would consume the query key `key` —
|
|
1599
1916
|
* i.e. whether the key belongs to the table rather than to the page hosting
|
|
@@ -1615,12 +1932,15 @@ function urlQueryConsumesKey(key) {
|
|
|
1615
1932
|
/**
|
|
1616
1933
|
* Parse a URL query string back into the table state subset.
|
|
1617
1934
|
*
|
|
1618
|
-
* Robust by design — schema drift
|
|
1619
|
-
* recipient's view:
|
|
1620
|
-
* - unknown fields (not in `knownFields`) →
|
|
1935
|
+
* Robust by design — schema drift, fields hidden from the recipient and
|
|
1936
|
+
* copy-paste errors must not break the recipient's view:
|
|
1937
|
+
* - unknown fields (not in `knownFields`) → dropped, listed in `unknown` /
|
|
1938
|
+
* `unknownSorters` (the parser does not warn — the caller decides)
|
|
1621
1939
|
* - filter pieces field filters cannot express (cross-field OR, unknown
|
|
1622
1940
|
* operator, …) → left out of `filters` and listed in `unsupported`, never
|
|
1623
|
-
* approximated (the parser does not warn — the caller decides)
|
|
1941
|
+
* approximated (the parser does not warn — the caller decides). Unless
|
|
1942
|
+
* `sync.residual` is `false`, those whose fields are all known come back
|
|
1943
|
+
* in `residual` instead
|
|
1624
1944
|
* - unknown controls (e.g. `$weird=42`) → silently ignored
|
|
1625
1945
|
* - malformed query → `{ filters: {}, sorters: [], searchTerm: "" }`
|
|
1626
1946
|
*
|
|
@@ -1648,30 +1968,35 @@ function urlQueryStringToState(urlString, opts = {}) {
|
|
|
1648
1968
|
const sortersGate = resolveAspectGate(opts.sync?.sorters);
|
|
1649
1969
|
const searchOff = opts.sync?.search === false;
|
|
1650
1970
|
const paginationOff = opts.sync?.pagination === false;
|
|
1651
|
-
const knownSet =
|
|
1971
|
+
const knownSet = toSet(opts.knownFields);
|
|
1972
|
+
const localSet = toSet(opts.localFields);
|
|
1973
|
+
const isHidden = (f) => !!knownSet && !knownSet.has(f) && !localSet?.has(f);
|
|
1652
1974
|
let filterKnown;
|
|
1653
1975
|
if (filtersGate === "all") filterKnown = knownSet ?? void 0;
|
|
1654
1976
|
else if (filtersGate !== "none") if (knownSet) {
|
|
1655
|
-
|
|
1656
|
-
for (const path of filtersGate) if (knownSet.has(path))
|
|
1977
|
+
const owned = /* @__PURE__ */ new Set();
|
|
1978
|
+
for (const path of filtersGate) if (knownSet.has(path)) owned.add(path);
|
|
1979
|
+
filterKnown = owned;
|
|
1657
1980
|
} else filterKnown = filtersGate;
|
|
1658
|
-
const
|
|
1659
|
-
|
|
1981
|
+
const decomposed = filtersGate === "none" ? null : decomposeUniqueryFilter(parsed.filter, {
|
|
1982
|
+
knownFields: filterKnown,
|
|
1983
|
+
carry: opts.sync?.residual !== false
|
|
1984
|
+
});
|
|
1985
|
+
const filters = decomposed?.filters ?? {};
|
|
1660
1986
|
const sorters = [];
|
|
1987
|
+
const unknownSorters = [];
|
|
1661
1988
|
if (sortersGate !== "none") {
|
|
1662
1989
|
const $sort = parsed.controls?.$sort;
|
|
1663
1990
|
if ($sort && typeof $sort === "object") for (const field in $sort) {
|
|
1664
|
-
if (knownSet && !knownSet.has(field)) continue;
|
|
1665
1991
|
if (!gateOwns(sortersGate, field)) continue;
|
|
1666
1992
|
const dir = $sort[field];
|
|
1667
|
-
if (dir
|
|
1993
|
+
if (dir !== 1 && dir !== -1) continue;
|
|
1994
|
+
const sorter = {
|
|
1668
1995
|
field,
|
|
1669
|
-
direction: "asc"
|
|
1670
|
-
}
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
direction: "desc"
|
|
1674
|
-
});
|
|
1996
|
+
direction: dir === 1 ? "asc" : "desc"
|
|
1997
|
+
};
|
|
1998
|
+
if (!knownSet || knownSet.has(field)) sorters.push(sorter);
|
|
1999
|
+
else if (isHidden(field)) unknownSorters.push(sorter);
|
|
1675
2000
|
}
|
|
1676
2001
|
}
|
|
1677
2002
|
const $search = parsed.controls?.$search;
|
|
@@ -1680,7 +2005,24 @@ function urlQueryStringToState(urlString, opts = {}) {
|
|
|
1680
2005
|
sorters,
|
|
1681
2006
|
searchTerm: !searchOff && typeof $search === "string" ? $search : ""
|
|
1682
2007
|
};
|
|
1683
|
-
|
|
2008
|
+
const unsupported$1 = decomposed?.unsupported ?? [];
|
|
2009
|
+
const unknown = [];
|
|
2010
|
+
for (const piece of decomposed?.unknown ?? []) {
|
|
2011
|
+
const hidden = piece.fields.filter(isHidden);
|
|
2012
|
+
if (hidden.length > 0) {
|
|
2013
|
+
if (!isBareEquality(piece.expr)) unknown.push({
|
|
2014
|
+
expr: piece.expr,
|
|
2015
|
+
fields: hidden
|
|
2016
|
+
});
|
|
2017
|
+
continue;
|
|
2018
|
+
}
|
|
2019
|
+
const issue = unsupported("cross-field", piece.expr);
|
|
2020
|
+
if (issue.fields.some((f) => filterKnown?.has(f))) unsupported$1.push(issue);
|
|
2021
|
+
}
|
|
2022
|
+
if (unsupported$1.length) out.unsupported = unsupported$1;
|
|
2023
|
+
if (decomposed?.residual.length) out.residual = decomposed.residual;
|
|
2024
|
+
if (unknown.length) out.unknown = unknown;
|
|
2025
|
+
if (unknownSorters.length) out.unknownSorters = unknownSorters;
|
|
1684
2026
|
if (opts.sync?.snapshot !== false && parsed.controls && "$snapshot" in parsed.controls) out.snapshot = true;
|
|
1685
2027
|
if (!searchOff) {
|
|
1686
2028
|
const $relevance = parsed.controls?.$relevance;
|
|
@@ -2265,15 +2607,19 @@ exports.conditionsForType = conditionsForType;
|
|
|
2265
2607
|
exports.csvCell = csvCell;
|
|
2266
2608
|
exports.dateShortcuts = dateShortcuts;
|
|
2267
2609
|
exports.debounce = debounce;
|
|
2610
|
+
exports.decomposeUniqueryFilter = decomposeUniqueryFilter;
|
|
2268
2611
|
exports.defaultCondition = defaultCondition;
|
|
2269
2612
|
exports.derivePresetAspects = derivePresetAspects;
|
|
2270
2613
|
exports.deserializeDraft = deserializeDraft;
|
|
2271
2614
|
exports.draftMatchesPreset = draftMatchesPreset;
|
|
2272
2615
|
exports.escapeRegex = escapeRegex;
|
|
2273
2616
|
exports.filledFilterCount = filledFilterCount;
|
|
2617
|
+
exports.filterExprFields = filterExprFields;
|
|
2618
|
+
exports.filterExprKey = filterExprKey;
|
|
2274
2619
|
exports.filterTokenLabel = filterTokenLabel;
|
|
2275
2620
|
exports.filtersToUniqueryFilter = filtersToUniqueryFilter;
|
|
2276
2621
|
exports.formatFilterCondition = formatFilterCondition;
|
|
2622
|
+
exports.formatFilterExpr = formatFilterExpr;
|
|
2277
2623
|
exports.fromWireSnapshot = fromWireSnapshot;
|
|
2278
2624
|
exports.gateOwns = gateOwns;
|
|
2279
2625
|
exports.hasSecondValue = hasSecondValue;
|
|
@@ -2289,15 +2635,20 @@ exports.mergeDisplayColumns = mergeDisplayColumns;
|
|
|
2289
2635
|
exports.mergeFilters = mergeFilters;
|
|
2290
2636
|
exports.mergeSorters = mergeSorters;
|
|
2291
2637
|
exports.normaliseSystemPresetId = normaliseSystemPresetId;
|
|
2638
|
+
exports.normalizeResidualFilters = normalizeResidualFilters;
|
|
2292
2639
|
exports.pageAlignedBlocksFor = pageAlignedBlocksFor;
|
|
2293
2640
|
exports.parseColumnFilterInput = parseColumnFilterInput;
|
|
2294
2641
|
exports.parseFilterInput = parseFilterInput;
|
|
2295
2642
|
exports.planFetch = planFetch;
|
|
2643
|
+
exports.prunePresetSnapshot = prunePresetSnapshot;
|
|
2644
|
+
exports.pruneResidualFilters = pruneResidualFilters;
|
|
2296
2645
|
exports.reconcileColumnWidthDefaults = reconcileColumnWidthDefaults;
|
|
2297
2646
|
exports.reorderColumnNames = reorderColumnNames;
|
|
2647
|
+
exports.residualGateOwns = residualGateOwns;
|
|
2298
2648
|
exports.resolveAspectGate = resolveAspectGate;
|
|
2299
2649
|
exports.resolveExportValue = resolveExportValue;
|
|
2300
2650
|
exports.resolveSystemPresets = resolveSystemPresets;
|
|
2651
|
+
exports.restoreDroppedEntries = restoreDroppedEntries;
|
|
2301
2652
|
exports.rowsToPks = rowsToPks;
|
|
2302
2653
|
exports.sameColumnSet = sameColumnSet;
|
|
2303
2654
|
exports.serializeDraft = serializeDraft;
|