@ai-matrx/design-system 0.21.8 → 0.21.10
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/CHANGELOG.md +12 -0
- package/dist/data-table/copy-helpers.cjs.map +1 -1
- package/dist/data-table/copy-helpers.d.cts +1 -1
- package/dist/data-table/copy-helpers.d.ts +1 -1
- package/dist/data-table/copy-helpers.js.map +1 -1
- package/dist/data-table/data-row-inspector.cjs.map +1 -1
- package/dist/data-table/data-row-inspector.js.map +1 -1
- package/dist/data-table/data-row-window.cjs.map +1 -1
- package/dist/data-table/data-row-window.js.map +1 -1
- package/dist/data-table/filter-engine.cjs +8 -3
- package/dist/data-table/filter-engine.cjs.map +1 -1
- package/dist/data-table/filter-engine.d.cts +4 -2
- package/dist/data-table/filter-engine.d.ts +4 -2
- package/dist/data-table/filter-engine.js +8 -3
- package/dist/data-table/filter-engine.js.map +1 -1
- package/dist/data-table/host.d.cts +1 -1
- package/dist/data-table/host.d.ts +1 -1
- package/dist/data-table/index.cjs +107 -32
- package/dist/data-table/index.cjs.map +1 -1
- package/dist/data-table/index.d.cts +2 -2
- package/dist/data-table/index.d.ts +2 -2
- package/dist/data-table/index.js +107 -32
- package/dist/data-table/index.js.map +1 -1
- package/dist/data-table/infer-filter.cjs +16 -6
- package/dist/data-table/infer-filter.cjs.map +1 -1
- package/dist/data-table/infer-filter.d.cts +1 -1
- package/dist/data-table/infer-filter.d.ts +1 -1
- package/dist/data-table/infer-filter.js +16 -6
- package/dist/data-table/infer-filter.js.map +1 -1
- package/dist/data-table/layered-filters.d.cts +1 -1
- package/dist/data-table/layered-filters.d.ts +1 -1
- package/dist/data-table/pagination.cjs +28 -14
- package/dist/data-table/pagination.cjs.map +1 -1
- package/dist/data-table/pagination.d.cts +7 -1
- package/dist/data-table/pagination.d.ts +7 -1
- package/dist/data-table/pagination.js +28 -14
- package/dist/data-table/pagination.js.map +1 -1
- package/dist/data-table/query-control.cjs +16 -6
- package/dist/data-table/query-control.cjs.map +1 -1
- package/dist/data-table/query-control.d.cts +1 -1
- package/dist/data-table/query-control.d.ts +1 -1
- package/dist/data-table/query-control.js +16 -6
- package/dist/data-table/query-control.js.map +1 -1
- package/dist/data-table/types.cjs.map +1 -1
- package/dist/data-table/types.d.cts +1 -1
- package/dist/data-table/types.d.ts +1 -1
- package/dist/data-table/url-state.d.cts +1 -1
- package/dist/data-table/url-state.d.ts +1 -1
- package/dist/data-table/xlsx.cjs.map +1 -1
- package/dist/data-table/xlsx.d.cts +1 -1
- package/dist/data-table/xlsx.d.ts +1 -1
- package/dist/data-table/xlsx.js.map +1 -1
- package/dist/{types-DsmrqJHN.d.cts → types-BAJiOaQL.d.cts} +8 -1
- package/dist/{types-Dpl8OW_m.d.ts → types-Da781-Wq.d.ts} +8 -1
- package/package.json +1 -1
|
@@ -44,6 +44,7 @@ function resolveColumnMarker(column) {
|
|
|
44
44
|
// src/data-table/filter-engine.ts
|
|
45
45
|
var SELECT_EMPTY_VALUE = "__empty__";
|
|
46
46
|
var SELECT_NOT_EMPTY_VALUE = "__not_empty__";
|
|
47
|
+
var SELECT_NULL_VALUE = "__null__";
|
|
47
48
|
function getCellValue(row, col) {
|
|
48
49
|
if (col.accessorFn) return col.accessorFn(row);
|
|
49
50
|
if (col.accessorKey) {
|
|
@@ -91,10 +92,13 @@ function collectSelectOptions(col, rows) {
|
|
|
91
92
|
const seen = /* @__PURE__ */ new Set();
|
|
92
93
|
const options = [];
|
|
93
94
|
let hasEmpty = false;
|
|
95
|
+
let hasEmptyCollection = false;
|
|
94
96
|
for (const row of rows) {
|
|
95
|
-
const
|
|
97
|
+
const cell = getCellValue(row, col);
|
|
98
|
+
const values = selectCellValues(cell);
|
|
96
99
|
if (values.length === 0) {
|
|
97
100
|
hasEmpty = true;
|
|
101
|
+
if (Array.isArray(cell)) hasEmptyCollection = true;
|
|
98
102
|
continue;
|
|
99
103
|
}
|
|
100
104
|
for (const value of values) {
|
|
@@ -103,12 +107,18 @@ function collectSelectOptions(col, rows) {
|
|
|
103
107
|
options.push({ value, label: value });
|
|
104
108
|
}
|
|
105
109
|
}
|
|
106
|
-
const base = col.filterOptions?.length ?
|
|
110
|
+
const base = col.filterOptions?.length ? col.filterOptions.map((option) => ({
|
|
111
|
+
...option,
|
|
112
|
+
label: option.label.trim() ? option.label : option.value === "" ? "(empty string)" : option.value.trim() ? option.value : "(whitespace)"
|
|
113
|
+
})) : options.sort((a, b) => a.label.localeCompare(b.label));
|
|
107
114
|
if (hasEmpty) {
|
|
108
|
-
base.
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
)
|
|
115
|
+
const emptyOption = !hasEmptyCollection && base.some((option) => option.value === "") ? { value: SELECT_NULL_VALUE, label: "(null)" } : { value: SELECT_EMPTY_VALUE, label: "(empty)" };
|
|
116
|
+
const additions = [];
|
|
117
|
+
if (!base.some((option) => option.value === emptyOption.value)) additions.push(emptyOption);
|
|
118
|
+
if (!base.some((option) => option.value === SELECT_NOT_EMPTY_VALUE)) {
|
|
119
|
+
additions.push({ value: SELECT_NOT_EMPTY_VALUE, label: "(not empty)" });
|
|
120
|
+
}
|
|
121
|
+
base.unshift(...additions);
|
|
112
122
|
}
|
|
113
123
|
return base;
|
|
114
124
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/data-table/infer-filter.ts","../../src/data-table/column-marker.ts","../../src/data-table/filter-engine.ts"],"sourcesContent":["import type { ColumnFilterKind, MatrxColumnDef } from \"./types\";\nimport { resolveColumnMarker } from \"./column-marker\";\nimport {\n SELECT_EMPTY_VALUE,\n SELECT_NOT_EMPTY_VALUE,\n getCellValue,\n selectCellValues,\n} from \"./filter-engine\";\n\n/** Auto-inference switches to text above this many distinct values… */\nconst SELECT_CARDINALITY_CAP = 24;\n\nexport type ResolvedFilterKind = \"text\" | \"select\" | \"boolean\" | \"number\";\n\n/**\n * Infer a filter UI from sample row values. Prefer select for low-cardinality\n * string/enum columns, boolean for booleans, number for numeric, else text.\n */\nexport function resolveFilterKind<T>(\n col: MatrxColumnDef<T>,\n sampleRows: T[],\n): ResolvedFilterKind | null {\n if (col.filter === false) return null;\n if (col.filter && col.filter !== \"auto\") return col.filter;\n if (resolveColumnMarker(col)) return \"boolean\";\n\n const values = sampleRows\n .slice(0, 200)\n .map((row) => getCellValue(row, col))\n .filter((v) => v != null);\n\n if (values.length === 0) return \"text\";\n\n if (values.every((v) => typeof v === \"boolean\")) return \"boolean\";\n if (values.every((v) => typeof v === \"number\" && Number.isFinite(v))) {\n return \"number\";\n }\n\n const strings = values.flatMap(selectCellValues);\n const unique = new Set(strings);\n if (\n unique.size >= 1 &&\n unique.size <= SELECT_CARDINALITY_CAP &&\n unique.size < strings.length * 0.6\n ) {\n return \"select\";\n }\n\n return \"text\";\n}\n\nexport function collectSelectOptions<T>(\n col: MatrxColumnDef<T>,\n rows: T[],\n): Array<{ value: string; label: string }> {\n const seen = new Set<string>();\n const options: Array<{ value: string; label: string }> = [];\n let hasEmpty = false;\n for (const row of rows) {\n const values = selectCellValues(getCellValue(row, col));\n if (values.length === 0) {\n hasEmpty = true;\n continue;\n }\n for (const value of values) {\n if (col.filterOptions?.length || seen.has(value)) continue;\n seen.add(value);\n options.push({ value, label: value });\n }\n }\n const base = col.filterOptions?.length\n ? [...col.filterOptions]\n : options.sort((a, b) => a.label.localeCompare(b.label));\n // Emptiness sentinels: whenever the column has blank cells, every select\n // filter can isolate them — or exclude them — with zero per-table config.\n if (hasEmpty) {\n base.unshift(\n { value: SELECT_EMPTY_VALUE, label: \"(empty)\" },\n { value: SELECT_NOT_EMPTY_VALUE, label: \"(not empty)\" },\n );\n }\n return base;\n}\n\nexport function filterKindLabel(\n kind: ColumnFilterKind | ResolvedFilterKind,\n): string {\n switch (kind) {\n case \"text\":\n return \"Text\";\n case \"select\":\n return \"Select\";\n case \"boolean\":\n return \"Boolean\";\n case \"number\":\n return \"Number\";\n case \"auto\":\n return \"Auto\";\n case false:\n return \"Off\";\n }\n}\n","import type { ColumnMarker, MatrxColumnDef } from \"./types\";\n\nexport type ResolvedColumnMarker = Exclude<ColumnMarker, false>;\n\nconst MARKER_ACCESSORS: Readonly<Record<string, ResolvedColumnMarker>> = {\n favorite: \"favorite\",\n is_favorite: \"favorite\",\n favourite: \"favorite\",\n is_favourite: \"favorite\",\n pinned: \"pin\",\n is_pinned: \"pin\",\n};\n\n/**\n * Resolve the semantic without guessing from partial names. A field such as\n * `favorite_count` is not a marker; consumers can always opt in explicitly.\n */\nexport function resolveColumnMarker<T>(\n column: MatrxColumnDef<T>,\n): ResolvedColumnMarker | undefined {\n if (column.marker === false) return undefined;\n if (column.marker) return column.marker;\n return column.accessorKey ? MARKER_ACCESSORS[column.accessorKey] : undefined;\n}\n\nexport function markerCopy(marker: ResolvedColumnMarker): {\n label: string;\n lowerLabel: string;\n onlyLabel: string;\n notLabel: string;\n} {\n return marker === \"favorite\"\n ? {\n label: \"Favorites\",\n lowerLabel: \"favorites\",\n onlyLabel: \"Favorites only\",\n notLabel: \"Not favorites\",\n }\n : {\n label: \"Pins\",\n lowerLabel: \"pins\",\n onlyLabel: \"Pins only\",\n notLabel: \"Not pinned\",\n };\n}\n","import type {\n ColumnFilterValue,\n ColumnFiltersState,\n MatrxColumnDef,\n SortState,\n TableSearchMatchMode,\n} from \"./types\";\nimport {\n completeLayeredFilterRules,\n layeredFilterMatchesValue,\n type LayeredFilterRule,\n} from \"./layered-filters\";\n\n/**\n * Sentinel select-filter values for \"no value\" / \"any value\" — every select\n * filter offers them automatically when the column has empty cells, so any\n * table can isolate rows missing a value (or having one) without extra config.\n */\nexport const SELECT_EMPTY_VALUE = \"__empty__\";\nexport const SELECT_NOT_EMPTY_VALUE = \"__not_empty__\";\n\nexport function columnId<T>(col: MatrxColumnDef<T>): string {\n if (col.id) return col.id;\n if (col.accessorKey) return String(col.accessorKey);\n throw new Error(\"MatrxDataTable column requires `id` or `accessorKey`\");\n}\n\nexport function getCellValue<T>(row: T, col: MatrxColumnDef<T>): unknown {\n if (col.accessorFn) return col.accessorFn(row);\n if (col.accessorKey) {\n return (row as Record<string, unknown>)[col.accessorKey];\n }\n return undefined;\n}\n\nexport function getFilterValue<T>(row: T, col: MatrxColumnDef<T>): unknown {\n return col.filterValue ? col.filterValue(row) : getCellValue(row, col);\n}\n\nexport function getSortValue<T>(row: T, col: MatrxColumnDef<T>): unknown {\n return col.sortValue ? col.sortValue(row) : getCellValue(row, col);\n}\n\nexport function stringifyCellValue(value: unknown): string {\n if (value == null) return \"\";\n if (typeof value === \"string\") return value;\n if (typeof value === \"number\" || typeof value === \"boolean\") {\n return String(value);\n }\n if (value instanceof Date) return value.toISOString();\n try {\n return JSON.stringify(value);\n } catch {\n return String(value);\n }\n}\n\n/** Unicode-aware tokenization shared by whole-word table search. */\nexport function tableSearchWords(value: string): string[] {\n return value\n .toLocaleLowerCase()\n .split(/[^\\p{L}\\p{N}]+/u)\n .filter(Boolean);\n}\n\n/**\n * Match the primary table search against one row's searchable text. In\n * whole-word mode every entered word must exist as a complete token, so\n * `are` matches `we are ready` but never `hardware`.\n */\nexport function matchesTableSearch(\n searchableText: string,\n query: string,\n mode: TableSearchMatchMode = \"contains\",\n): boolean {\n const normalizedQuery = query.trim().toLocaleLowerCase();\n if (!normalizedQuery) return true;\n if (mode === \"contains\") {\n return searchableText.toLocaleLowerCase().includes(normalizedQuery);\n }\n const actualWords = new Set(tableSearchWords(searchableText));\n const expectedWords = tableSearchWords(normalizedQuery);\n return (\n expectedWords.length > 0 &&\n expectedWords.every((word) => actualWords.has(word))\n );\n}\n\nfunction compareValues(a: unknown, b: unknown): number {\n if (a == null && b == null) return 0;\n if (a == null) return -1;\n if (b == null) return 1;\n if (typeof a === \"number\" && typeof b === \"number\") return a - b;\n if (typeof a === \"boolean\" && typeof b === \"boolean\") {\n return Number(a) - Number(b);\n }\n return stringifyCellValue(a).localeCompare(stringifyCellValue(b), undefined, {\n numeric: true,\n sensitivity: \"base\",\n });\n}\n\n/** Select filters treat array-valued cells as sets, never as serialized JSON choices. */\nexport function selectCellValues(value: unknown): string[] {\n return (Array.isArray(value) ? value : [value])\n .map(stringifyCellValue)\n .filter((item) => item !== \"\");\n}\n\nexport function passesColumnFilter(\n value: unknown,\n filter: ColumnFilterValue,\n): boolean {\n const matches = (() => {\n switch (filter.kind) {\n case \"text\": {\n const s = stringifyCellValue(value);\n if (filter.mode === \"null\") return value == null;\n if (filter.mode === \"not_null\") return value != null;\n if (filter.mode === \"empty\") return s.trim() === \"\";\n if (filter.mode === \"not_empty\") return s.trim() !== \"\";\n const q = filter.value.trim().toLowerCase();\n if (!q) return true;\n return s.toLowerCase().includes(q);\n }\n case \"select\": {\n const selected =\n filter.values ??\n (filter.value && filter.value !== \"__all__\" ? [filter.value] : []);\n if (selected.length === 0) return true;\n const values = selectCellValues(value);\n // OR semantics across the set; sentinels compose (\"A or (empty)\").\n return selected.some((v) =>\n v === SELECT_EMPTY_VALUE\n ? values.length === 0\n : v === SELECT_NOT_EMPTY_VALUE\n ? values.length > 0\n : values.includes(v),\n );\n }\n case \"boolean\": {\n return Boolean(value) === filter.value;\n }\n case \"number\": {\n const n =\n typeof value === \"number\"\n ? value\n : Number(stringifyCellValue(value).replace(/[^0-9.-]/g, \"\"));\n if (Number.isNaN(n)) return false;\n if (filter.min !== undefined && n < filter.min) return false;\n if (filter.max !== undefined && n > filter.max) return false;\n return true;\n }\n }\n })();\n return filter.negated ? !matches : matches;\n}\n\nexport function filterAndSortRows<T>(\n rows: T[],\n columns: MatrxColumnDef<T>[],\n columnFilters: ColumnFiltersState,\n sort: SortState | null,\n globalSearch: string,\n anyOf?: { columnIds: string[]; query: string },\n layeredFilters?: readonly LayeredFilterRule[],\n globalSearchMode: TableSearchMatchMode = \"contains\",\n searchText?: (row: T) => string,\n): T[] {\n const colById = new Map(columns.map((c) => [columnId(c), c]));\n const q = globalSearch.trim().toLowerCase();\n const anyQ = anyOf?.query.trim().toLowerCase() ?? \"\";\n const activeLayeredFilters = completeLayeredFilterRules(layeredFilters);\n\n let result = rows.filter((row) => {\n for (const [id, filter] of Object.entries(columnFilters)) {\n if (!filter) continue;\n const col = colById.get(id);\n if (!col || col.filter === false) continue;\n if (!passesColumnFilter(getFilterValue(row, col), filter)) return false;\n }\n\n for (const rule of activeLayeredFilters) {\n const col = colById.get(rule.field);\n if (!col || !layeredFilterMatchesValue(getFilterValue(row, col), rule)) {\n return false;\n }\n }\n\n if (anyQ && anyOf) {\n const hit = anyOf.columnIds.some((id) => {\n const col = colById.get(id);\n if (!col) return false;\n return stringifyCellValue(getFilterValue(row, col))\n .toLowerCase()\n .includes(anyQ);\n });\n if (!hit) return false;\n }\n\n if (!q) return true;\n const searchableValues = columns\n .filter(\n (col) =>\n col.filter !== false || Boolean(col.accessorKey || col.accessorFn),\n )\n .map((col) => stringifyCellValue(getFilterValue(row, col)));\n const additionalSearchText = searchText?.(row);\n if (additionalSearchText) searchableValues.push(additionalSearchText);\n if (globalSearchMode === \"contains\") {\n return searchableValues.some((value) =>\n matchesTableSearch(value, q, \"contains\"),\n );\n }\n return matchesTableSearch(searchableValues.join(\" \"), q, \"whole_words\");\n });\n\n if (sort) {\n const col = colById.get(sort.id);\n if (col && col.sortable !== false) {\n const dir = sort.direction === \"asc\" ? 1 : -1;\n result = [...result].sort((a, b) => {\n const va = getSortValue(a, col);\n const vb = getSortValue(b, col);\n // Empty cells (null / \"\") sort LAST in both directions — sorting a\n // column should surface its values, never a wall of blanks.\n const ea = va == null || stringifyCellValue(va) === \"\";\n const eb = vb == null || stringifyCellValue(vb) === \"\";\n if (ea !== eb) return ea ? 1 : -1;\n return compareValues(va, vb) * dir;\n });\n }\n }\n\n return result;\n}\n\nexport function countActiveColumnFilters(filters: ColumnFiltersState): number {\n let n = 0;\n for (const f of Object.values(filters)) {\n if (!f) continue;\n switch (f.kind) {\n case \"text\":\n if (f.value.trim() || (f.mode && f.mode !== \"contains\")) n += 1;\n break;\n case \"select\":\n if (f.values ? f.values.length > 0 : f.value && f.value !== \"__all__\")\n n += 1;\n break;\n case \"boolean\":\n n += 1;\n break;\n case \"number\":\n if (f.min !== undefined || f.max !== undefined) n += 1;\n break;\n }\n }\n return n;\n}\n\nexport function isColumnFilterActive(\n filter: ColumnFilterValue | undefined,\n): boolean {\n if (!filter) return false;\n switch (filter.kind) {\n case \"text\":\n return (\n filter.value.trim().length > 0 ||\n (filter.mode !== undefined && filter.mode !== \"contains\")\n );\n case \"select\":\n return filter.values\n ? filter.values.length > 0\n : Boolean(filter.value) && filter.value !== \"__all__\";\n case \"boolean\":\n return true;\n case \"number\":\n return filter.min !== undefined || filter.max !== undefined;\n }\n}\n\n/** Apply draft edits onto a row for display (shallow field merge). */\nexport function applyRowEdits<T>(\n row: T,\n edits: Record<string, unknown> | undefined,\n): T {\n if (!edits || Object.keys(edits).length === 0) return row;\n return { ...row, ...edits };\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/data-table/infer-filter.ts","../../src/data-table/column-marker.ts","../../src/data-table/filter-engine.ts"],"sourcesContent":["import type { ColumnFilterKind, MatrxColumnDef } from \"./types\";\nimport { resolveColumnMarker } from \"./column-marker\";\nimport {\n SELECT_EMPTY_VALUE,\n SELECT_NOT_EMPTY_VALUE,\n SELECT_NULL_VALUE,\n getCellValue,\n selectCellValues,\n} from \"./filter-engine\";\n\n/** Auto-inference switches to text above this many distinct values… */\nconst SELECT_CARDINALITY_CAP = 24;\n\nexport type ResolvedFilterKind = \"text\" | \"select\" | \"boolean\" | \"number\";\n\n/**\n * Infer a filter UI from sample row values. Prefer select for low-cardinality\n * string/enum columns, boolean for booleans, number for numeric, else text.\n */\nexport function resolveFilterKind<T>(\n col: MatrxColumnDef<T>,\n sampleRows: T[],\n): ResolvedFilterKind | null {\n if (col.filter === false) return null;\n if (col.filter && col.filter !== \"auto\") return col.filter;\n if (resolveColumnMarker(col)) return \"boolean\";\n\n const values = sampleRows\n .slice(0, 200)\n .map((row) => getCellValue(row, col))\n .filter((v) => v != null);\n\n if (values.length === 0) return \"text\";\n\n if (values.every((v) => typeof v === \"boolean\")) return \"boolean\";\n if (values.every((v) => typeof v === \"number\" && Number.isFinite(v))) {\n return \"number\";\n }\n\n const strings = values.flatMap(selectCellValues);\n const unique = new Set(strings);\n if (\n unique.size >= 1 &&\n unique.size <= SELECT_CARDINALITY_CAP &&\n unique.size < strings.length * 0.6\n ) {\n return \"select\";\n }\n\n return \"text\";\n}\n\nexport function collectSelectOptions<T>(\n col: MatrxColumnDef<T>,\n rows: T[],\n): Array<{ value: string; label: string }> {\n const seen = new Set<string>();\n const options: Array<{ value: string; label: string }> = [];\n let hasEmpty = false;\n let hasEmptyCollection = false;\n for (const row of rows) {\n const cell = getCellValue(row, col);\n const values = selectCellValues(cell);\n if (values.length === 0) {\n hasEmpty = true;\n if (Array.isArray(cell)) hasEmptyCollection = true;\n continue;\n }\n for (const value of values) {\n if (col.filterOptions?.length || seen.has(value)) continue;\n seen.add(value);\n options.push({ value, label: value });\n }\n }\n const base = col.filterOptions?.length\n ? col.filterOptions.map((option) => ({\n ...option,\n label: option.label.trim() ? option.label : option.value === \"\" ? \"(empty string)\" : option.value.trim() ? option.value : \"(whitespace)\",\n }))\n : options.sort((a, b) => a.label.localeCompare(b.label));\n // Emptiness sentinels: whenever the column has blank cells, every select\n // filter can isolate them — or exclude them — with zero per-table config.\n if (hasEmpty) {\n // An explicit literal-empty choice must remain independently deselectable.\n // The combined empty sentinel would overlap it after All -> uncheck.\n // Empty collections still require that broader option: neither null nor\n // a literal empty string represents an empty array.\n const emptyOption = !hasEmptyCollection && base.some((option) => option.value === \"\")\n ? { value: SELECT_NULL_VALUE, label: \"(null)\" }\n : { value: SELECT_EMPTY_VALUE, label: \"(empty)\" };\n const additions = [];\n if (!base.some((option) => option.value === emptyOption.value)) additions.push(emptyOption);\n if (!base.some((option) => option.value === SELECT_NOT_EMPTY_VALUE)) {\n additions.push({ value: SELECT_NOT_EMPTY_VALUE, label: \"(not empty)\" });\n }\n base.unshift(...additions);\n }\n return base;\n}\n\nexport function filterKindLabel(\n kind: ColumnFilterKind | ResolvedFilterKind,\n): string {\n switch (kind) {\n case \"text\":\n return \"Text\";\n case \"select\":\n return \"Select\";\n case \"boolean\":\n return \"Boolean\";\n case \"number\":\n return \"Number\";\n case \"auto\":\n return \"Auto\";\n case false:\n return \"Off\";\n }\n}\n","import type { ColumnMarker, MatrxColumnDef } from \"./types\";\n\nexport type ResolvedColumnMarker = Exclude<ColumnMarker, false>;\n\nconst MARKER_ACCESSORS: Readonly<Record<string, ResolvedColumnMarker>> = {\n favorite: \"favorite\",\n is_favorite: \"favorite\",\n favourite: \"favorite\",\n is_favourite: \"favorite\",\n pinned: \"pin\",\n is_pinned: \"pin\",\n};\n\n/**\n * Resolve the semantic without guessing from partial names. A field such as\n * `favorite_count` is not a marker; consumers can always opt in explicitly.\n */\nexport function resolveColumnMarker<T>(\n column: MatrxColumnDef<T>,\n): ResolvedColumnMarker | undefined {\n if (column.marker === false) return undefined;\n if (column.marker) return column.marker;\n return column.accessorKey ? MARKER_ACCESSORS[column.accessorKey] : undefined;\n}\n\nexport function markerCopy(marker: ResolvedColumnMarker): {\n label: string;\n lowerLabel: string;\n onlyLabel: string;\n notLabel: string;\n} {\n return marker === \"favorite\"\n ? {\n label: \"Favorites\",\n lowerLabel: \"favorites\",\n onlyLabel: \"Favorites only\",\n notLabel: \"Not favorites\",\n }\n : {\n label: \"Pins\",\n lowerLabel: \"pins\",\n onlyLabel: \"Pins only\",\n notLabel: \"Not pinned\",\n };\n}\n","import type {\n ColumnFilterValue,\n ColumnFiltersState,\n MatrxColumnDef,\n SortState,\n TableSearchMatchMode,\n} from \"./types\";\nimport {\n completeLayeredFilterRules,\n layeredFilterMatchesValue,\n type LayeredFilterRule,\n} from \"./layered-filters\";\n\n/**\n * Sentinel select-filter values for \"no value\" / \"any value\" — every select\n * filter offers them automatically when the column has empty cells, so any\n * table can isolate rows missing a value (or having one) without extra config.\n */\nexport const SELECT_EMPTY_VALUE = \"__empty__\";\nexport const SELECT_NOT_EMPTY_VALUE = \"__not_empty__\";\n/** Exact SQL-style NULL selection, distinct from the empty-cell option. */\nexport const SELECT_NULL_VALUE = \"__null__\";\n\nexport function columnId<T>(col: MatrxColumnDef<T>): string {\n if (col.id) return col.id;\n if (col.accessorKey) return String(col.accessorKey);\n throw new Error(\"MatrxDataTable column requires `id` or `accessorKey`\");\n}\n\nexport function getCellValue<T>(row: T, col: MatrxColumnDef<T>): unknown {\n if (col.accessorFn) return col.accessorFn(row);\n if (col.accessorKey) {\n return (row as Record<string, unknown>)[col.accessorKey];\n }\n return undefined;\n}\n\nexport function getFilterValue<T>(row: T, col: MatrxColumnDef<T>): unknown {\n return col.filterValue ? col.filterValue(row) : getCellValue(row, col);\n}\n\nexport function getSortValue<T>(row: T, col: MatrxColumnDef<T>): unknown {\n return col.sortValue ? col.sortValue(row) : getCellValue(row, col);\n}\n\nexport function stringifyCellValue(value: unknown): string {\n if (value == null) return \"\";\n if (typeof value === \"string\") return value;\n if (typeof value === \"number\" || typeof value === \"boolean\") {\n return String(value);\n }\n if (value instanceof Date) return value.toISOString();\n try {\n return JSON.stringify(value);\n } catch {\n return String(value);\n }\n}\n\n/** Unicode-aware tokenization shared by whole-word table search. */\nexport function tableSearchWords(value: string): string[] {\n return value\n .toLocaleLowerCase()\n .split(/[^\\p{L}\\p{N}]+/u)\n .filter(Boolean);\n}\n\n/**\n * Match the primary table search against one row's searchable text. In\n * whole-word mode every entered word must exist as a complete token, so\n * `are` matches `we are ready` but never `hardware`.\n */\nexport function matchesTableSearch(\n searchableText: string,\n query: string,\n mode: TableSearchMatchMode = \"contains\",\n): boolean {\n const normalizedQuery = query.trim().toLocaleLowerCase();\n if (!normalizedQuery) return true;\n if (mode === \"contains\") {\n return searchableText.toLocaleLowerCase().includes(normalizedQuery);\n }\n const actualWords = new Set(tableSearchWords(searchableText));\n const expectedWords = tableSearchWords(normalizedQuery);\n return (\n expectedWords.length > 0 &&\n expectedWords.every((word) => actualWords.has(word))\n );\n}\n\nfunction compareValues(a: unknown, b: unknown): number {\n if (a == null && b == null) return 0;\n if (a == null) return -1;\n if (b == null) return 1;\n if (typeof a === \"number\" && typeof b === \"number\") return a - b;\n if (typeof a === \"boolean\" && typeof b === \"boolean\") {\n return Number(a) - Number(b);\n }\n return stringifyCellValue(a).localeCompare(stringifyCellValue(b), undefined, {\n numeric: true,\n sensitivity: \"base\",\n });\n}\n\n/** Select filters treat array-valued cells as sets, never as serialized JSON choices. */\nexport function selectCellValues(value: unknown): string[] {\n return (Array.isArray(value) ? value : [value])\n .map(stringifyCellValue)\n .filter((item) => item !== \"\");\n}\n\nexport function passesColumnFilter(\n value: unknown,\n filter: ColumnFilterValue,\n): boolean {\n const matches = (() => {\n switch (filter.kind) {\n case \"text\": {\n const s = stringifyCellValue(value);\n if (filter.mode === \"null\") return value == null;\n if (filter.mode === \"not_null\") return value != null;\n if (filter.mode === \"empty\") return s.trim() === \"\";\n if (filter.mode === \"not_empty\") return s.trim() !== \"\";\n const q = filter.value.trim().toLowerCase();\n if (!q) return true;\n return s.toLowerCase().includes(q);\n }\n case \"select\": {\n // `values: []` is an explicit multi-select state: the user deselected\n // every option, so it must produce an empty result. Legacy single-value\n // filters and an absent `values` field retain their clear/all behavior.\n if (filter.values !== undefined && filter.values.length === 0) {\n return false;\n }\n const selected = filter.values ??\n (filter.value && filter.value !== \"__all__\" ? [filter.value] : []);\n if (selected.length === 0) return true;\n const values = selectCellValues(value);\n // OR semantics across the set; sentinels compose (\"A or (empty)\").\n return selected.some((v) =>\n v === SELECT_NULL_VALUE\n ? value == null\n : v === SELECT_EMPTY_VALUE\n ? values.length === 0\n : v === SELECT_NOT_EMPTY_VALUE\n ? values.length > 0\n : v === \"\"\n ? value === \"\" || (Array.isArray(value) && value.includes(\"\"))\n : values.includes(v),\n );\n }\n case \"boolean\": {\n return Boolean(value) === filter.value;\n }\n case \"number\": {\n const n =\n typeof value === \"number\"\n ? value\n : Number(stringifyCellValue(value).replace(/[^0-9.-]/g, \"\"));\n if (Number.isNaN(n)) return false;\n if (filter.min !== undefined && n < filter.min) return false;\n if (filter.max !== undefined && n > filter.max) return false;\n return true;\n }\n }\n })();\n return filter.negated ? !matches : matches;\n}\n\nexport function filterAndSortRows<T>(\n rows: T[],\n columns: MatrxColumnDef<T>[],\n columnFilters: ColumnFiltersState,\n sort: SortState | null,\n globalSearch: string,\n anyOf?: { columnIds: string[]; query: string },\n layeredFilters?: readonly LayeredFilterRule[],\n globalSearchMode: TableSearchMatchMode = \"contains\",\n searchText?: (row: T) => string,\n): T[] {\n const colById = new Map(columns.map((c) => [columnId(c), c]));\n const q = globalSearch.trim().toLowerCase();\n const anyQ = anyOf?.query.trim().toLowerCase() ?? \"\";\n const activeLayeredFilters = completeLayeredFilterRules(layeredFilters);\n\n let result = rows.filter((row) => {\n for (const [id, filter] of Object.entries(columnFilters)) {\n if (!filter) continue;\n const col = colById.get(id);\n if (!col || col.filter === false) continue;\n if (!passesColumnFilter(getFilterValue(row, col), filter)) return false;\n }\n\n for (const rule of activeLayeredFilters) {\n const col = colById.get(rule.field);\n if (!col || !layeredFilterMatchesValue(getFilterValue(row, col), rule)) {\n return false;\n }\n }\n\n if (anyQ && anyOf) {\n const hit = anyOf.columnIds.some((id) => {\n const col = colById.get(id);\n if (!col) return false;\n return stringifyCellValue(getFilterValue(row, col))\n .toLowerCase()\n .includes(anyQ);\n });\n if (!hit) return false;\n }\n\n if (!q) return true;\n const searchableValues = columns\n .filter(\n (col) =>\n col.filter !== false || Boolean(col.accessorKey || col.accessorFn),\n )\n .map((col) => stringifyCellValue(getFilterValue(row, col)));\n const additionalSearchText = searchText?.(row);\n if (additionalSearchText) searchableValues.push(additionalSearchText);\n if (globalSearchMode === \"contains\") {\n return searchableValues.some((value) =>\n matchesTableSearch(value, q, \"contains\"),\n );\n }\n return matchesTableSearch(searchableValues.join(\" \"), q, \"whole_words\");\n });\n\n if (sort) {\n const col = colById.get(sort.id);\n if (col && col.sortable !== false) {\n const dir = sort.direction === \"asc\" ? 1 : -1;\n result = [...result].sort((a, b) => {\n const va = getSortValue(a, col);\n const vb = getSortValue(b, col);\n // Empty cells (null / \"\") sort LAST in both directions — sorting a\n // column should surface its values, never a wall of blanks.\n const ea = va == null || stringifyCellValue(va) === \"\";\n const eb = vb == null || stringifyCellValue(vb) === \"\";\n if (ea !== eb) return ea ? 1 : -1;\n return compareValues(va, vb) * dir;\n });\n }\n }\n\n return result;\n}\n\nexport function countActiveColumnFilters(filters: ColumnFiltersState): number {\n let n = 0;\n for (const f of Object.values(filters)) {\n if (!f) continue;\n switch (f.kind) {\n case \"text\":\n if (f.value.trim() || (f.mode && f.mode !== \"contains\")) n += 1;\n break;\n case \"select\":\n if (f.values !== undefined || (f.value && f.value !== \"__all__\"))\n n += 1;\n break;\n case \"boolean\":\n n += 1;\n break;\n case \"number\":\n if (f.min !== undefined || f.max !== undefined) n += 1;\n break;\n }\n }\n return n;\n}\n\nexport function isColumnFilterActive(\n filter: ColumnFilterValue | undefined,\n): boolean {\n if (!filter) return false;\n switch (filter.kind) {\n case \"text\":\n return (\n filter.value.trim().length > 0 ||\n (filter.mode !== undefined && filter.mode !== \"contains\")\n );\n case \"select\":\n return filter.values !== undefined\n ? true\n : Boolean(filter.value) && filter.value !== \"__all__\";\n case \"boolean\":\n return true;\n case \"number\":\n return filter.min !== undefined || filter.max !== undefined;\n }\n}\n\n/** Apply draft edits onto a row for display (shallow field merge). */\nexport function applyRowEdits<T>(\n row: T,\n edits: Record<string, unknown> | undefined,\n): T {\n if (!edits || Object.keys(edits).length === 0) return row;\n return { ...row, ...edits };\n}\n"],"mappings":"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","names":[]}
|
|
@@ -17,6 +17,7 @@ function resolveColumnMarker(column) {
|
|
|
17
17
|
// src/data-table/filter-engine.ts
|
|
18
18
|
var SELECT_EMPTY_VALUE = "__empty__";
|
|
19
19
|
var SELECT_NOT_EMPTY_VALUE = "__not_empty__";
|
|
20
|
+
var SELECT_NULL_VALUE = "__null__";
|
|
20
21
|
function getCellValue(row, col) {
|
|
21
22
|
if (col.accessorFn) return col.accessorFn(row);
|
|
22
23
|
if (col.accessorKey) {
|
|
@@ -64,10 +65,13 @@ function collectSelectOptions(col, rows) {
|
|
|
64
65
|
const seen = /* @__PURE__ */ new Set();
|
|
65
66
|
const options = [];
|
|
66
67
|
let hasEmpty = false;
|
|
68
|
+
let hasEmptyCollection = false;
|
|
67
69
|
for (const row of rows) {
|
|
68
|
-
const
|
|
70
|
+
const cell = getCellValue(row, col);
|
|
71
|
+
const values = selectCellValues(cell);
|
|
69
72
|
if (values.length === 0) {
|
|
70
73
|
hasEmpty = true;
|
|
74
|
+
if (Array.isArray(cell)) hasEmptyCollection = true;
|
|
71
75
|
continue;
|
|
72
76
|
}
|
|
73
77
|
for (const value of values) {
|
|
@@ -76,12 +80,18 @@ function collectSelectOptions(col, rows) {
|
|
|
76
80
|
options.push({ value, label: value });
|
|
77
81
|
}
|
|
78
82
|
}
|
|
79
|
-
const base = col.filterOptions?.length ?
|
|
83
|
+
const base = col.filterOptions?.length ? col.filterOptions.map((option) => ({
|
|
84
|
+
...option,
|
|
85
|
+
label: option.label.trim() ? option.label : option.value === "" ? "(empty string)" : option.value.trim() ? option.value : "(whitespace)"
|
|
86
|
+
})) : options.sort((a, b) => a.label.localeCompare(b.label));
|
|
80
87
|
if (hasEmpty) {
|
|
81
|
-
base.
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
)
|
|
88
|
+
const emptyOption = !hasEmptyCollection && base.some((option) => option.value === "") ? { value: SELECT_NULL_VALUE, label: "(null)" } : { value: SELECT_EMPTY_VALUE, label: "(empty)" };
|
|
89
|
+
const additions = [];
|
|
90
|
+
if (!base.some((option) => option.value === emptyOption.value)) additions.push(emptyOption);
|
|
91
|
+
if (!base.some((option) => option.value === SELECT_NOT_EMPTY_VALUE)) {
|
|
92
|
+
additions.push({ value: SELECT_NOT_EMPTY_VALUE, label: "(not empty)" });
|
|
93
|
+
}
|
|
94
|
+
base.unshift(...additions);
|
|
85
95
|
}
|
|
86
96
|
return base;
|
|
87
97
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/data-table/column-marker.ts","../../src/data-table/filter-engine.ts","../../src/data-table/infer-filter.ts"],"sourcesContent":["import type { ColumnMarker, MatrxColumnDef } from \"./types\";\n\nexport type ResolvedColumnMarker = Exclude<ColumnMarker, false>;\n\nconst MARKER_ACCESSORS: Readonly<Record<string, ResolvedColumnMarker>> = {\n favorite: \"favorite\",\n is_favorite: \"favorite\",\n favourite: \"favorite\",\n is_favourite: \"favorite\",\n pinned: \"pin\",\n is_pinned: \"pin\",\n};\n\n/**\n * Resolve the semantic without guessing from partial names. A field such as\n * `favorite_count` is not a marker; consumers can always opt in explicitly.\n */\nexport function resolveColumnMarker<T>(\n column: MatrxColumnDef<T>,\n): ResolvedColumnMarker | undefined {\n if (column.marker === false) return undefined;\n if (column.marker) return column.marker;\n return column.accessorKey ? MARKER_ACCESSORS[column.accessorKey] : undefined;\n}\n\nexport function markerCopy(marker: ResolvedColumnMarker): {\n label: string;\n lowerLabel: string;\n onlyLabel: string;\n notLabel: string;\n} {\n return marker === \"favorite\"\n ? {\n label: \"Favorites\",\n lowerLabel: \"favorites\",\n onlyLabel: \"Favorites only\",\n notLabel: \"Not favorites\",\n }\n : {\n label: \"Pins\",\n lowerLabel: \"pins\",\n onlyLabel: \"Pins only\",\n notLabel: \"Not pinned\",\n };\n}\n","import type {\n ColumnFilterValue,\n ColumnFiltersState,\n MatrxColumnDef,\n SortState,\n TableSearchMatchMode,\n} from \"./types\";\nimport {\n completeLayeredFilterRules,\n layeredFilterMatchesValue,\n type LayeredFilterRule,\n} from \"./layered-filters\";\n\n/**\n * Sentinel select-filter values for \"no value\" / \"any value\" — every select\n * filter offers them automatically when the column has empty cells, so any\n * table can isolate rows missing a value (or having one) without extra config.\n */\nexport const SELECT_EMPTY_VALUE = \"__empty__\";\nexport const SELECT_NOT_EMPTY_VALUE = \"__not_empty__\";\n\nexport function columnId<T>(col: MatrxColumnDef<T>): string {\n if (col.id) return col.id;\n if (col.accessorKey) return String(col.accessorKey);\n throw new Error(\"MatrxDataTable column requires `id` or `accessorKey`\");\n}\n\nexport function getCellValue<T>(row: T, col: MatrxColumnDef<T>): unknown {\n if (col.accessorFn) return col.accessorFn(row);\n if (col.accessorKey) {\n return (row as Record<string, unknown>)[col.accessorKey];\n }\n return undefined;\n}\n\nexport function getFilterValue<T>(row: T, col: MatrxColumnDef<T>): unknown {\n return col.filterValue ? col.filterValue(row) : getCellValue(row, col);\n}\n\nexport function getSortValue<T>(row: T, col: MatrxColumnDef<T>): unknown {\n return col.sortValue ? col.sortValue(row) : getCellValue(row, col);\n}\n\nexport function stringifyCellValue(value: unknown): string {\n if (value == null) return \"\";\n if (typeof value === \"string\") return value;\n if (typeof value === \"number\" || typeof value === \"boolean\") {\n return String(value);\n }\n if (value instanceof Date) return value.toISOString();\n try {\n return JSON.stringify(value);\n } catch {\n return String(value);\n }\n}\n\n/** Unicode-aware tokenization shared by whole-word table search. */\nexport function tableSearchWords(value: string): string[] {\n return value\n .toLocaleLowerCase()\n .split(/[^\\p{L}\\p{N}]+/u)\n .filter(Boolean);\n}\n\n/**\n * Match the primary table search against one row's searchable text. In\n * whole-word mode every entered word must exist as a complete token, so\n * `are` matches `we are ready` but never `hardware`.\n */\nexport function matchesTableSearch(\n searchableText: string,\n query: string,\n mode: TableSearchMatchMode = \"contains\",\n): boolean {\n const normalizedQuery = query.trim().toLocaleLowerCase();\n if (!normalizedQuery) return true;\n if (mode === \"contains\") {\n return searchableText.toLocaleLowerCase().includes(normalizedQuery);\n }\n const actualWords = new Set(tableSearchWords(searchableText));\n const expectedWords = tableSearchWords(normalizedQuery);\n return (\n expectedWords.length > 0 &&\n expectedWords.every((word) => actualWords.has(word))\n );\n}\n\nfunction compareValues(a: unknown, b: unknown): number {\n if (a == null && b == null) return 0;\n if (a == null) return -1;\n if (b == null) return 1;\n if (typeof a === \"number\" && typeof b === \"number\") return a - b;\n if (typeof a === \"boolean\" && typeof b === \"boolean\") {\n return Number(a) - Number(b);\n }\n return stringifyCellValue(a).localeCompare(stringifyCellValue(b), undefined, {\n numeric: true,\n sensitivity: \"base\",\n });\n}\n\n/** Select filters treat array-valued cells as sets, never as serialized JSON choices. */\nexport function selectCellValues(value: unknown): string[] {\n return (Array.isArray(value) ? value : [value])\n .map(stringifyCellValue)\n .filter((item) => item !== \"\");\n}\n\nexport function passesColumnFilter(\n value: unknown,\n filter: ColumnFilterValue,\n): boolean {\n const matches = (() => {\n switch (filter.kind) {\n case \"text\": {\n const s = stringifyCellValue(value);\n if (filter.mode === \"null\") return value == null;\n if (filter.mode === \"not_null\") return value != null;\n if (filter.mode === \"empty\") return s.trim() === \"\";\n if (filter.mode === \"not_empty\") return s.trim() !== \"\";\n const q = filter.value.trim().toLowerCase();\n if (!q) return true;\n return s.toLowerCase().includes(q);\n }\n case \"select\": {\n const selected =\n filter.values ??\n (filter.value && filter.value !== \"__all__\" ? [filter.value] : []);\n if (selected.length === 0) return true;\n const values = selectCellValues(value);\n // OR semantics across the set; sentinels compose (\"A or (empty)\").\n return selected.some((v) =>\n v === SELECT_EMPTY_VALUE\n ? values.length === 0\n : v === SELECT_NOT_EMPTY_VALUE\n ? values.length > 0\n : values.includes(v),\n );\n }\n case \"boolean\": {\n return Boolean(value) === filter.value;\n }\n case \"number\": {\n const n =\n typeof value === \"number\"\n ? value\n : Number(stringifyCellValue(value).replace(/[^0-9.-]/g, \"\"));\n if (Number.isNaN(n)) return false;\n if (filter.min !== undefined && n < filter.min) return false;\n if (filter.max !== undefined && n > filter.max) return false;\n return true;\n }\n }\n })();\n return filter.negated ? !matches : matches;\n}\n\nexport function filterAndSortRows<T>(\n rows: T[],\n columns: MatrxColumnDef<T>[],\n columnFilters: ColumnFiltersState,\n sort: SortState | null,\n globalSearch: string,\n anyOf?: { columnIds: string[]; query: string },\n layeredFilters?: readonly LayeredFilterRule[],\n globalSearchMode: TableSearchMatchMode = \"contains\",\n searchText?: (row: T) => string,\n): T[] {\n const colById = new Map(columns.map((c) => [columnId(c), c]));\n const q = globalSearch.trim().toLowerCase();\n const anyQ = anyOf?.query.trim().toLowerCase() ?? \"\";\n const activeLayeredFilters = completeLayeredFilterRules(layeredFilters);\n\n let result = rows.filter((row) => {\n for (const [id, filter] of Object.entries(columnFilters)) {\n if (!filter) continue;\n const col = colById.get(id);\n if (!col || col.filter === false) continue;\n if (!passesColumnFilter(getFilterValue(row, col), filter)) return false;\n }\n\n for (const rule of activeLayeredFilters) {\n const col = colById.get(rule.field);\n if (!col || !layeredFilterMatchesValue(getFilterValue(row, col), rule)) {\n return false;\n }\n }\n\n if (anyQ && anyOf) {\n const hit = anyOf.columnIds.some((id) => {\n const col = colById.get(id);\n if (!col) return false;\n return stringifyCellValue(getFilterValue(row, col))\n .toLowerCase()\n .includes(anyQ);\n });\n if (!hit) return false;\n }\n\n if (!q) return true;\n const searchableValues = columns\n .filter(\n (col) =>\n col.filter !== false || Boolean(col.accessorKey || col.accessorFn),\n )\n .map((col) => stringifyCellValue(getFilterValue(row, col)));\n const additionalSearchText = searchText?.(row);\n if (additionalSearchText) searchableValues.push(additionalSearchText);\n if (globalSearchMode === \"contains\") {\n return searchableValues.some((value) =>\n matchesTableSearch(value, q, \"contains\"),\n );\n }\n return matchesTableSearch(searchableValues.join(\" \"), q, \"whole_words\");\n });\n\n if (sort) {\n const col = colById.get(sort.id);\n if (col && col.sortable !== false) {\n const dir = sort.direction === \"asc\" ? 1 : -1;\n result = [...result].sort((a, b) => {\n const va = getSortValue(a, col);\n const vb = getSortValue(b, col);\n // Empty cells (null / \"\") sort LAST in both directions — sorting a\n // column should surface its values, never a wall of blanks.\n const ea = va == null || stringifyCellValue(va) === \"\";\n const eb = vb == null || stringifyCellValue(vb) === \"\";\n if (ea !== eb) return ea ? 1 : -1;\n return compareValues(va, vb) * dir;\n });\n }\n }\n\n return result;\n}\n\nexport function countActiveColumnFilters(filters: ColumnFiltersState): number {\n let n = 0;\n for (const f of Object.values(filters)) {\n if (!f) continue;\n switch (f.kind) {\n case \"text\":\n if (f.value.trim() || (f.mode && f.mode !== \"contains\")) n += 1;\n break;\n case \"select\":\n if (f.values ? f.values.length > 0 : f.value && f.value !== \"__all__\")\n n += 1;\n break;\n case \"boolean\":\n n += 1;\n break;\n case \"number\":\n if (f.min !== undefined || f.max !== undefined) n += 1;\n break;\n }\n }\n return n;\n}\n\nexport function isColumnFilterActive(\n filter: ColumnFilterValue | undefined,\n): boolean {\n if (!filter) return false;\n switch (filter.kind) {\n case \"text\":\n return (\n filter.value.trim().length > 0 ||\n (filter.mode !== undefined && filter.mode !== \"contains\")\n );\n case \"select\":\n return filter.values\n ? filter.values.length > 0\n : Boolean(filter.value) && filter.value !== \"__all__\";\n case \"boolean\":\n return true;\n case \"number\":\n return filter.min !== undefined || filter.max !== undefined;\n }\n}\n\n/** Apply draft edits onto a row for display (shallow field merge). */\nexport function applyRowEdits<T>(\n row: T,\n edits: Record<string, unknown> | undefined,\n): T {\n if (!edits || Object.keys(edits).length === 0) return row;\n return { ...row, ...edits };\n}\n","import type { ColumnFilterKind, MatrxColumnDef } from \"./types\";\nimport { resolveColumnMarker } from \"./column-marker\";\nimport {\n SELECT_EMPTY_VALUE,\n SELECT_NOT_EMPTY_VALUE,\n getCellValue,\n selectCellValues,\n} from \"./filter-engine\";\n\n/** Auto-inference switches to text above this many distinct values… */\nconst SELECT_CARDINALITY_CAP = 24;\n\nexport type ResolvedFilterKind = \"text\" | \"select\" | \"boolean\" | \"number\";\n\n/**\n * Infer a filter UI from sample row values. Prefer select for low-cardinality\n * string/enum columns, boolean for booleans, number for numeric, else text.\n */\nexport function resolveFilterKind<T>(\n col: MatrxColumnDef<T>,\n sampleRows: T[],\n): ResolvedFilterKind | null {\n if (col.filter === false) return null;\n if (col.filter && col.filter !== \"auto\") return col.filter;\n if (resolveColumnMarker(col)) return \"boolean\";\n\n const values = sampleRows\n .slice(0, 200)\n .map((row) => getCellValue(row, col))\n .filter((v) => v != null);\n\n if (values.length === 0) return \"text\";\n\n if (values.every((v) => typeof v === \"boolean\")) return \"boolean\";\n if (values.every((v) => typeof v === \"number\" && Number.isFinite(v))) {\n return \"number\";\n }\n\n const strings = values.flatMap(selectCellValues);\n const unique = new Set(strings);\n if (\n unique.size >= 1 &&\n unique.size <= SELECT_CARDINALITY_CAP &&\n unique.size < strings.length * 0.6\n ) {\n return \"select\";\n }\n\n return \"text\";\n}\n\nexport function collectSelectOptions<T>(\n col: MatrxColumnDef<T>,\n rows: T[],\n): Array<{ value: string; label: string }> {\n const seen = new Set<string>();\n const options: Array<{ value: string; label: string }> = [];\n let hasEmpty = false;\n for (const row of rows) {\n const values = selectCellValues(getCellValue(row, col));\n if (values.length === 0) {\n hasEmpty = true;\n continue;\n }\n for (const value of values) {\n if (col.filterOptions?.length || seen.has(value)) continue;\n seen.add(value);\n options.push({ value, label: value });\n }\n }\n const base = col.filterOptions?.length\n ? [...col.filterOptions]\n : options.sort((a, b) => a.label.localeCompare(b.label));\n // Emptiness sentinels: whenever the column has blank cells, every select\n // filter can isolate them — or exclude them — with zero per-table config.\n if (hasEmpty) {\n base.unshift(\n { value: SELECT_EMPTY_VALUE, label: \"(empty)\" },\n { value: SELECT_NOT_EMPTY_VALUE, label: \"(not empty)\" },\n );\n }\n return base;\n}\n\nexport function filterKindLabel(\n kind: ColumnFilterKind | ResolvedFilterKind,\n): string {\n switch (kind) {\n case \"text\":\n return \"Text\";\n case \"select\":\n return \"Select\";\n case \"boolean\":\n return \"Boolean\";\n case \"number\":\n return \"Number\";\n case \"auto\":\n return \"Auto\";\n case false:\n return \"Off\";\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/data-table/column-marker.ts","../../src/data-table/filter-engine.ts","../../src/data-table/infer-filter.ts"],"sourcesContent":["import type { ColumnMarker, MatrxColumnDef } from \"./types\";\n\nexport type ResolvedColumnMarker = Exclude<ColumnMarker, false>;\n\nconst MARKER_ACCESSORS: Readonly<Record<string, ResolvedColumnMarker>> = {\n favorite: \"favorite\",\n is_favorite: \"favorite\",\n favourite: \"favorite\",\n is_favourite: \"favorite\",\n pinned: \"pin\",\n is_pinned: \"pin\",\n};\n\n/**\n * Resolve the semantic without guessing from partial names. A field such as\n * `favorite_count` is not a marker; consumers can always opt in explicitly.\n */\nexport function resolveColumnMarker<T>(\n column: MatrxColumnDef<T>,\n): ResolvedColumnMarker | undefined {\n if (column.marker === false) return undefined;\n if (column.marker) return column.marker;\n return column.accessorKey ? MARKER_ACCESSORS[column.accessorKey] : undefined;\n}\n\nexport function markerCopy(marker: ResolvedColumnMarker): {\n label: string;\n lowerLabel: string;\n onlyLabel: string;\n notLabel: string;\n} {\n return marker === \"favorite\"\n ? {\n label: \"Favorites\",\n lowerLabel: \"favorites\",\n onlyLabel: \"Favorites only\",\n notLabel: \"Not favorites\",\n }\n : {\n label: \"Pins\",\n lowerLabel: \"pins\",\n onlyLabel: \"Pins only\",\n notLabel: \"Not pinned\",\n };\n}\n","import type {\n ColumnFilterValue,\n ColumnFiltersState,\n MatrxColumnDef,\n SortState,\n TableSearchMatchMode,\n} from \"./types\";\nimport {\n completeLayeredFilterRules,\n layeredFilterMatchesValue,\n type LayeredFilterRule,\n} from \"./layered-filters\";\n\n/**\n * Sentinel select-filter values for \"no value\" / \"any value\" — every select\n * filter offers them automatically when the column has empty cells, so any\n * table can isolate rows missing a value (or having one) without extra config.\n */\nexport const SELECT_EMPTY_VALUE = \"__empty__\";\nexport const SELECT_NOT_EMPTY_VALUE = \"__not_empty__\";\n/** Exact SQL-style NULL selection, distinct from the empty-cell option. */\nexport const SELECT_NULL_VALUE = \"__null__\";\n\nexport function columnId<T>(col: MatrxColumnDef<T>): string {\n if (col.id) return col.id;\n if (col.accessorKey) return String(col.accessorKey);\n throw new Error(\"MatrxDataTable column requires `id` or `accessorKey`\");\n}\n\nexport function getCellValue<T>(row: T, col: MatrxColumnDef<T>): unknown {\n if (col.accessorFn) return col.accessorFn(row);\n if (col.accessorKey) {\n return (row as Record<string, unknown>)[col.accessorKey];\n }\n return undefined;\n}\n\nexport function getFilterValue<T>(row: T, col: MatrxColumnDef<T>): unknown {\n return col.filterValue ? col.filterValue(row) : getCellValue(row, col);\n}\n\nexport function getSortValue<T>(row: T, col: MatrxColumnDef<T>): unknown {\n return col.sortValue ? col.sortValue(row) : getCellValue(row, col);\n}\n\nexport function stringifyCellValue(value: unknown): string {\n if (value == null) return \"\";\n if (typeof value === \"string\") return value;\n if (typeof value === \"number\" || typeof value === \"boolean\") {\n return String(value);\n }\n if (value instanceof Date) return value.toISOString();\n try {\n return JSON.stringify(value);\n } catch {\n return String(value);\n }\n}\n\n/** Unicode-aware tokenization shared by whole-word table search. */\nexport function tableSearchWords(value: string): string[] {\n return value\n .toLocaleLowerCase()\n .split(/[^\\p{L}\\p{N}]+/u)\n .filter(Boolean);\n}\n\n/**\n * Match the primary table search against one row's searchable text. In\n * whole-word mode every entered word must exist as a complete token, so\n * `are` matches `we are ready` but never `hardware`.\n */\nexport function matchesTableSearch(\n searchableText: string,\n query: string,\n mode: TableSearchMatchMode = \"contains\",\n): boolean {\n const normalizedQuery = query.trim().toLocaleLowerCase();\n if (!normalizedQuery) return true;\n if (mode === \"contains\") {\n return searchableText.toLocaleLowerCase().includes(normalizedQuery);\n }\n const actualWords = new Set(tableSearchWords(searchableText));\n const expectedWords = tableSearchWords(normalizedQuery);\n return (\n expectedWords.length > 0 &&\n expectedWords.every((word) => actualWords.has(word))\n );\n}\n\nfunction compareValues(a: unknown, b: unknown): number {\n if (a == null && b == null) return 0;\n if (a == null) return -1;\n if (b == null) return 1;\n if (typeof a === \"number\" && typeof b === \"number\") return a - b;\n if (typeof a === \"boolean\" && typeof b === \"boolean\") {\n return Number(a) - Number(b);\n }\n return stringifyCellValue(a).localeCompare(stringifyCellValue(b), undefined, {\n numeric: true,\n sensitivity: \"base\",\n });\n}\n\n/** Select filters treat array-valued cells as sets, never as serialized JSON choices. */\nexport function selectCellValues(value: unknown): string[] {\n return (Array.isArray(value) ? value : [value])\n .map(stringifyCellValue)\n .filter((item) => item !== \"\");\n}\n\nexport function passesColumnFilter(\n value: unknown,\n filter: ColumnFilterValue,\n): boolean {\n const matches = (() => {\n switch (filter.kind) {\n case \"text\": {\n const s = stringifyCellValue(value);\n if (filter.mode === \"null\") return value == null;\n if (filter.mode === \"not_null\") return value != null;\n if (filter.mode === \"empty\") return s.trim() === \"\";\n if (filter.mode === \"not_empty\") return s.trim() !== \"\";\n const q = filter.value.trim().toLowerCase();\n if (!q) return true;\n return s.toLowerCase().includes(q);\n }\n case \"select\": {\n // `values: []` is an explicit multi-select state: the user deselected\n // every option, so it must produce an empty result. Legacy single-value\n // filters and an absent `values` field retain their clear/all behavior.\n if (filter.values !== undefined && filter.values.length === 0) {\n return false;\n }\n const selected = filter.values ??\n (filter.value && filter.value !== \"__all__\" ? [filter.value] : []);\n if (selected.length === 0) return true;\n const values = selectCellValues(value);\n // OR semantics across the set; sentinels compose (\"A or (empty)\").\n return selected.some((v) =>\n v === SELECT_NULL_VALUE\n ? value == null\n : v === SELECT_EMPTY_VALUE\n ? values.length === 0\n : v === SELECT_NOT_EMPTY_VALUE\n ? values.length > 0\n : v === \"\"\n ? value === \"\" || (Array.isArray(value) && value.includes(\"\"))\n : values.includes(v),\n );\n }\n case \"boolean\": {\n return Boolean(value) === filter.value;\n }\n case \"number\": {\n const n =\n typeof value === \"number\"\n ? value\n : Number(stringifyCellValue(value).replace(/[^0-9.-]/g, \"\"));\n if (Number.isNaN(n)) return false;\n if (filter.min !== undefined && n < filter.min) return false;\n if (filter.max !== undefined && n > filter.max) return false;\n return true;\n }\n }\n })();\n return filter.negated ? !matches : matches;\n}\n\nexport function filterAndSortRows<T>(\n rows: T[],\n columns: MatrxColumnDef<T>[],\n columnFilters: ColumnFiltersState,\n sort: SortState | null,\n globalSearch: string,\n anyOf?: { columnIds: string[]; query: string },\n layeredFilters?: readonly LayeredFilterRule[],\n globalSearchMode: TableSearchMatchMode = \"contains\",\n searchText?: (row: T) => string,\n): T[] {\n const colById = new Map(columns.map((c) => [columnId(c), c]));\n const q = globalSearch.trim().toLowerCase();\n const anyQ = anyOf?.query.trim().toLowerCase() ?? \"\";\n const activeLayeredFilters = completeLayeredFilterRules(layeredFilters);\n\n let result = rows.filter((row) => {\n for (const [id, filter] of Object.entries(columnFilters)) {\n if (!filter) continue;\n const col = colById.get(id);\n if (!col || col.filter === false) continue;\n if (!passesColumnFilter(getFilterValue(row, col), filter)) return false;\n }\n\n for (const rule of activeLayeredFilters) {\n const col = colById.get(rule.field);\n if (!col || !layeredFilterMatchesValue(getFilterValue(row, col), rule)) {\n return false;\n }\n }\n\n if (anyQ && anyOf) {\n const hit = anyOf.columnIds.some((id) => {\n const col = colById.get(id);\n if (!col) return false;\n return stringifyCellValue(getFilterValue(row, col))\n .toLowerCase()\n .includes(anyQ);\n });\n if (!hit) return false;\n }\n\n if (!q) return true;\n const searchableValues = columns\n .filter(\n (col) =>\n col.filter !== false || Boolean(col.accessorKey || col.accessorFn),\n )\n .map((col) => stringifyCellValue(getFilterValue(row, col)));\n const additionalSearchText = searchText?.(row);\n if (additionalSearchText) searchableValues.push(additionalSearchText);\n if (globalSearchMode === \"contains\") {\n return searchableValues.some((value) =>\n matchesTableSearch(value, q, \"contains\"),\n );\n }\n return matchesTableSearch(searchableValues.join(\" \"), q, \"whole_words\");\n });\n\n if (sort) {\n const col = colById.get(sort.id);\n if (col && col.sortable !== false) {\n const dir = sort.direction === \"asc\" ? 1 : -1;\n result = [...result].sort((a, b) => {\n const va = getSortValue(a, col);\n const vb = getSortValue(b, col);\n // Empty cells (null / \"\") sort LAST in both directions — sorting a\n // column should surface its values, never a wall of blanks.\n const ea = va == null || stringifyCellValue(va) === \"\";\n const eb = vb == null || stringifyCellValue(vb) === \"\";\n if (ea !== eb) return ea ? 1 : -1;\n return compareValues(va, vb) * dir;\n });\n }\n }\n\n return result;\n}\n\nexport function countActiveColumnFilters(filters: ColumnFiltersState): number {\n let n = 0;\n for (const f of Object.values(filters)) {\n if (!f) continue;\n switch (f.kind) {\n case \"text\":\n if (f.value.trim() || (f.mode && f.mode !== \"contains\")) n += 1;\n break;\n case \"select\":\n if (f.values !== undefined || (f.value && f.value !== \"__all__\"))\n n += 1;\n break;\n case \"boolean\":\n n += 1;\n break;\n case \"number\":\n if (f.min !== undefined || f.max !== undefined) n += 1;\n break;\n }\n }\n return n;\n}\n\nexport function isColumnFilterActive(\n filter: ColumnFilterValue | undefined,\n): boolean {\n if (!filter) return false;\n switch (filter.kind) {\n case \"text\":\n return (\n filter.value.trim().length > 0 ||\n (filter.mode !== undefined && filter.mode !== \"contains\")\n );\n case \"select\":\n return filter.values !== undefined\n ? true\n : Boolean(filter.value) && filter.value !== \"__all__\";\n case \"boolean\":\n return true;\n case \"number\":\n return filter.min !== undefined || filter.max !== undefined;\n }\n}\n\n/** Apply draft edits onto a row for display (shallow field merge). */\nexport function applyRowEdits<T>(\n row: T,\n edits: Record<string, unknown> | undefined,\n): T {\n if (!edits || Object.keys(edits).length === 0) return row;\n return { ...row, ...edits };\n}\n","import type { ColumnFilterKind, MatrxColumnDef } from \"./types\";\nimport { resolveColumnMarker } from \"./column-marker\";\nimport {\n SELECT_EMPTY_VALUE,\n SELECT_NOT_EMPTY_VALUE,\n SELECT_NULL_VALUE,\n getCellValue,\n selectCellValues,\n} from \"./filter-engine\";\n\n/** Auto-inference switches to text above this many distinct values… */\nconst SELECT_CARDINALITY_CAP = 24;\n\nexport type ResolvedFilterKind = \"text\" | \"select\" | \"boolean\" | \"number\";\n\n/**\n * Infer a filter UI from sample row values. Prefer select for low-cardinality\n * string/enum columns, boolean for booleans, number for numeric, else text.\n */\nexport function resolveFilterKind<T>(\n col: MatrxColumnDef<T>,\n sampleRows: T[],\n): ResolvedFilterKind | null {\n if (col.filter === false) return null;\n if (col.filter && col.filter !== \"auto\") return col.filter;\n if (resolveColumnMarker(col)) return \"boolean\";\n\n const values = sampleRows\n .slice(0, 200)\n .map((row) => getCellValue(row, col))\n .filter((v) => v != null);\n\n if (values.length === 0) return \"text\";\n\n if (values.every((v) => typeof v === \"boolean\")) return \"boolean\";\n if (values.every((v) => typeof v === \"number\" && Number.isFinite(v))) {\n return \"number\";\n }\n\n const strings = values.flatMap(selectCellValues);\n const unique = new Set(strings);\n if (\n unique.size >= 1 &&\n unique.size <= SELECT_CARDINALITY_CAP &&\n unique.size < strings.length * 0.6\n ) {\n return \"select\";\n }\n\n return \"text\";\n}\n\nexport function collectSelectOptions<T>(\n col: MatrxColumnDef<T>,\n rows: T[],\n): Array<{ value: string; label: string }> {\n const seen = new Set<string>();\n const options: Array<{ value: string; label: string }> = [];\n let hasEmpty = false;\n let hasEmptyCollection = false;\n for (const row of rows) {\n const cell = getCellValue(row, col);\n const values = selectCellValues(cell);\n if (values.length === 0) {\n hasEmpty = true;\n if (Array.isArray(cell)) hasEmptyCollection = true;\n continue;\n }\n for (const value of values) {\n if (col.filterOptions?.length || seen.has(value)) continue;\n seen.add(value);\n options.push({ value, label: value });\n }\n }\n const base = col.filterOptions?.length\n ? col.filterOptions.map((option) => ({\n ...option,\n label: option.label.trim() ? option.label : option.value === \"\" ? \"(empty string)\" : option.value.trim() ? option.value : \"(whitespace)\",\n }))\n : options.sort((a, b) => a.label.localeCompare(b.label));\n // Emptiness sentinels: whenever the column has blank cells, every select\n // filter can isolate them — or exclude them — with zero per-table config.\n if (hasEmpty) {\n // An explicit literal-empty choice must remain independently deselectable.\n // The combined empty sentinel would overlap it after All -> uncheck.\n // Empty collections still require that broader option: neither null nor\n // a literal empty string represents an empty array.\n const emptyOption = !hasEmptyCollection && base.some((option) => option.value === \"\")\n ? { value: SELECT_NULL_VALUE, label: \"(null)\" }\n : { value: SELECT_EMPTY_VALUE, label: \"(empty)\" };\n const additions = [];\n if (!base.some((option) => option.value === emptyOption.value)) additions.push(emptyOption);\n if (!base.some((option) => option.value === SELECT_NOT_EMPTY_VALUE)) {\n additions.push({ value: SELECT_NOT_EMPTY_VALUE, label: \"(not empty)\" });\n }\n base.unshift(...additions);\n }\n return base;\n}\n\nexport function filterKindLabel(\n kind: ColumnFilterKind | ResolvedFilterKind,\n): string {\n switch (kind) {\n case \"text\":\n return \"Text\";\n case \"select\":\n return \"Select\";\n case \"boolean\":\n return \"Boolean\";\n case \"number\":\n return \"Number\";\n case \"auto\":\n return \"Auto\";\n case false:\n return \"Off\";\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { H as LAYERED_FILTER_OPERATORS, I as LAYERED_FILTER_OPERATOR_LABELS, J as LayeredFilterField, K as LayeredFilterOperator, L as LayeredFilterRule, N as columnFiltersToLayeredRules, O as completeLayeredFilterRules, P as decodeLayeredFilterRules, Q as encodeLayeredFilterRules, R as isCompleteLayeredFilterRule, U as isLayeredFilterOperator, V as layeredFilterMatchesValue, W as layeredFilterNeedsValue, X as layeredFilterRuleSummary, Y as operatorsForLayeredField } from '../types-
|
|
1
|
+
export { H as LAYERED_FILTER_OPERATORS, I as LAYERED_FILTER_OPERATOR_LABELS, J as LayeredFilterField, K as LayeredFilterOperator, L as LayeredFilterRule, N as columnFiltersToLayeredRules, O as completeLayeredFilterRules, P as decodeLayeredFilterRules, Q as encodeLayeredFilterRules, R as isCompleteLayeredFilterRule, U as isLayeredFilterOperator, V as layeredFilterMatchesValue, W as layeredFilterNeedsValue, X as layeredFilterRuleSummary, Y as operatorsForLayeredField } from '../types-BAJiOaQL.cjs';
|
|
2
2
|
import '../content-transfer.cjs';
|
|
3
3
|
import 'react';
|
|
4
4
|
import '@ai-matrx/kit/content-transfer';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { H as LAYERED_FILTER_OPERATORS, I as LAYERED_FILTER_OPERATOR_LABELS, J as LayeredFilterField, K as LayeredFilterOperator, L as LayeredFilterRule, N as columnFiltersToLayeredRules, O as completeLayeredFilterRules, P as decodeLayeredFilterRules, Q as encodeLayeredFilterRules, R as isCompleteLayeredFilterRule, U as isLayeredFilterOperator, V as layeredFilterMatchesValue, W as layeredFilterNeedsValue, X as layeredFilterRuleSummary, Y as operatorsForLayeredField } from '../types-
|
|
1
|
+
export { H as LAYERED_FILTER_OPERATORS, I as LAYERED_FILTER_OPERATOR_LABELS, J as LayeredFilterField, K as LayeredFilterOperator, L as LayeredFilterRule, N as columnFiltersToLayeredRules, O as completeLayeredFilterRules, P as decodeLayeredFilterRules, Q as encodeLayeredFilterRules, R as isCompleteLayeredFilterRule, U as isLayeredFilterOperator, V as layeredFilterMatchesValue, W as layeredFilterNeedsValue, X as layeredFilterRuleSummary, Y as operatorsForLayeredField } from '../types-Da781-Wq.js';
|
|
2
2
|
import '../content-transfer.js';
|
|
3
3
|
import 'react';
|
|
4
4
|
import '@ai-matrx/kit/content-transfer';
|
|
@@ -451,6 +451,7 @@ function PageNavigationControl({
|
|
|
451
451
|
}
|
|
452
452
|
function PaginationAction({
|
|
453
453
|
HostAction,
|
|
454
|
+
compact = false,
|
|
454
455
|
active = false,
|
|
455
456
|
disabledHint,
|
|
456
457
|
visibilityClassName = "inline-flex",
|
|
@@ -479,6 +480,7 @@ function PaginationAction({
|
|
|
479
480
|
onClick: props.onClick,
|
|
480
481
|
className: cn(
|
|
481
482
|
PAGINATION_ACTION_CLASS,
|
|
483
|
+
compact && "h-8 w-8 min-h-8 min-w-8",
|
|
482
484
|
active && PAGINATION_ACTIVE_CLASS,
|
|
483
485
|
fallbackClassName
|
|
484
486
|
),
|
|
@@ -492,6 +494,8 @@ var PAGINATION_ACTION_CLASS = "h-11 w-11 min-h-11 min-w-11 rounded-md border bor
|
|
|
492
494
|
var PAGINATION_ACTIVE_CLASS = "border-primary bg-primary text-primary-foreground hover:bg-primary/90";
|
|
493
495
|
function GenericTablePagination({
|
|
494
496
|
totalItems,
|
|
497
|
+
unknownTotal,
|
|
498
|
+
isFetching = false,
|
|
495
499
|
itemsPerPage,
|
|
496
500
|
currentPage,
|
|
497
501
|
onPageChange,
|
|
@@ -532,11 +536,12 @@ function GenericTablePagination({
|
|
|
532
536
|
(_, i) => startPage + i
|
|
533
537
|
);
|
|
534
538
|
};
|
|
535
|
-
const isFirstPageDisabled = currentPage <= 1;
|
|
536
|
-
const isLastPageDisabled = currentPage >= totalPages;
|
|
539
|
+
const isFirstPageDisabled = isFetching || currentPage <= 1;
|
|
540
|
+
const isLastPageDisabled = isFetching || (unknownTotal ? !unknownTotal.hasNextPage : currentPage >= totalPages);
|
|
537
541
|
const startItem = totalItems === 0 ? 0 : progressive ? 1 : (currentPage - 1) * effectiveItemsPerPage + 1;
|
|
538
542
|
const endItem = Math.min(progressive?.shownCount ?? currentPage * effectiveItemsPerPage, totalItems);
|
|
539
543
|
const defaultLabelFormat = (start, end, total) => {
|
|
544
|
+
if (unknownTotal) return `${unknownTotal.pageRowCount.toLocaleString()} on page ${currentPage.toLocaleString()}`;
|
|
540
545
|
if (progressive) return progressive.loadedCount !== void 0 && progressive.loadedCount !== total ? `${end} shown / ${total} matching \xB7 ${progressive.loadedCount} loaded` : `${end} shown / ${total} loaded`;
|
|
541
546
|
return compact ? `${start}-${end} of ${total}` : `Showing ${start} to ${end} of ${total} entries`;
|
|
542
547
|
};
|
|
@@ -561,6 +566,7 @@ function GenericTablePagination({
|
|
|
561
566
|
children: /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
|
|
562
567
|
Select,
|
|
563
568
|
{
|
|
569
|
+
disabled: isFetching,
|
|
564
570
|
value: itemsPerPage.toString(),
|
|
565
571
|
onValueChange: (value) => onItemsPerPageChange(parseInt(value)),
|
|
566
572
|
children: [
|
|
@@ -577,7 +583,7 @@ function GenericTablePagination({
|
|
|
577
583
|
),
|
|
578
584
|
/* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(SelectContent, { children: [
|
|
579
585
|
availablePageSizes.map((size) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(SelectItem, { value: size.toString(), children: size }, size)),
|
|
580
|
-
showAllOption ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(SelectItem, { value: allValue.toString(), children: allOptionLabel }) : null
|
|
586
|
+
showAllOption && !unknownTotal ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(SelectItem, { value: allValue.toString(), children: allOptionLabel }) : null
|
|
581
587
|
] })
|
|
582
588
|
]
|
|
583
589
|
}
|
|
@@ -589,7 +595,7 @@ function GenericTablePagination({
|
|
|
589
595
|
{
|
|
590
596
|
className: cn("flex min-w-0 justify-center", infoContainerClassName),
|
|
591
597
|
children: showPageInfo && !hideEntriesInfo && /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_jsx_runtime8.Fragment, { children: [
|
|
592
|
-
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "truncate text-xs tabular-nums text-muted-foreground sm:hidden", children: progressive ? `${endItem}/${totalItems} ${progressive.loadedCount !== void 0 && progressive.loadedCount !== totalItems ? "matching" : "loaded"}` : `${startItem}-${endItem} / ${totalItems}` }),
|
|
598
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "truncate text-xs tabular-nums text-muted-foreground sm:hidden", children: labelFormat ? formatLabel(startItem, endItem, totalItems) : unknownTotal ? defaultLabelFormat(startItem, endItem, totalItems) : progressive ? `${endItem}/${totalItems} ${progressive.loadedCount !== void 0 && progressive.loadedCount !== totalItems ? "matching" : "loaded"}` : `${startItem}-${endItem} / ${totalItems}` }),
|
|
593
599
|
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
|
|
594
600
|
"span",
|
|
595
601
|
{
|
|
@@ -614,7 +620,7 @@ function GenericTablePagination({
|
|
|
614
620
|
}
|
|
615
621
|
) }),
|
|
616
622
|
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)(TooltipContent, { children: "Show more already-loaded rows" })
|
|
617
|
-
] }) : null : showPageControls && /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
|
|
623
|
+
] }) : null : showPageControls && (unknownTotal ? currentPage > 1 || unknownTotal.hasNextPage : totalPages > 1) && /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
|
|
618
624
|
"div",
|
|
619
625
|
{
|
|
620
626
|
className: cn(
|
|
@@ -627,8 +633,9 @@ function GenericTablePagination({
|
|
|
627
633
|
PaginationAction,
|
|
628
634
|
{
|
|
629
635
|
HostAction: host.ToolbarAction,
|
|
636
|
+
compact,
|
|
630
637
|
ariaLabel: "First page",
|
|
631
|
-
disabledHint: "Already on the first page",
|
|
638
|
+
disabledHint: isFetching ? "Loading source page" : "Already on the first page",
|
|
632
639
|
visibilityClassName: "hidden sm:inline-flex",
|
|
633
640
|
fallbackClassName: navButtonClassName,
|
|
634
641
|
onClick: () => onPageChange(1),
|
|
@@ -640,41 +647,46 @@ function GenericTablePagination({
|
|
|
640
647
|
PaginationAction,
|
|
641
648
|
{
|
|
642
649
|
HostAction: host.ToolbarAction,
|
|
650
|
+
compact,
|
|
643
651
|
ariaLabel: "Previous page",
|
|
644
|
-
disabledHint: "Already on the first page",
|
|
652
|
+
disabledHint: isFetching ? "Loading source page" : "Already on the first page",
|
|
645
653
|
fallbackClassName: navButtonClassName,
|
|
646
654
|
onClick: () => onPageChange(currentPage - 1),
|
|
647
655
|
disabled: isFirstPageDisabled,
|
|
648
656
|
children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(ChevronLeft, { className: "h-5 w-5" })
|
|
649
657
|
}
|
|
650
658
|
),
|
|
651
|
-
getPageNumbers(3).map((page) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
|
|
659
|
+
!unknownTotal && getPageNumbers(3).map((page) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
|
|
652
660
|
PaginationAction,
|
|
653
661
|
{
|
|
654
662
|
HostAction: host.ToolbarAction,
|
|
663
|
+
compact,
|
|
655
664
|
ariaLabel: `Page ${page}`,
|
|
656
665
|
active: currentPage === page,
|
|
657
|
-
disabledHint: `Already on page ${page}`,
|
|
666
|
+
disabledHint: isFetching ? "Loading source page" : `Already on page ${page}`,
|
|
658
667
|
visibilityClassName: cn(
|
|
659
668
|
"sm:hidden",
|
|
660
669
|
page === currentPage ? "inline-flex" : "hidden min-[360px]:inline-flex"
|
|
661
670
|
),
|
|
662
671
|
fallbackClassName: page === currentPage ? pageActiveButtonClassName : pageButtonClassName,
|
|
663
672
|
onClick: () => onPageChange(page),
|
|
673
|
+
disabled: isFetching,
|
|
664
674
|
children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { children: page })
|
|
665
675
|
},
|
|
666
676
|
`mobile-${page}`
|
|
667
677
|
)),
|
|
668
|
-
getPageNumbers().map((page) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
|
|
678
|
+
!unknownTotal && getPageNumbers().map((page) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
|
|
669
679
|
PaginationAction,
|
|
670
680
|
{
|
|
671
681
|
HostAction: host.ToolbarAction,
|
|
682
|
+
compact,
|
|
672
683
|
ariaLabel: `Page ${page}`,
|
|
673
684
|
active: currentPage === page,
|
|
674
|
-
disabledHint: `Already on page ${page}`,
|
|
685
|
+
disabledHint: isFetching ? "Loading source page" : `Already on page ${page}`,
|
|
675
686
|
visibilityClassName: "hidden sm:inline-flex",
|
|
676
687
|
fallbackClassName: currentPage === page ? pageActiveButtonClassName : pageButtonClassName,
|
|
677
688
|
onClick: () => onPageChange(page),
|
|
689
|
+
disabled: isFetching,
|
|
678
690
|
children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { children: page })
|
|
679
691
|
},
|
|
680
692
|
`desktop-${page}`
|
|
@@ -683,20 +695,22 @@ function GenericTablePagination({
|
|
|
683
695
|
PaginationAction,
|
|
684
696
|
{
|
|
685
697
|
HostAction: host.ToolbarAction,
|
|
698
|
+
compact,
|
|
686
699
|
ariaLabel: "Next page",
|
|
687
|
-
disabledHint: "Already on the last page",
|
|
700
|
+
disabledHint: isFetching ? "Loading source page" : "Already on the last page",
|
|
688
701
|
fallbackClassName: navButtonClassName,
|
|
689
702
|
onClick: () => onPageChange(currentPage + 1),
|
|
690
703
|
disabled: isLastPageDisabled,
|
|
691
704
|
children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(ChevronRight, { className: "h-5 w-5" })
|
|
692
705
|
}
|
|
693
706
|
),
|
|
694
|
-
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
|
|
707
|
+
!unknownTotal && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
|
|
695
708
|
PaginationAction,
|
|
696
709
|
{
|
|
697
710
|
HostAction: host.ToolbarAction,
|
|
711
|
+
compact,
|
|
698
712
|
ariaLabel: "Last page",
|
|
699
|
-
disabledHint: "Already on the last page",
|
|
713
|
+
disabledHint: isFetching ? "Loading source page" : "Already on the last page",
|
|
700
714
|
visibilityClassName: "hidden sm:inline-flex",
|
|
701
715
|
fallbackClassName: navButtonClassName,
|
|
702
716
|
onClick: () => onPageChange(totalPages),
|