@atscript/ui-table 0.1.132 → 0.1.134
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 +299 -16
- package/dist/index.d.cts +222 -8
- package/dist/index.d.mts +222 -8
- package/dist/index.mjs +289 -18
- package/package.json +8 -8
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ClientError } from "@atscript/db-client";
|
|
2
|
-
import { getDefaultClientFactory } from "@atscript/ui";
|
|
2
|
+
import { getDefaultClientFactory, str } from "@atscript/ui";
|
|
3
3
|
import { buildUrl } from "@uniqu/url/builder";
|
|
4
4
|
import { parseUrl } from "@uniqu/url";
|
|
5
5
|
//#region src/filters/filter-conditions.ts
|
|
@@ -148,13 +148,22 @@ function unescapeRegex(input) {
|
|
|
148
148
|
//#region src/filters/filter-input-format.ts
|
|
149
149
|
/**
|
|
150
150
|
* Coerce a raw string value to the appropriate JS type for the column.
|
|
151
|
-
* Number columns get numeric values
|
|
151
|
+
* Number columns get numeric values, boolean columns understand the
|
|
152
|
+
* `true` / `false` vocabulary (case-insensitive) so the condition carries a
|
|
153
|
+
* real boolean and round-trips through the URL as one. Anything else stays
|
|
154
|
+
* a string. Since 0.1.133 for booleans.
|
|
152
155
|
*/
|
|
153
156
|
function coerceValue(raw, columnType) {
|
|
154
157
|
if (columnType === "number") {
|
|
155
158
|
const n = Number(raw);
|
|
156
159
|
return Number.isNaN(n) ? raw : n;
|
|
157
160
|
}
|
|
161
|
+
if (columnType === "boolean") {
|
|
162
|
+
const lower = raw.toLowerCase();
|
|
163
|
+
if (lower === "true") return true;
|
|
164
|
+
if (lower === "false") return false;
|
|
165
|
+
return raw;
|
|
166
|
+
}
|
|
158
167
|
return raw;
|
|
159
168
|
}
|
|
160
169
|
/** Default condition type when no symbol matches the input. */
|
|
@@ -579,6 +588,10 @@ function derivePresetAspects(content) {
|
|
|
579
588
|
* Convert in-memory dict-form snapshot to the wire form persisted on the
|
|
580
589
|
* server. Entries-arrays are sorted by `field` so consumers (server-side
|
|
581
590
|
* aspect derivation, dirty detection, equality checks) see a stable order.
|
|
591
|
+
*
|
|
592
|
+
* Empty aspects round-trip as empty (since 0.1.133): a saved view with no
|
|
593
|
+
* filters owns `filterOps` and clears them on apply, which is not the same
|
|
594
|
+
* thing as a view that never claimed the aspect.
|
|
582
595
|
*/
|
|
583
596
|
function toWireSnapshot(snapshot) {
|
|
584
597
|
const wire = {};
|
|
@@ -591,7 +604,7 @@ function toWireSnapshot(snapshot) {
|
|
|
591
604
|
field,
|
|
592
605
|
width: columnWidths[field]
|
|
593
606
|
});
|
|
594
|
-
|
|
607
|
+
columns.columnWidths = entries;
|
|
595
608
|
}
|
|
596
609
|
wire.columns = columns;
|
|
597
610
|
}
|
|
@@ -602,7 +615,7 @@ function toWireSnapshot(snapshot) {
|
|
|
602
615
|
field,
|
|
603
616
|
conditions: snapshot.filterOps[field]
|
|
604
617
|
});
|
|
605
|
-
|
|
618
|
+
wire.filterOps = entries;
|
|
606
619
|
}
|
|
607
620
|
if (snapshot.sorters) wire.sorters = snapshot.sorters;
|
|
608
621
|
if (snapshot.itemsPerPage !== void 0) wire.itemsPerPage = snapshot.itemsPerPage;
|
|
@@ -613,7 +626,7 @@ function fromWireSnapshot(wire) {
|
|
|
613
626
|
if (wire.columns) {
|
|
614
627
|
const { columnNames, columnWidths } = wire.columns;
|
|
615
628
|
const columns = { columnNames };
|
|
616
|
-
if (columnWidths
|
|
629
|
+
if (columnWidths) {
|
|
617
630
|
const dict = {};
|
|
618
631
|
for (const entry of columnWidths) dict[entry.field] = entry.width;
|
|
619
632
|
columns.columnWidths = dict;
|
|
@@ -621,7 +634,7 @@ function fromWireSnapshot(wire) {
|
|
|
621
634
|
snapshot.columns = columns;
|
|
622
635
|
}
|
|
623
636
|
if (wire.filters) snapshot.filters = wire.filters;
|
|
624
|
-
if (wire.filterOps
|
|
637
|
+
if (wire.filterOps) {
|
|
625
638
|
const dict = {};
|
|
626
639
|
for (const entry of wire.filterOps) dict[entry.field] = entry.conditions;
|
|
627
640
|
snapshot.filterOps = dict;
|
|
@@ -818,7 +831,7 @@ function draftMatchesPreset(draft, presetSnapshot, availableAspects) {
|
|
|
818
831
|
/**
|
|
819
832
|
* Framework-agnostic wrapper over `@atscript/db-client`'s `Client` for the
|
|
820
833
|
* `AsPresetEntry` table. Handles wire serialisation, list-splitting by
|
|
821
|
-
* `type`, capabilities side-channel, and 401/403 → `denied` semantics.
|
|
834
|
+
* `type`, capabilities side-channel, and 401/403/404 → `denied` semantics.
|
|
822
835
|
*
|
|
823
836
|
* Stateless: every method is a fresh request. The Vue composable layer
|
|
824
837
|
* holds reactive state; this class only translates intent → HTTP.
|
|
@@ -844,8 +857,8 @@ var PresetsClient = class {
|
|
|
844
857
|
* `(app, tableKey)`. By default also fetches `capabilities` in parallel —
|
|
845
858
|
* pass `{ capabilities: false }` for refresh-after-mutation calls (fav
|
|
846
859
|
* toggle, default change, save/save-as, public toggle, rename, delete)
|
|
847
|
-
* where role-derived capabilities can't have changed.
|
|
848
|
-
* (401/403) collapse to `denied: true` with empty data so the UI hides
|
|
860
|
+
* where role-derived capabilities can't have changed. Unavailable
|
|
861
|
+
* responses (401/403/404) collapse to `denied: true` with empty data so the UI hides
|
|
849
862
|
* itself silently.
|
|
850
863
|
*/
|
|
851
864
|
async list(opts = {}) {
|
|
@@ -857,7 +870,7 @@ var PresetsClient = class {
|
|
|
857
870
|
};
|
|
858
871
|
try {
|
|
859
872
|
const [rows, capabilities] = await Promise.all([this.client.query({ filter }), fetchCapabilities ? this.loadCapabilities().catch((err) => {
|
|
860
|
-
if (
|
|
873
|
+
if (isUnavailableError(err)) throw err;
|
|
861
874
|
return null;
|
|
862
875
|
}) : Promise.resolve(void 0)]);
|
|
863
876
|
const presets = [];
|
|
@@ -871,7 +884,7 @@ var PresetsClient = class {
|
|
|
871
884
|
denied: false
|
|
872
885
|
};
|
|
873
886
|
} catch (err) {
|
|
874
|
-
if (
|
|
887
|
+
if (isUnavailableError(err)) return {
|
|
875
888
|
presets: [],
|
|
876
889
|
userConf: null,
|
|
877
890
|
capabilities: null,
|
|
@@ -987,12 +1000,22 @@ var PresetsHttpError = class extends Error {
|
|
|
987
1000
|
this.name = "PresetsHttpError";
|
|
988
1001
|
}
|
|
989
1002
|
};
|
|
990
|
-
/**
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
1003
|
+
/**
|
|
1004
|
+
* True when the presets/app-prefs surface is simply not available to this
|
|
1005
|
+
* client: HTTP 401/403 (not signed in / not permitted) or 404 (the
|
|
1006
|
+
* controller is not mounted on this server). All three collapse to
|
|
1007
|
+
* `denied: true` — the UI hides itself silently instead of surfacing an
|
|
1008
|
+
* error. Covers both `ClientError` and `PresetsHttpError`.
|
|
1009
|
+
*/
|
|
1010
|
+
function isUnavailableError(err) {
|
|
1011
|
+
const status = err instanceof ClientError || err instanceof PresetsHttpError ? err.status : void 0;
|
|
1012
|
+
return status === 401 || status === 403 || status === 404;
|
|
995
1013
|
}
|
|
1014
|
+
/**
|
|
1015
|
+
* @deprecated Renamed to `isUnavailableError` in 0.1.133 — it also covers
|
|
1016
|
+
* 404 (feature not mounted), not just auth failures.
|
|
1017
|
+
*/
|
|
1018
|
+
const isAuthError = isUnavailableError;
|
|
996
1019
|
//#endregion
|
|
997
1020
|
//#region src/presets/app-prefs-client.ts
|
|
998
1021
|
/**
|
|
@@ -1025,7 +1048,7 @@ var AppPrefsClient = class {
|
|
|
1025
1048
|
denied: false
|
|
1026
1049
|
};
|
|
1027
1050
|
} catch (err) {
|
|
1028
|
-
if (
|
|
1051
|
+
if (isUnavailableError(err)) return {
|
|
1029
1052
|
row: null,
|
|
1030
1053
|
prefs: null,
|
|
1031
1054
|
denied: true
|
|
@@ -1231,6 +1254,36 @@ function stateToUrlQueryString(state, defaults) {
|
|
|
1231
1254
|
return buildUrl(query);
|
|
1232
1255
|
}
|
|
1233
1256
|
/**
|
|
1257
|
+
* The `$`-controls {@link urlQueryStringToState} actually reads. Any other
|
|
1258
|
+
* `$key` is ignored by the parser, so it is NOT the table's to remove.
|
|
1259
|
+
*/
|
|
1260
|
+
const CONSUMED_CONTROLS = new Set([
|
|
1261
|
+
"$sort",
|
|
1262
|
+
"$search",
|
|
1263
|
+
"$relevance",
|
|
1264
|
+
"$skip"
|
|
1265
|
+
]);
|
|
1266
|
+
/** Characters that can only appear in a uniqu filter key, never in a page flag. */
|
|
1267
|
+
const FILTER_OPERATOR_CHAR = /[<>!~]/;
|
|
1268
|
+
/**
|
|
1269
|
+
* Whether {@link urlQueryStringToState} would consume the query key `key` —
|
|
1270
|
+
* i.e. whether the key belongs to the table rather than to the page hosting
|
|
1271
|
+
* it. This is the ownership rule the `useTableUrlQuery` router bridge reads
|
|
1272
|
+
* and writes by, so both directions agree: a key the parser ignores is never
|
|
1273
|
+
* removed, and a key it reads is the table's to remove.
|
|
1274
|
+
*
|
|
1275
|
+
* Shape-only, and deliberately conservative for bare `field=value` keys: a
|
|
1276
|
+
* filter on a column and a host flag are indistinguishable without the table
|
|
1277
|
+
* definition, so those are owned only once the bridge has written them
|
|
1278
|
+
* itself (or once a `prefix` namespaces them).
|
|
1279
|
+
*
|
|
1280
|
+
* @since 0.1.133
|
|
1281
|
+
*/
|
|
1282
|
+
function urlQueryConsumesKey(key) {
|
|
1283
|
+
if (key.startsWith("$")) return CONSUMED_CONTROLS.has(key);
|
|
1284
|
+
return FILTER_OPERATOR_CHAR.test(key);
|
|
1285
|
+
}
|
|
1286
|
+
/**
|
|
1234
1287
|
* Parse a URL query string back into the table state subset.
|
|
1235
1288
|
*
|
|
1236
1289
|
* Robust by design — schema drift and copy-paste errors must not break the
|
|
@@ -1627,4 +1680,222 @@ function reorderColumnNames(names, fromPath, toPath, position) {
|
|
|
1627
1680
|
return without.slice(0, insertAt).concat(fromPath, without.slice(insertAt));
|
|
1628
1681
|
}
|
|
1629
1682
|
//#endregion
|
|
1630
|
-
export
|
|
1683
|
+
//#region src/export/csv.ts
|
|
1684
|
+
const FORMULA_LEADERS = new Set([
|
|
1685
|
+
"=",
|
|
1686
|
+
"+",
|
|
1687
|
+
"-",
|
|
1688
|
+
"@",
|
|
1689
|
+
" ",
|
|
1690
|
+
"\r"
|
|
1691
|
+
]);
|
|
1692
|
+
/** RFC 4180 row separator. */
|
|
1693
|
+
const CRLF = "\r\n";
|
|
1694
|
+
/**
|
|
1695
|
+
* Render one value as an RFC 4180 field: formula-escaped (unless opted out),
|
|
1696
|
+
* then quoted when it contains the delimiter, a quote, CR or LF — with inner
|
|
1697
|
+
* quotes doubled.
|
|
1698
|
+
*/
|
|
1699
|
+
function csvCell(value, opts = {}) {
|
|
1700
|
+
const delimiter = opts.delimiter ?? ",";
|
|
1701
|
+
let text = value === null || value === void 0 ? "" : String(value);
|
|
1702
|
+
if (typeof value === "string" && opts.escapeFormulas !== false && text.length > 0 && FORMULA_LEADERS.has(text[0])) text = `'${text}`;
|
|
1703
|
+
return text.includes(delimiter) || text.includes("\"") || text.includes("\n") || text.includes("\r") ? `"${text.replaceAll("\"", "\"\"")}"` : text;
|
|
1704
|
+
}
|
|
1705
|
+
/**
|
|
1706
|
+
* Serialise a header row + data rows to an RFC 4180 CSV string (`\r\n` row
|
|
1707
|
+
* separators, quoted fields where required, optional UTF-8 BOM).
|
|
1708
|
+
*
|
|
1709
|
+
* Pure — no DOM, no framework. See {@link csvCell} for the per-field rules.
|
|
1710
|
+
*/
|
|
1711
|
+
function toCsv(header, rows, opts = {}) {
|
|
1712
|
+
const delimiter = opts.delimiter ?? ",";
|
|
1713
|
+
const lines = [header.map((h) => csvCell(h, opts)).join(delimiter)];
|
|
1714
|
+
for (const row of rows) lines.push(row.map((v) => csvCell(v, opts)).join(delimiter));
|
|
1715
|
+
const body = lines.join(CRLF) + CRLF;
|
|
1716
|
+
return opts.bom ? `${body}` : body;
|
|
1717
|
+
}
|
|
1718
|
+
//#endregion
|
|
1719
|
+
//#region src/export/export-paging.ts
|
|
1720
|
+
/** Default page size used by the export pager when the caller gives none. */
|
|
1721
|
+
const DEFAULT_EXPORT_PAGE_SIZE = 500;
|
|
1722
|
+
/** Rejection raised when an export is cancelled through its `AbortSignal`. */
|
|
1723
|
+
var ExportAbortError = class extends Error {
|
|
1724
|
+
/** Matches the DOM convention so `err.name === "AbortError"` checks work. */
|
|
1725
|
+
name = "AbortError";
|
|
1726
|
+
constructor(message = "Export aborted") {
|
|
1727
|
+
super(message);
|
|
1728
|
+
}
|
|
1729
|
+
};
|
|
1730
|
+
/**
|
|
1731
|
+
* Append the primary key(s) to a query's `$sort` as a tiebreaker so paging is
|
|
1732
|
+
* stable: two rows equal on every user sorter keep a deterministic relative
|
|
1733
|
+
* order, and no row is skipped or duplicated across page boundaries.
|
|
1734
|
+
*
|
|
1735
|
+
* Existing sort fields are preserved in order and never replaced — a pk the
|
|
1736
|
+
* user already sorts by is left where it is. Returns the input query when
|
|
1737
|
+
* there is nothing to add.
|
|
1738
|
+
*/
|
|
1739
|
+
function withStableOrder(query, primaryKeys) {
|
|
1740
|
+
if (primaryKeys.length === 0) return query;
|
|
1741
|
+
const current = query.controls?.$sort ?? {};
|
|
1742
|
+
const missing = primaryKeys.filter((pk) => !(pk in current));
|
|
1743
|
+
if (missing.length === 0) return query;
|
|
1744
|
+
const $sort = { ...current };
|
|
1745
|
+
for (const pk of missing) $sort[pk] = 1;
|
|
1746
|
+
return {
|
|
1747
|
+
...query,
|
|
1748
|
+
controls: {
|
|
1749
|
+
...query.controls,
|
|
1750
|
+
$sort
|
|
1751
|
+
}
|
|
1752
|
+
};
|
|
1753
|
+
}
|
|
1754
|
+
/**
|
|
1755
|
+
* Page through `query` until the endpoint runs out of rows, collecting every
|
|
1756
|
+
* row into one array.
|
|
1757
|
+
*
|
|
1758
|
+
* Stops when a page comes back short (fewer rows than `pageSize`), when the
|
|
1759
|
+
* reported `count` is reached, or when `maxRows` is hit. Checks `signal`
|
|
1760
|
+
* before and after every request so a cancel lands between pages rather than
|
|
1761
|
+
* mid-flight.
|
|
1762
|
+
*/
|
|
1763
|
+
async function collectExportRows(opts) {
|
|
1764
|
+
const pageSize = opts.pageSize && opts.pageSize > 0 ? opts.pageSize : 500;
|
|
1765
|
+
const mapRow = opts.mapRow ?? ((row) => row);
|
|
1766
|
+
const rows = [];
|
|
1767
|
+
let total;
|
|
1768
|
+
let page = 1;
|
|
1769
|
+
for (;;) {
|
|
1770
|
+
throwIfAborted(opts.signal);
|
|
1771
|
+
const result = await opts.fetchPage(opts.query, page, pageSize);
|
|
1772
|
+
throwIfAborted(opts.signal);
|
|
1773
|
+
if (page === 1 && typeof result.count === "number") total = result.count;
|
|
1774
|
+
const data = result.data ?? [];
|
|
1775
|
+
const take = opts.maxRows === void 0 ? data.length : Math.min(data.length, opts.maxRows - rows.length);
|
|
1776
|
+
for (let i = 0; i < take; i++) rows.push(mapRow(data[i]));
|
|
1777
|
+
opts.onProgress?.(rows.length, total);
|
|
1778
|
+
if (data.length < pageSize || opts.maxRows !== void 0 && rows.length >= opts.maxRows || total !== void 0 && rows.length >= total) break;
|
|
1779
|
+
page++;
|
|
1780
|
+
}
|
|
1781
|
+
return {
|
|
1782
|
+
rows,
|
|
1783
|
+
total
|
|
1784
|
+
};
|
|
1785
|
+
}
|
|
1786
|
+
function throwIfAborted(signal) {
|
|
1787
|
+
if (signal?.aborted) throw new ExportAbortError();
|
|
1788
|
+
}
|
|
1789
|
+
//#endregion
|
|
1790
|
+
//#region src/export/export-value.ts
|
|
1791
|
+
/**
|
|
1792
|
+
* Turn a raw cell value into the scalar an export carries, using the same
|
|
1793
|
+
* column metadata the cell renderers read:
|
|
1794
|
+
*
|
|
1795
|
+
* - `null` / `undefined` → `null` (an empty CSV field).
|
|
1796
|
+
* - union / enum columns (`column.options`) → the option's **label**, so the
|
|
1797
|
+
* file reads like the screen; an unknown key falls back to the raw value.
|
|
1798
|
+
* - `Date` → ISO 8601 (spreadsheet- and re-import-friendly, unlike a
|
|
1799
|
+
* locale-formatted date).
|
|
1800
|
+
* - arrays → comma-joined scalars; other objects → JSON.
|
|
1801
|
+
* - numbers / booleans pass through untouched so a spreadsheet keeps them
|
|
1802
|
+
* numeric.
|
|
1803
|
+
*
|
|
1804
|
+
* Callers override per column (`formatters`) or globally (`formatCell`) when
|
|
1805
|
+
* they want different text.
|
|
1806
|
+
*/
|
|
1807
|
+
function resolveExportValue(value, column) {
|
|
1808
|
+
if (value === null || value === void 0) return null;
|
|
1809
|
+
const options = column?.options;
|
|
1810
|
+
if (options?.length) {
|
|
1811
|
+
const key = typeof value === "string" || typeof value === "number" ? String(value) : void 0;
|
|
1812
|
+
if (key !== void 0) {
|
|
1813
|
+
const hit = options.find((o) => o.key === key);
|
|
1814
|
+
if (hit) return hit.label;
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
if (typeof value === "number" || typeof value === "boolean") return value;
|
|
1818
|
+
if (typeof value === "string") return value;
|
|
1819
|
+
if (value instanceof Date) return value.toISOString();
|
|
1820
|
+
if (Array.isArray(value)) return value.map((v) => scalarText(v)).join(", ");
|
|
1821
|
+
return scalarText(value);
|
|
1822
|
+
}
|
|
1823
|
+
function scalarText(value) {
|
|
1824
|
+
if (value === null || value === void 0 || typeof value === "function") return "";
|
|
1825
|
+
try {
|
|
1826
|
+
return str(value);
|
|
1827
|
+
} catch {
|
|
1828
|
+
return "";
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1831
|
+
//#endregion
|
|
1832
|
+
//#region src/columns/display-columns.ts
|
|
1833
|
+
/** Columns with no explicit `order` land after every server column. */
|
|
1834
|
+
const TRAILING_ORDER = Number.MAX_SAFE_INTEGER;
|
|
1835
|
+
/** Turn a {@link DisplayColumnDef} into the `ColumnDef` the table renders. */
|
|
1836
|
+
function displayColumnToDef(def) {
|
|
1837
|
+
return {
|
|
1838
|
+
path: def.key,
|
|
1839
|
+
label: def.label,
|
|
1840
|
+
type: def.type ?? "text",
|
|
1841
|
+
component: def.component,
|
|
1842
|
+
sortable: def.sortable === "local",
|
|
1843
|
+
filterable: false,
|
|
1844
|
+
nullable: true,
|
|
1845
|
+
order: def.order ?? TRAILING_ORDER,
|
|
1846
|
+
width: def.width,
|
|
1847
|
+
local: true
|
|
1848
|
+
};
|
|
1849
|
+
}
|
|
1850
|
+
/**
|
|
1851
|
+
* Merge client-owned columns into the server's column list, ordered by each
|
|
1852
|
+
* column's `order`. `Array#sort` is stable, so server columns that share an
|
|
1853
|
+
* `order` keep their `/meta` sequence. Returns `base` untouched when there is
|
|
1854
|
+
* nothing to merge.
|
|
1855
|
+
*/
|
|
1856
|
+
function mergeDisplayColumns(base, display) {
|
|
1857
|
+
if (display.length === 0) return base;
|
|
1858
|
+
return [...base, ...display.map(displayColumnToDef)].toSorted((a, b) => a.order - b.order);
|
|
1859
|
+
}
|
|
1860
|
+
//#endregion
|
|
1861
|
+
//#region src/utils/sort-rows.ts
|
|
1862
|
+
/**
|
|
1863
|
+
* Coerce a primitive cell value to the text used for comparing and matching
|
|
1864
|
+
* cells — the string compare in {@link sortRowsLocally} and the substring
|
|
1865
|
+
* search of an in-memory table. Objects and functions collapse to `""` so
|
|
1866
|
+
* `'[object Object]'` never drives an ordering or matches a search.
|
|
1867
|
+
*/
|
|
1868
|
+
function cellAsString(v) {
|
|
1869
|
+
if (v == null) return "";
|
|
1870
|
+
if (typeof v === "string") return v;
|
|
1871
|
+
if (typeof v === "number" || typeof v === "boolean") return v.toString();
|
|
1872
|
+
return "";
|
|
1873
|
+
}
|
|
1874
|
+
/**
|
|
1875
|
+
* Sort rows in memory by `sorters`, numerically when both sides are numbers
|
|
1876
|
+
* and locale-aware otherwise. Returns a NEW array; the input is untouched. A
|
|
1877
|
+
* no-op (returns the input reference) when there is nothing to sort.
|
|
1878
|
+
*
|
|
1879
|
+
* `getValue` reads a sorter's field off a row — pass a dot-path reader for
|
|
1880
|
+
* nested fields; the default is a flat property read.
|
|
1881
|
+
*/
|
|
1882
|
+
function sortRowsLocally(rows, sorters, getValue = (row, field) => row[field]) {
|
|
1883
|
+
if (sorters.length === 0 || rows.length < 2) return rows;
|
|
1884
|
+
return rows.toSorted((a, b) => {
|
|
1885
|
+
for (const s of sorters) {
|
|
1886
|
+
const dir = s.direction === "desc" ? -1 : 1;
|
|
1887
|
+
const av = getValue(a, s.field);
|
|
1888
|
+
const bv = getValue(b, s.field);
|
|
1889
|
+
if (typeof av === "number" && typeof bv === "number") {
|
|
1890
|
+
if (av < bv) return -dir;
|
|
1891
|
+
if (av > bv) return dir;
|
|
1892
|
+
} else {
|
|
1893
|
+
const cmp = cellAsString(av).localeCompare(cellAsString(bv));
|
|
1894
|
+
if (cmp !== 0) return cmp * dir;
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
return 0;
|
|
1898
|
+
});
|
|
1899
|
+
}
|
|
1900
|
+
//#endregion
|
|
1901
|
+
export { APP_CONF_PREFIX, AppPrefsClient, DEFAULT_EXPORT_PAGE_SIZE, DEFAULT_ROW_HEIGHT_PX, DRAFT_PERSISTED_ASPECTS, ExportAbortError, MAX_DEFAULT_COLUMN_WIDTH_PX, NULL_OPS, PRESET_ASPECTS, PresetsClient, PresetsHttpError, RESERVED_ID_PREFIXES, STANDARD_PRESET_ID, SYSTEM_PRESET_PREFIX, USER_CONF_PREFIX, appConfId, arraysEqual, blockStartFor, buildTableQuery, cellAsString, clampTopIndex, collectExportRows, columnFilterType, computeDefaultColumnWidth, conditionLabel, conditionsForType, csvCell, dateShortcuts, debounce, defaultCondition, derivePresetAspects, deserializeDraft, draftMatchesPreset, escapeRegex, filledFilterCount, filterTokenLabel, filtersToUniqueryFilter, formatFilterCondition, fromWireSnapshot, hasSecondValue, isAuthError, isDirtyAgainst, isEmptyDraft, isFilled, isSimpleEq, isSystemPresetId, isUnavailableError, mergeDisplayColumns, mergeFilters, mergeSorters, normaliseSystemPresetId, pageAlignedBlocksFor, parseFilterInput, planFetch, reconcileColumnWidthDefaults, reorderColumnNames, resolveAspectGate, resolveExportValue, resolveSystemPresets, rowsToPks, sameColumnSet, serializeDraft, setsEqual, sortRowsLocally, sortersEqual, stableStringify, stateToUrlQueryString, toCsv, toWireSnapshot, togglePk, trimSelection, unescapeRegex, uniqueryFilterToFieldFilters, urlQueryConsumesKey, urlQueryStringToState, userConfId, walkBackwardAbsorb, walkForwardAbsorb, withStableOrder };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atscript/ui-table",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.134",
|
|
4
4
|
"description": "Framework-agnostic filter model, filter-to-Uniquery conversion, and preset serialization for atscript tables",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"atscript",
|
|
@@ -42,18 +42,18 @@
|
|
|
42
42
|
"access": "public"
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@atscript/ui": "0.1.
|
|
45
|
+
"@atscript/ui": "0.1.134"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
|
-
"@atscript/db-client": "^0.1.
|
|
49
|
-
"@uniqu/core": "^0.1.
|
|
50
|
-
"@uniqu/url": "^0.1.
|
|
48
|
+
"@atscript/db-client": "^0.1.129",
|
|
49
|
+
"@uniqu/core": "^0.1.8",
|
|
50
|
+
"@uniqu/url": "^0.1.8",
|
|
51
51
|
"vitest": "npm:@voidzero-dev/vite-plus-test@0.1.14"
|
|
52
52
|
},
|
|
53
53
|
"peerDependencies": {
|
|
54
|
-
"@atscript/db-client": "^0.1.
|
|
55
|
-
"@uniqu/core": "^0.1.
|
|
56
|
-
"@uniqu/url": "^0.1.
|
|
54
|
+
"@atscript/db-client": "^0.1.129",
|
|
55
|
+
"@uniqu/core": "^0.1.8",
|
|
56
|
+
"@uniqu/url": "^0.1.8"
|
|
57
57
|
},
|
|
58
58
|
"scripts": {
|
|
59
59
|
"build": "vp pack",
|