@atscript/ui-table 0.1.133 → 0.1.135
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 +228 -0
- package/dist/index.d.cts +185 -1
- package/dist/index.d.mts +185 -1
- package/dist/index.mjs +220 -2
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -1681,10 +1681,230 @@ function reorderColumnNames(names, fromPath, toPath, position) {
|
|
|
1681
1681
|
return without.slice(0, insertAt).concat(fromPath, without.slice(insertAt));
|
|
1682
1682
|
}
|
|
1683
1683
|
//#endregion
|
|
1684
|
+
//#region src/export/csv.ts
|
|
1685
|
+
const FORMULA_LEADERS = new Set([
|
|
1686
|
+
"=",
|
|
1687
|
+
"+",
|
|
1688
|
+
"-",
|
|
1689
|
+
"@",
|
|
1690
|
+
" ",
|
|
1691
|
+
"\r"
|
|
1692
|
+
]);
|
|
1693
|
+
/** RFC 4180 row separator. */
|
|
1694
|
+
const CRLF = "\r\n";
|
|
1695
|
+
/**
|
|
1696
|
+
* Render one value as an RFC 4180 field: formula-escaped (unless opted out),
|
|
1697
|
+
* then quoted when it contains the delimiter, a quote, CR or LF — with inner
|
|
1698
|
+
* quotes doubled.
|
|
1699
|
+
*/
|
|
1700
|
+
function csvCell(value, opts = {}) {
|
|
1701
|
+
const delimiter = opts.delimiter ?? ",";
|
|
1702
|
+
let text = value === null || value === void 0 ? "" : String(value);
|
|
1703
|
+
if (typeof value === "string" && opts.escapeFormulas !== false && text.length > 0 && FORMULA_LEADERS.has(text[0])) text = `'${text}`;
|
|
1704
|
+
return text.includes(delimiter) || text.includes("\"") || text.includes("\n") || text.includes("\r") ? `"${text.replaceAll("\"", "\"\"")}"` : text;
|
|
1705
|
+
}
|
|
1706
|
+
/**
|
|
1707
|
+
* Serialise a header row + data rows to an RFC 4180 CSV string (`\r\n` row
|
|
1708
|
+
* separators, quoted fields where required, optional UTF-8 BOM).
|
|
1709
|
+
*
|
|
1710
|
+
* Pure — no DOM, no framework. See {@link csvCell} for the per-field rules.
|
|
1711
|
+
*/
|
|
1712
|
+
function toCsv(header, rows, opts = {}) {
|
|
1713
|
+
const delimiter = opts.delimiter ?? ",";
|
|
1714
|
+
const lines = [header.map((h) => csvCell(h, opts)).join(delimiter)];
|
|
1715
|
+
for (const row of rows) lines.push(row.map((v) => csvCell(v, opts)).join(delimiter));
|
|
1716
|
+
const body = lines.join(CRLF) + CRLF;
|
|
1717
|
+
return opts.bom ? `${body}` : body;
|
|
1718
|
+
}
|
|
1719
|
+
//#endregion
|
|
1720
|
+
//#region src/export/export-paging.ts
|
|
1721
|
+
/** Default page size used by the export pager when the caller gives none. */
|
|
1722
|
+
const DEFAULT_EXPORT_PAGE_SIZE = 500;
|
|
1723
|
+
/** Rejection raised when an export is cancelled through its `AbortSignal`. */
|
|
1724
|
+
var ExportAbortError = class extends Error {
|
|
1725
|
+
/** Matches the DOM convention so `err.name === "AbortError"` checks work. */
|
|
1726
|
+
name = "AbortError";
|
|
1727
|
+
constructor(message = "Export aborted") {
|
|
1728
|
+
super(message);
|
|
1729
|
+
}
|
|
1730
|
+
};
|
|
1731
|
+
/**
|
|
1732
|
+
* Append the primary key(s) to a query's `$sort` as a tiebreaker so paging is
|
|
1733
|
+
* stable: two rows equal on every user sorter keep a deterministic relative
|
|
1734
|
+
* order, and no row is skipped or duplicated across page boundaries.
|
|
1735
|
+
*
|
|
1736
|
+
* Existing sort fields are preserved in order and never replaced — a pk the
|
|
1737
|
+
* user already sorts by is left where it is. Returns the input query when
|
|
1738
|
+
* there is nothing to add.
|
|
1739
|
+
*/
|
|
1740
|
+
function withStableOrder(query, primaryKeys) {
|
|
1741
|
+
if (primaryKeys.length === 0) return query;
|
|
1742
|
+
const current = query.controls?.$sort ?? {};
|
|
1743
|
+
const missing = primaryKeys.filter((pk) => !(pk in current));
|
|
1744
|
+
if (missing.length === 0) return query;
|
|
1745
|
+
const $sort = { ...current };
|
|
1746
|
+
for (const pk of missing) $sort[pk] = 1;
|
|
1747
|
+
return {
|
|
1748
|
+
...query,
|
|
1749
|
+
controls: {
|
|
1750
|
+
...query.controls,
|
|
1751
|
+
$sort
|
|
1752
|
+
}
|
|
1753
|
+
};
|
|
1754
|
+
}
|
|
1755
|
+
/**
|
|
1756
|
+
* Page through `query` until the endpoint runs out of rows, collecting every
|
|
1757
|
+
* row into one array.
|
|
1758
|
+
*
|
|
1759
|
+
* Stops when a page comes back short (fewer rows than `pageSize`), when the
|
|
1760
|
+
* reported `count` is reached, or when `maxRows` is hit. Checks `signal`
|
|
1761
|
+
* before and after every request so a cancel lands between pages rather than
|
|
1762
|
+
* mid-flight.
|
|
1763
|
+
*/
|
|
1764
|
+
async function collectExportRows(opts) {
|
|
1765
|
+
const pageSize = opts.pageSize && opts.pageSize > 0 ? opts.pageSize : 500;
|
|
1766
|
+
const mapRow = opts.mapRow ?? ((row) => row);
|
|
1767
|
+
const rows = [];
|
|
1768
|
+
let total;
|
|
1769
|
+
let page = 1;
|
|
1770
|
+
for (;;) {
|
|
1771
|
+
throwIfAborted(opts.signal);
|
|
1772
|
+
const result = await opts.fetchPage(opts.query, page, pageSize);
|
|
1773
|
+
throwIfAborted(opts.signal);
|
|
1774
|
+
if (page === 1 && typeof result.count === "number") total = result.count;
|
|
1775
|
+
const data = result.data ?? [];
|
|
1776
|
+
const take = opts.maxRows === void 0 ? data.length : Math.min(data.length, opts.maxRows - rows.length);
|
|
1777
|
+
for (let i = 0; i < take; i++) rows.push(mapRow(data[i]));
|
|
1778
|
+
opts.onProgress?.(rows.length, total);
|
|
1779
|
+
if (data.length < pageSize || opts.maxRows !== void 0 && rows.length >= opts.maxRows || total !== void 0 && rows.length >= total) break;
|
|
1780
|
+
page++;
|
|
1781
|
+
}
|
|
1782
|
+
return {
|
|
1783
|
+
rows,
|
|
1784
|
+
total
|
|
1785
|
+
};
|
|
1786
|
+
}
|
|
1787
|
+
function throwIfAborted(signal) {
|
|
1788
|
+
if (signal?.aborted) throw new ExportAbortError();
|
|
1789
|
+
}
|
|
1790
|
+
//#endregion
|
|
1791
|
+
//#region src/export/export-value.ts
|
|
1792
|
+
/**
|
|
1793
|
+
* Turn a raw cell value into the scalar an export carries, using the same
|
|
1794
|
+
* column metadata the cell renderers read:
|
|
1795
|
+
*
|
|
1796
|
+
* - `null` / `undefined` → `null` (an empty CSV field).
|
|
1797
|
+
* - union / enum columns (`column.options`) → the option's **label**, so the
|
|
1798
|
+
* file reads like the screen; an unknown key falls back to the raw value.
|
|
1799
|
+
* - `Date` → ISO 8601 (spreadsheet- and re-import-friendly, unlike a
|
|
1800
|
+
* locale-formatted date).
|
|
1801
|
+
* - arrays → comma-joined scalars; other objects → JSON.
|
|
1802
|
+
* - numbers / booleans pass through untouched so a spreadsheet keeps them
|
|
1803
|
+
* numeric.
|
|
1804
|
+
*
|
|
1805
|
+
* Callers override per column (`formatters`) or globally (`formatCell`) when
|
|
1806
|
+
* they want different text.
|
|
1807
|
+
*/
|
|
1808
|
+
function resolveExportValue(value, column) {
|
|
1809
|
+
if (value === null || value === void 0) return null;
|
|
1810
|
+
const options = column?.options;
|
|
1811
|
+
if (options?.length) {
|
|
1812
|
+
const key = typeof value === "string" || typeof value === "number" ? String(value) : void 0;
|
|
1813
|
+
if (key !== void 0) {
|
|
1814
|
+
const hit = options.find((o) => o.key === key);
|
|
1815
|
+
if (hit) return hit.label;
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
if (typeof value === "number" || typeof value === "boolean") return value;
|
|
1819
|
+
if (typeof value === "string") return value;
|
|
1820
|
+
if (value instanceof Date) return value.toISOString();
|
|
1821
|
+
if (Array.isArray(value)) return value.map((v) => scalarText(v)).join(", ");
|
|
1822
|
+
return scalarText(value);
|
|
1823
|
+
}
|
|
1824
|
+
function scalarText(value) {
|
|
1825
|
+
if (value === null || value === void 0 || typeof value === "function") return "";
|
|
1826
|
+
try {
|
|
1827
|
+
return (0, _atscript_ui.str)(value);
|
|
1828
|
+
} catch {
|
|
1829
|
+
return "";
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
//#endregion
|
|
1833
|
+
//#region src/columns/display-columns.ts
|
|
1834
|
+
/** Columns with no explicit `order` land after every server column. */
|
|
1835
|
+
const TRAILING_ORDER = Number.MAX_SAFE_INTEGER;
|
|
1836
|
+
/** Turn a {@link DisplayColumnDef} into the `ColumnDef` the table renders. */
|
|
1837
|
+
function displayColumnToDef(def) {
|
|
1838
|
+
return {
|
|
1839
|
+
path: def.key,
|
|
1840
|
+
label: def.label,
|
|
1841
|
+
type: def.type ?? "text",
|
|
1842
|
+
component: def.component,
|
|
1843
|
+
sortable: def.sortable === "local",
|
|
1844
|
+
filterable: false,
|
|
1845
|
+
nullable: true,
|
|
1846
|
+
order: def.order ?? TRAILING_ORDER,
|
|
1847
|
+
width: def.width,
|
|
1848
|
+
local: true
|
|
1849
|
+
};
|
|
1850
|
+
}
|
|
1851
|
+
/**
|
|
1852
|
+
* Merge client-owned columns into the server's column list, ordered by each
|
|
1853
|
+
* column's `order`. `Array#sort` is stable, so server columns that share an
|
|
1854
|
+
* `order` keep their `/meta` sequence. Returns `base` untouched when there is
|
|
1855
|
+
* nothing to merge.
|
|
1856
|
+
*/
|
|
1857
|
+
function mergeDisplayColumns(base, display) {
|
|
1858
|
+
if (display.length === 0) return base;
|
|
1859
|
+
return [...base, ...display.map(displayColumnToDef)].toSorted((a, b) => a.order - b.order);
|
|
1860
|
+
}
|
|
1861
|
+
//#endregion
|
|
1862
|
+
//#region src/utils/sort-rows.ts
|
|
1863
|
+
/**
|
|
1864
|
+
* Coerce a primitive cell value to the text used for comparing and matching
|
|
1865
|
+
* cells — the string compare in {@link sortRowsLocally} and the substring
|
|
1866
|
+
* search of an in-memory table. Objects and functions collapse to `""` so
|
|
1867
|
+
* `'[object Object]'` never drives an ordering or matches a search.
|
|
1868
|
+
*/
|
|
1869
|
+
function cellAsString(v) {
|
|
1870
|
+
if (v == null) return "";
|
|
1871
|
+
if (typeof v === "string") return v;
|
|
1872
|
+
if (typeof v === "number" || typeof v === "boolean") return v.toString();
|
|
1873
|
+
return "";
|
|
1874
|
+
}
|
|
1875
|
+
/**
|
|
1876
|
+
* Sort rows in memory by `sorters`, numerically when both sides are numbers
|
|
1877
|
+
* and locale-aware otherwise. Returns a NEW array; the input is untouched. A
|
|
1878
|
+
* no-op (returns the input reference) when there is nothing to sort.
|
|
1879
|
+
*
|
|
1880
|
+
* `getValue` reads a sorter's field off a row — pass a dot-path reader for
|
|
1881
|
+
* nested fields; the default is a flat property read.
|
|
1882
|
+
*/
|
|
1883
|
+
function sortRowsLocally(rows, sorters, getValue = (row, field) => row[field]) {
|
|
1884
|
+
if (sorters.length === 0 || rows.length < 2) return rows;
|
|
1885
|
+
return rows.toSorted((a, b) => {
|
|
1886
|
+
for (const s of sorters) {
|
|
1887
|
+
const dir = s.direction === "desc" ? -1 : 1;
|
|
1888
|
+
const av = getValue(a, s.field);
|
|
1889
|
+
const bv = getValue(b, s.field);
|
|
1890
|
+
if (typeof av === "number" && typeof bv === "number") {
|
|
1891
|
+
if (av < bv) return -dir;
|
|
1892
|
+
if (av > bv) return dir;
|
|
1893
|
+
} else {
|
|
1894
|
+
const cmp = cellAsString(av).localeCompare(cellAsString(bv));
|
|
1895
|
+
if (cmp !== 0) return cmp * dir;
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
return 0;
|
|
1899
|
+
});
|
|
1900
|
+
}
|
|
1901
|
+
//#endregion
|
|
1684
1902
|
exports.APP_CONF_PREFIX = APP_CONF_PREFIX;
|
|
1685
1903
|
exports.AppPrefsClient = AppPrefsClient;
|
|
1904
|
+
exports.DEFAULT_EXPORT_PAGE_SIZE = DEFAULT_EXPORT_PAGE_SIZE;
|
|
1686
1905
|
exports.DEFAULT_ROW_HEIGHT_PX = DEFAULT_ROW_HEIGHT_PX;
|
|
1687
1906
|
exports.DRAFT_PERSISTED_ASPECTS = DRAFT_PERSISTED_ASPECTS;
|
|
1907
|
+
exports.ExportAbortError = ExportAbortError;
|
|
1688
1908
|
exports.MAX_DEFAULT_COLUMN_WIDTH_PX = MAX_DEFAULT_COLUMN_WIDTH_PX;
|
|
1689
1909
|
exports.NULL_OPS = NULL_OPS;
|
|
1690
1910
|
exports.PRESET_ASPECTS = PRESET_ASPECTS;
|
|
@@ -1698,11 +1918,14 @@ exports.appConfId = appConfId;
|
|
|
1698
1918
|
exports.arraysEqual = arraysEqual;
|
|
1699
1919
|
exports.blockStartFor = blockStartFor;
|
|
1700
1920
|
exports.buildTableQuery = buildTableQuery;
|
|
1921
|
+
exports.cellAsString = cellAsString;
|
|
1701
1922
|
exports.clampTopIndex = clampTopIndex;
|
|
1923
|
+
exports.collectExportRows = collectExportRows;
|
|
1702
1924
|
exports.columnFilterType = columnFilterType;
|
|
1703
1925
|
exports.computeDefaultColumnWidth = computeDefaultColumnWidth;
|
|
1704
1926
|
exports.conditionLabel = conditionLabel;
|
|
1705
1927
|
exports.conditionsForType = conditionsForType;
|
|
1928
|
+
exports.csvCell = csvCell;
|
|
1706
1929
|
exports.dateShortcuts = dateShortcuts;
|
|
1707
1930
|
exports.debounce = debounce;
|
|
1708
1931
|
exports.defaultCondition = defaultCondition;
|
|
@@ -1723,6 +1946,7 @@ exports.isFilled = isFilled;
|
|
|
1723
1946
|
exports.isSimpleEq = isSimpleEq;
|
|
1724
1947
|
exports.isSystemPresetId = isSystemPresetId;
|
|
1725
1948
|
exports.isUnavailableError = isUnavailableError;
|
|
1949
|
+
exports.mergeDisplayColumns = mergeDisplayColumns;
|
|
1726
1950
|
exports.mergeFilters = mergeFilters;
|
|
1727
1951
|
exports.mergeSorters = mergeSorters;
|
|
1728
1952
|
exports.normaliseSystemPresetId = normaliseSystemPresetId;
|
|
@@ -1732,14 +1956,17 @@ exports.planFetch = planFetch;
|
|
|
1732
1956
|
exports.reconcileColumnWidthDefaults = reconcileColumnWidthDefaults;
|
|
1733
1957
|
exports.reorderColumnNames = reorderColumnNames;
|
|
1734
1958
|
exports.resolveAspectGate = resolveAspectGate;
|
|
1959
|
+
exports.resolveExportValue = resolveExportValue;
|
|
1735
1960
|
exports.resolveSystemPresets = resolveSystemPresets;
|
|
1736
1961
|
exports.rowsToPks = rowsToPks;
|
|
1737
1962
|
exports.sameColumnSet = sameColumnSet;
|
|
1738
1963
|
exports.serializeDraft = serializeDraft;
|
|
1739
1964
|
exports.setsEqual = setsEqual;
|
|
1965
|
+
exports.sortRowsLocally = sortRowsLocally;
|
|
1740
1966
|
exports.sortersEqual = sortersEqual;
|
|
1741
1967
|
exports.stableStringify = stableStringify;
|
|
1742
1968
|
exports.stateToUrlQueryString = stateToUrlQueryString;
|
|
1969
|
+
exports.toCsv = toCsv;
|
|
1743
1970
|
exports.toWireSnapshot = toWireSnapshot;
|
|
1744
1971
|
exports.togglePk = togglePk;
|
|
1745
1972
|
exports.trimSelection = trimSelection;
|
|
@@ -1750,3 +1977,4 @@ exports.urlQueryStringToState = urlQueryStringToState;
|
|
|
1750
1977
|
exports.userConfId = userConfId;
|
|
1751
1978
|
exports.walkBackwardAbsorb = walkBackwardAbsorb;
|
|
1752
1979
|
exports.walkForwardAbsorb = walkForwardAbsorb;
|
|
1980
|
+
exports.withStableOrder = withStableOrder;
|
package/dist/index.d.cts
CHANGED
|
@@ -1106,4 +1106,188 @@ type ColumnReorderPosition = "before" | "after";
|
|
|
1106
1106
|
*/
|
|
1107
1107
|
declare function reorderColumnNames(names: string[], fromPath: string, toPath: string, position: ColumnReorderPosition): string[];
|
|
1108
1108
|
//#endregion
|
|
1109
|
-
export
|
|
1109
|
+
//#region src/export/csv.d.ts
|
|
1110
|
+
/** A cell value after export-time resolution — always a spreadsheet scalar. */
|
|
1111
|
+
type ExportScalar = string | number | boolean | null;
|
|
1112
|
+
/** Options shared by {@link csvCell} and {@link toCsv}. */
|
|
1113
|
+
interface CsvOptions {
|
|
1114
|
+
/**
|
|
1115
|
+
* Prepend a UTF-8 byte-order mark. Excel needs it to detect UTF-8 in a
|
|
1116
|
+
* `.csv` opened by double-click; most other tools don't care. Default `false`.
|
|
1117
|
+
*/
|
|
1118
|
+
bom?: boolean;
|
|
1119
|
+
/**
|
|
1120
|
+
* Prefix a STRING cell starting with `=`, `+`, `-`, `@`, TAB or CR with a
|
|
1121
|
+
* single quote so a spreadsheet treats it as text instead of a formula
|
|
1122
|
+
* (CSV-injection defence). Numbers and booleans are never touched — a
|
|
1123
|
+
* negative number stays numeric. Default `true`; set `false` when the file
|
|
1124
|
+
* is consumed by a parser rather than a spreadsheet.
|
|
1125
|
+
*/
|
|
1126
|
+
escapeFormulas?: boolean;
|
|
1127
|
+
/** Field delimiter. Default `","`. */
|
|
1128
|
+
delimiter?: string;
|
|
1129
|
+
}
|
|
1130
|
+
/**
|
|
1131
|
+
* Render one value as an RFC 4180 field: formula-escaped (unless opted out),
|
|
1132
|
+
* then quoted when it contains the delimiter, a quote, CR or LF — with inner
|
|
1133
|
+
* quotes doubled.
|
|
1134
|
+
*/
|
|
1135
|
+
declare function csvCell(value: ExportScalar | undefined, opts?: CsvOptions): string;
|
|
1136
|
+
/**
|
|
1137
|
+
* Serialise a header row + data rows to an RFC 4180 CSV string (`\r\n` row
|
|
1138
|
+
* separators, quoted fields where required, optional UTF-8 BOM).
|
|
1139
|
+
*
|
|
1140
|
+
* Pure — no DOM, no framework. See {@link csvCell} for the per-field rules.
|
|
1141
|
+
*/
|
|
1142
|
+
declare function toCsv(header: readonly string[], rows: readonly (readonly ExportScalar[])[], opts?: CsvOptions): string;
|
|
1143
|
+
//#endregion
|
|
1144
|
+
//#region src/export/export-paging.d.ts
|
|
1145
|
+
/** Default page size used by the export pager when the caller gives none. */
|
|
1146
|
+
declare const DEFAULT_EXPORT_PAGE_SIZE = 500;
|
|
1147
|
+
/** One page of an export run — the subset of `PageResult` the pager needs. */
|
|
1148
|
+
interface ExportPage {
|
|
1149
|
+
data: Record<string, unknown>[];
|
|
1150
|
+
/** Total row count, when the endpoint reports one. */
|
|
1151
|
+
count?: number;
|
|
1152
|
+
}
|
|
1153
|
+
/** Fetches one page of the export query. Mirrors `client.pages(query, page, size)`. */
|
|
1154
|
+
type ExportPageFetcher = (query: Uniquery, page: number, size: number) => Promise<ExportPage>;
|
|
1155
|
+
/** Rejection raised when an export is cancelled through its `AbortSignal`. */
|
|
1156
|
+
declare class ExportAbortError extends Error {
|
|
1157
|
+
/** Matches the DOM convention so `err.name === "AbortError"` checks work. */
|
|
1158
|
+
readonly name = "AbortError";
|
|
1159
|
+
constructor(message?: string);
|
|
1160
|
+
}
|
|
1161
|
+
interface CollectExportRowsOptions<T = Record<string, unknown>> {
|
|
1162
|
+
/** The query to page through. Pass it through {@link withStableOrder} first. */
|
|
1163
|
+
query: Uniquery;
|
|
1164
|
+
/** Rows per request. Default {@link DEFAULT_EXPORT_PAGE_SIZE}. */
|
|
1165
|
+
pageSize?: number;
|
|
1166
|
+
fetchPage: ExportPageFetcher;
|
|
1167
|
+
/** Aborts between pages; the returned promise rejects with {@link ExportAbortError}. */
|
|
1168
|
+
signal?: AbortSignal;
|
|
1169
|
+
/** Called after every settled page. `total` comes from the first page's `count`. */
|
|
1170
|
+
onProgress?: (done: number, total?: number) => void;
|
|
1171
|
+
/** Hard ceiling on collected rows — the pager stops once it is reached. */
|
|
1172
|
+
maxRows?: number;
|
|
1173
|
+
/**
|
|
1174
|
+
* Project each row as it arrives — the collected array holds the projection,
|
|
1175
|
+
* not the raw row, so a formatted export never keeps both in memory.
|
|
1176
|
+
* Identity by default.
|
|
1177
|
+
*/
|
|
1178
|
+
mapRow?: (row: Record<string, unknown>) => T;
|
|
1179
|
+
}
|
|
1180
|
+
/**
|
|
1181
|
+
* Append the primary key(s) to a query's `$sort` as a tiebreaker so paging is
|
|
1182
|
+
* stable: two rows equal on every user sorter keep a deterministic relative
|
|
1183
|
+
* order, and no row is skipped or duplicated across page boundaries.
|
|
1184
|
+
*
|
|
1185
|
+
* Existing sort fields are preserved in order and never replaced — a pk the
|
|
1186
|
+
* user already sorts by is left where it is. Returns the input query when
|
|
1187
|
+
* there is nothing to add.
|
|
1188
|
+
*/
|
|
1189
|
+
declare function withStableOrder(query: Uniquery, primaryKeys: readonly string[]): Uniquery;
|
|
1190
|
+
/**
|
|
1191
|
+
* Page through `query` until the endpoint runs out of rows, collecting every
|
|
1192
|
+
* row into one array.
|
|
1193
|
+
*
|
|
1194
|
+
* Stops when a page comes back short (fewer rows than `pageSize`), when the
|
|
1195
|
+
* reported `count` is reached, or when `maxRows` is hit. Checks `signal`
|
|
1196
|
+
* before and after every request so a cancel lands between pages rather than
|
|
1197
|
+
* mid-flight.
|
|
1198
|
+
*/
|
|
1199
|
+
declare function collectExportRows<T = Record<string, unknown>>(opts: CollectExportRowsOptions<T>): Promise<{
|
|
1200
|
+
rows: T[];
|
|
1201
|
+
total?: number;
|
|
1202
|
+
}>;
|
|
1203
|
+
//#endregion
|
|
1204
|
+
//#region src/export/export-value.d.ts
|
|
1205
|
+
/**
|
|
1206
|
+
* Turn a raw cell value into the scalar an export carries, using the same
|
|
1207
|
+
* column metadata the cell renderers read:
|
|
1208
|
+
*
|
|
1209
|
+
* - `null` / `undefined` → `null` (an empty CSV field).
|
|
1210
|
+
* - union / enum columns (`column.options`) → the option's **label**, so the
|
|
1211
|
+
* file reads like the screen; an unknown key falls back to the raw value.
|
|
1212
|
+
* - `Date` → ISO 8601 (spreadsheet- and re-import-friendly, unlike a
|
|
1213
|
+
* locale-formatted date).
|
|
1214
|
+
* - arrays → comma-joined scalars; other objects → JSON.
|
|
1215
|
+
* - numbers / booleans pass through untouched so a spreadsheet keeps them
|
|
1216
|
+
* numeric.
|
|
1217
|
+
*
|
|
1218
|
+
* Callers override per column (`formatters`) or globally (`formatCell`) when
|
|
1219
|
+
* they want different text.
|
|
1220
|
+
*/
|
|
1221
|
+
declare function resolveExportValue(value: unknown, column?: ColumnDef): ExportScalar;
|
|
1222
|
+
//#endregion
|
|
1223
|
+
//#region src/columns/display-columns.d.ts
|
|
1224
|
+
/**
|
|
1225
|
+
* A client-owned column the app adds on top of the server's `/meta` columns —
|
|
1226
|
+
* a derived value, a count, a link, a widget. It has a label, a width and a
|
|
1227
|
+
* renderer, takes part in column visibility / reordering / presets like any
|
|
1228
|
+
* other column, but never reaches the backend: no `$select`, no server sort,
|
|
1229
|
+
* no filters.
|
|
1230
|
+
*
|
|
1231
|
+
* Since 0.1.134.
|
|
1232
|
+
*/
|
|
1233
|
+
interface DisplayColumnDef {
|
|
1234
|
+
/**
|
|
1235
|
+
* Stable key. Becomes the column's `path`, so it is what `cell-<key>` /
|
|
1236
|
+
* `header-<key>` slots, the `columnNames` model and presets refer to. Must
|
|
1237
|
+
* not collide with a real field path.
|
|
1238
|
+
*/
|
|
1239
|
+
key: string;
|
|
1240
|
+
/** Header label. Unlike `fixed` chrome columns, a display column is labelled. */
|
|
1241
|
+
label: string;
|
|
1242
|
+
/** Default width (any CSS length), e.g. `"8em"`. */
|
|
1243
|
+
width?: string;
|
|
1244
|
+
/** Initial position among the server columns (lower = earlier). Appended when omitted. */
|
|
1245
|
+
order?: number;
|
|
1246
|
+
/**
|
|
1247
|
+
* `'local'` makes the header offer sorting, applied client-side over the
|
|
1248
|
+
* rows currently loaded (page-local). Anything else — including the default
|
|
1249
|
+
* — leaves the column unsortable, since the server cannot sort it.
|
|
1250
|
+
*
|
|
1251
|
+
* Paged tables only: `<AsWindowTable>` caches rows by absolute index, so
|
|
1252
|
+
* there is no page to re-order and the affordance is not offered there.
|
|
1253
|
+
*/
|
|
1254
|
+
sortable?: false | "local";
|
|
1255
|
+
/**
|
|
1256
|
+
* The value `sortable: 'local'` orders by. A display column has no server
|
|
1257
|
+
* field behind it, so without this the sorter reads `row[key]` — which is
|
|
1258
|
+
* `undefined` unless the app happens to carry that key on the row. Ignored
|
|
1259
|
+
* when the column is not sorted locally.
|
|
1260
|
+
*/
|
|
1261
|
+
sortValue?: (row: Record<string, unknown>) => unknown;
|
|
1262
|
+
/** Named cell component (`components[name]` on the table context). */
|
|
1263
|
+
component?: string;
|
|
1264
|
+
/** Cell type for the `types[type]` dispatch. Default `"text"`. */
|
|
1265
|
+
type?: string;
|
|
1266
|
+
}
|
|
1267
|
+
/**
|
|
1268
|
+
* Merge client-owned columns into the server's column list, ordered by each
|
|
1269
|
+
* column's `order`. `Array#sort` is stable, so server columns that share an
|
|
1270
|
+
* `order` keep their `/meta` sequence. Returns `base` untouched when there is
|
|
1271
|
+
* nothing to merge.
|
|
1272
|
+
*/
|
|
1273
|
+
declare function mergeDisplayColumns(base: ColumnDef[], display: readonly DisplayColumnDef[]): ColumnDef[];
|
|
1274
|
+
//#endregion
|
|
1275
|
+
//#region src/utils/sort-rows.d.ts
|
|
1276
|
+
/**
|
|
1277
|
+
* Coerce a primitive cell value to the text used for comparing and matching
|
|
1278
|
+
* cells — the string compare in {@link sortRowsLocally} and the substring
|
|
1279
|
+
* search of an in-memory table. Objects and functions collapse to `""` so
|
|
1280
|
+
* `'[object Object]'` never drives an ordering or matches a search.
|
|
1281
|
+
*/
|
|
1282
|
+
declare function cellAsString(v: unknown): string;
|
|
1283
|
+
/**
|
|
1284
|
+
* Sort rows in memory by `sorters`, numerically when both sides are numbers
|
|
1285
|
+
* and locale-aware otherwise. Returns a NEW array; the input is untouched. A
|
|
1286
|
+
* no-op (returns the input reference) when there is nothing to sort.
|
|
1287
|
+
*
|
|
1288
|
+
* `getValue` reads a sorter's field off a row — pass a dot-path reader for
|
|
1289
|
+
* nested fields; the default is a flat property read.
|
|
1290
|
+
*/
|
|
1291
|
+
declare function sortRowsLocally<T extends Record<string, unknown>>(rows: T[], sorters: readonly SortControl[], getValue?: (row: T, field: string) => unknown): T[];
|
|
1292
|
+
//#endregion
|
|
1293
|
+
export { APP_CONF_PREFIX, type AppConfData, AppPrefsClient, type AppPrefsClientConfig, type AppPrefsLoadResult, type AsPresetEntryData, type AsPresetEntryRow, type AsPresetsErrorCode, type AspectGate, type AspectMask, type BuildTableQueryOptions, type ColumnFilterType, type ColumnReorderPosition, type ColumnWidthEntry, type ColumnWidthsMap, type ConfigTab, type CsvOptions, DEFAULT_EXPORT_PAGE_SIZE, DEFAULT_ROW_HEIGHT_PX, DRAFT_PERSISTED_ASPECTS, type DateShortcut, type DisplayColumnDef, type DraftPersistedAspect, ExportAbortError, type ExportPage, type ExportPageFetcher, type ExportScalar, type FetchPlan, type FetchPlanMode, type FieldFilters, type FilterCondition, type FilterConditionType, MAX_DEFAULT_COLUMN_WIDTH_PX, type MergeResult, NULL_OPS, PRESET_ASPECTS, type PageAlignedBlock, type PlanFetchArgs, type PresetAspect, type PresetCapabilities, type PresetColumnWidthEntry, type PresetData, type PresetDraft, type PresetFilterOpEntry, type PresetLimitReachedBody, type PresetSnapshot, type PresetSnapshotWire, type PresetSorterEntry, PresetsClient, type PresetsClientConfig, PresetsHttpError, type PresetsListResult, type PresetsSaveAsOptions, type PresetsSaveResult, type QueryOptions, RESERVED_ID_PREFIXES, STANDARD_PRESET_ID, SYSTEM_PRESET_PREFIX, type SelectionMode, type SystemPreset, type SystemPresetInput, type TableStateData, type TableStateMethods, USER_CONF_PREFIX, type UrlQueryDefaults, type UrlQueryParseOptions, type UrlQueryStateLike, type UrlQueryStateSnapshot, type UrlQuerySync, type UserConfData, 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/dist/index.d.mts
CHANGED
|
@@ -1106,4 +1106,188 @@ type ColumnReorderPosition = "before" | "after";
|
|
|
1106
1106
|
*/
|
|
1107
1107
|
declare function reorderColumnNames(names: string[], fromPath: string, toPath: string, position: ColumnReorderPosition): string[];
|
|
1108
1108
|
//#endregion
|
|
1109
|
-
export
|
|
1109
|
+
//#region src/export/csv.d.ts
|
|
1110
|
+
/** A cell value after export-time resolution — always a spreadsheet scalar. */
|
|
1111
|
+
type ExportScalar = string | number | boolean | null;
|
|
1112
|
+
/** Options shared by {@link csvCell} and {@link toCsv}. */
|
|
1113
|
+
interface CsvOptions {
|
|
1114
|
+
/**
|
|
1115
|
+
* Prepend a UTF-8 byte-order mark. Excel needs it to detect UTF-8 in a
|
|
1116
|
+
* `.csv` opened by double-click; most other tools don't care. Default `false`.
|
|
1117
|
+
*/
|
|
1118
|
+
bom?: boolean;
|
|
1119
|
+
/**
|
|
1120
|
+
* Prefix a STRING cell starting with `=`, `+`, `-`, `@`, TAB or CR with a
|
|
1121
|
+
* single quote so a spreadsheet treats it as text instead of a formula
|
|
1122
|
+
* (CSV-injection defence). Numbers and booleans are never touched — a
|
|
1123
|
+
* negative number stays numeric. Default `true`; set `false` when the file
|
|
1124
|
+
* is consumed by a parser rather than a spreadsheet.
|
|
1125
|
+
*/
|
|
1126
|
+
escapeFormulas?: boolean;
|
|
1127
|
+
/** Field delimiter. Default `","`. */
|
|
1128
|
+
delimiter?: string;
|
|
1129
|
+
}
|
|
1130
|
+
/**
|
|
1131
|
+
* Render one value as an RFC 4180 field: formula-escaped (unless opted out),
|
|
1132
|
+
* then quoted when it contains the delimiter, a quote, CR or LF — with inner
|
|
1133
|
+
* quotes doubled.
|
|
1134
|
+
*/
|
|
1135
|
+
declare function csvCell(value: ExportScalar | undefined, opts?: CsvOptions): string;
|
|
1136
|
+
/**
|
|
1137
|
+
* Serialise a header row + data rows to an RFC 4180 CSV string (`\r\n` row
|
|
1138
|
+
* separators, quoted fields where required, optional UTF-8 BOM).
|
|
1139
|
+
*
|
|
1140
|
+
* Pure — no DOM, no framework. See {@link csvCell} for the per-field rules.
|
|
1141
|
+
*/
|
|
1142
|
+
declare function toCsv(header: readonly string[], rows: readonly (readonly ExportScalar[])[], opts?: CsvOptions): string;
|
|
1143
|
+
//#endregion
|
|
1144
|
+
//#region src/export/export-paging.d.ts
|
|
1145
|
+
/** Default page size used by the export pager when the caller gives none. */
|
|
1146
|
+
declare const DEFAULT_EXPORT_PAGE_SIZE = 500;
|
|
1147
|
+
/** One page of an export run — the subset of `PageResult` the pager needs. */
|
|
1148
|
+
interface ExportPage {
|
|
1149
|
+
data: Record<string, unknown>[];
|
|
1150
|
+
/** Total row count, when the endpoint reports one. */
|
|
1151
|
+
count?: number;
|
|
1152
|
+
}
|
|
1153
|
+
/** Fetches one page of the export query. Mirrors `client.pages(query, page, size)`. */
|
|
1154
|
+
type ExportPageFetcher = (query: Uniquery, page: number, size: number) => Promise<ExportPage>;
|
|
1155
|
+
/** Rejection raised when an export is cancelled through its `AbortSignal`. */
|
|
1156
|
+
declare class ExportAbortError extends Error {
|
|
1157
|
+
/** Matches the DOM convention so `err.name === "AbortError"` checks work. */
|
|
1158
|
+
readonly name = "AbortError";
|
|
1159
|
+
constructor(message?: string);
|
|
1160
|
+
}
|
|
1161
|
+
interface CollectExportRowsOptions<T = Record<string, unknown>> {
|
|
1162
|
+
/** The query to page through. Pass it through {@link withStableOrder} first. */
|
|
1163
|
+
query: Uniquery;
|
|
1164
|
+
/** Rows per request. Default {@link DEFAULT_EXPORT_PAGE_SIZE}. */
|
|
1165
|
+
pageSize?: number;
|
|
1166
|
+
fetchPage: ExportPageFetcher;
|
|
1167
|
+
/** Aborts between pages; the returned promise rejects with {@link ExportAbortError}. */
|
|
1168
|
+
signal?: AbortSignal;
|
|
1169
|
+
/** Called after every settled page. `total` comes from the first page's `count`. */
|
|
1170
|
+
onProgress?: (done: number, total?: number) => void;
|
|
1171
|
+
/** Hard ceiling on collected rows — the pager stops once it is reached. */
|
|
1172
|
+
maxRows?: number;
|
|
1173
|
+
/**
|
|
1174
|
+
* Project each row as it arrives — the collected array holds the projection,
|
|
1175
|
+
* not the raw row, so a formatted export never keeps both in memory.
|
|
1176
|
+
* Identity by default.
|
|
1177
|
+
*/
|
|
1178
|
+
mapRow?: (row: Record<string, unknown>) => T;
|
|
1179
|
+
}
|
|
1180
|
+
/**
|
|
1181
|
+
* Append the primary key(s) to a query's `$sort` as a tiebreaker so paging is
|
|
1182
|
+
* stable: two rows equal on every user sorter keep a deterministic relative
|
|
1183
|
+
* order, and no row is skipped or duplicated across page boundaries.
|
|
1184
|
+
*
|
|
1185
|
+
* Existing sort fields are preserved in order and never replaced — a pk the
|
|
1186
|
+
* user already sorts by is left where it is. Returns the input query when
|
|
1187
|
+
* there is nothing to add.
|
|
1188
|
+
*/
|
|
1189
|
+
declare function withStableOrder(query: Uniquery, primaryKeys: readonly string[]): Uniquery;
|
|
1190
|
+
/**
|
|
1191
|
+
* Page through `query` until the endpoint runs out of rows, collecting every
|
|
1192
|
+
* row into one array.
|
|
1193
|
+
*
|
|
1194
|
+
* Stops when a page comes back short (fewer rows than `pageSize`), when the
|
|
1195
|
+
* reported `count` is reached, or when `maxRows` is hit. Checks `signal`
|
|
1196
|
+
* before and after every request so a cancel lands between pages rather than
|
|
1197
|
+
* mid-flight.
|
|
1198
|
+
*/
|
|
1199
|
+
declare function collectExportRows<T = Record<string, unknown>>(opts: CollectExportRowsOptions<T>): Promise<{
|
|
1200
|
+
rows: T[];
|
|
1201
|
+
total?: number;
|
|
1202
|
+
}>;
|
|
1203
|
+
//#endregion
|
|
1204
|
+
//#region src/export/export-value.d.ts
|
|
1205
|
+
/**
|
|
1206
|
+
* Turn a raw cell value into the scalar an export carries, using the same
|
|
1207
|
+
* column metadata the cell renderers read:
|
|
1208
|
+
*
|
|
1209
|
+
* - `null` / `undefined` → `null` (an empty CSV field).
|
|
1210
|
+
* - union / enum columns (`column.options`) → the option's **label**, so the
|
|
1211
|
+
* file reads like the screen; an unknown key falls back to the raw value.
|
|
1212
|
+
* - `Date` → ISO 8601 (spreadsheet- and re-import-friendly, unlike a
|
|
1213
|
+
* locale-formatted date).
|
|
1214
|
+
* - arrays → comma-joined scalars; other objects → JSON.
|
|
1215
|
+
* - numbers / booleans pass through untouched so a spreadsheet keeps them
|
|
1216
|
+
* numeric.
|
|
1217
|
+
*
|
|
1218
|
+
* Callers override per column (`formatters`) or globally (`formatCell`) when
|
|
1219
|
+
* they want different text.
|
|
1220
|
+
*/
|
|
1221
|
+
declare function resolveExportValue(value: unknown, column?: ColumnDef): ExportScalar;
|
|
1222
|
+
//#endregion
|
|
1223
|
+
//#region src/columns/display-columns.d.ts
|
|
1224
|
+
/**
|
|
1225
|
+
* A client-owned column the app adds on top of the server's `/meta` columns —
|
|
1226
|
+
* a derived value, a count, a link, a widget. It has a label, a width and a
|
|
1227
|
+
* renderer, takes part in column visibility / reordering / presets like any
|
|
1228
|
+
* other column, but never reaches the backend: no `$select`, no server sort,
|
|
1229
|
+
* no filters.
|
|
1230
|
+
*
|
|
1231
|
+
* Since 0.1.134.
|
|
1232
|
+
*/
|
|
1233
|
+
interface DisplayColumnDef {
|
|
1234
|
+
/**
|
|
1235
|
+
* Stable key. Becomes the column's `path`, so it is what `cell-<key>` /
|
|
1236
|
+
* `header-<key>` slots, the `columnNames` model and presets refer to. Must
|
|
1237
|
+
* not collide with a real field path.
|
|
1238
|
+
*/
|
|
1239
|
+
key: string;
|
|
1240
|
+
/** Header label. Unlike `fixed` chrome columns, a display column is labelled. */
|
|
1241
|
+
label: string;
|
|
1242
|
+
/** Default width (any CSS length), e.g. `"8em"`. */
|
|
1243
|
+
width?: string;
|
|
1244
|
+
/** Initial position among the server columns (lower = earlier). Appended when omitted. */
|
|
1245
|
+
order?: number;
|
|
1246
|
+
/**
|
|
1247
|
+
* `'local'` makes the header offer sorting, applied client-side over the
|
|
1248
|
+
* rows currently loaded (page-local). Anything else — including the default
|
|
1249
|
+
* — leaves the column unsortable, since the server cannot sort it.
|
|
1250
|
+
*
|
|
1251
|
+
* Paged tables only: `<AsWindowTable>` caches rows by absolute index, so
|
|
1252
|
+
* there is no page to re-order and the affordance is not offered there.
|
|
1253
|
+
*/
|
|
1254
|
+
sortable?: false | "local";
|
|
1255
|
+
/**
|
|
1256
|
+
* The value `sortable: 'local'` orders by. A display column has no server
|
|
1257
|
+
* field behind it, so without this the sorter reads `row[key]` — which is
|
|
1258
|
+
* `undefined` unless the app happens to carry that key on the row. Ignored
|
|
1259
|
+
* when the column is not sorted locally.
|
|
1260
|
+
*/
|
|
1261
|
+
sortValue?: (row: Record<string, unknown>) => unknown;
|
|
1262
|
+
/** Named cell component (`components[name]` on the table context). */
|
|
1263
|
+
component?: string;
|
|
1264
|
+
/** Cell type for the `types[type]` dispatch. Default `"text"`. */
|
|
1265
|
+
type?: string;
|
|
1266
|
+
}
|
|
1267
|
+
/**
|
|
1268
|
+
* Merge client-owned columns into the server's column list, ordered by each
|
|
1269
|
+
* column's `order`. `Array#sort` is stable, so server columns that share an
|
|
1270
|
+
* `order` keep their `/meta` sequence. Returns `base` untouched when there is
|
|
1271
|
+
* nothing to merge.
|
|
1272
|
+
*/
|
|
1273
|
+
declare function mergeDisplayColumns(base: ColumnDef[], display: readonly DisplayColumnDef[]): ColumnDef[];
|
|
1274
|
+
//#endregion
|
|
1275
|
+
//#region src/utils/sort-rows.d.ts
|
|
1276
|
+
/**
|
|
1277
|
+
* Coerce a primitive cell value to the text used for comparing and matching
|
|
1278
|
+
* cells — the string compare in {@link sortRowsLocally} and the substring
|
|
1279
|
+
* search of an in-memory table. Objects and functions collapse to `""` so
|
|
1280
|
+
* `'[object Object]'` never drives an ordering or matches a search.
|
|
1281
|
+
*/
|
|
1282
|
+
declare function cellAsString(v: unknown): string;
|
|
1283
|
+
/**
|
|
1284
|
+
* Sort rows in memory by `sorters`, numerically when both sides are numbers
|
|
1285
|
+
* and locale-aware otherwise. Returns a NEW array; the input is untouched. A
|
|
1286
|
+
* no-op (returns the input reference) when there is nothing to sort.
|
|
1287
|
+
*
|
|
1288
|
+
* `getValue` reads a sorter's field off a row — pass a dot-path reader for
|
|
1289
|
+
* nested fields; the default is a flat property read.
|
|
1290
|
+
*/
|
|
1291
|
+
declare function sortRowsLocally<T extends Record<string, unknown>>(rows: T[], sorters: readonly SortControl[], getValue?: (row: T, field: string) => unknown): T[];
|
|
1292
|
+
//#endregion
|
|
1293
|
+
export { APP_CONF_PREFIX, type AppConfData, AppPrefsClient, type AppPrefsClientConfig, type AppPrefsLoadResult, type AsPresetEntryData, type AsPresetEntryRow, type AsPresetsErrorCode, type AspectGate, type AspectMask, type BuildTableQueryOptions, type ColumnFilterType, type ColumnReorderPosition, type ColumnWidthEntry, type ColumnWidthsMap, type ConfigTab, type CsvOptions, DEFAULT_EXPORT_PAGE_SIZE, DEFAULT_ROW_HEIGHT_PX, DRAFT_PERSISTED_ASPECTS, type DateShortcut, type DisplayColumnDef, type DraftPersistedAspect, ExportAbortError, type ExportPage, type ExportPageFetcher, type ExportScalar, type FetchPlan, type FetchPlanMode, type FieldFilters, type FilterCondition, type FilterConditionType, MAX_DEFAULT_COLUMN_WIDTH_PX, type MergeResult, NULL_OPS, PRESET_ASPECTS, type PageAlignedBlock, type PlanFetchArgs, type PresetAspect, type PresetCapabilities, type PresetColumnWidthEntry, type PresetData, type PresetDraft, type PresetFilterOpEntry, type PresetLimitReachedBody, type PresetSnapshot, type PresetSnapshotWire, type PresetSorterEntry, PresetsClient, type PresetsClientConfig, PresetsHttpError, type PresetsListResult, type PresetsSaveAsOptions, type PresetsSaveResult, type QueryOptions, RESERVED_ID_PREFIXES, STANDARD_PRESET_ID, SYSTEM_PRESET_PREFIX, type SelectionMode, type SystemPreset, type SystemPresetInput, type TableStateData, type TableStateMethods, USER_CONF_PREFIX, type UrlQueryDefaults, type UrlQueryParseOptions, type UrlQueryStateLike, type UrlQueryStateSnapshot, type UrlQuerySync, type UserConfData, 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/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
|
|
@@ -1680,4 +1680,222 @@ function reorderColumnNames(names, fromPath, toPath, position) {
|
|
|
1680
1680
|
return without.slice(0, insertAt).concat(fromPath, without.slice(insertAt));
|
|
1681
1681
|
}
|
|
1682
1682
|
//#endregion
|
|
1683
|
-
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.135",
|
|
4
4
|
"description": "Framework-agnostic filter model, filter-to-Uniquery conversion, and preset serialization for atscript tables",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"atscript",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"access": "public"
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@atscript/ui": "0.1.
|
|
45
|
+
"@atscript/ui": "0.1.135"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@atscript/db-client": "^0.1.129",
|