@ai-matrx/design-system 0.21.13 → 0.21.14
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 +14 -0
- package/dist/content-transfer.cjs +1 -0
- package/dist/content-transfer.cjs.map +1 -1
- package/dist/content-transfer.js +1 -0
- package/dist/content-transfer.js.map +1 -1
- 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 +1 -0
- package/dist/data-table/data-row-inspector.cjs.map +1 -1
- package/dist/data-table/data-row-inspector.js +1 -0
- package/dist/data-table/data-row-inspector.js.map +1 -1
- package/dist/data-table/data-row-window.cjs +1 -0
- package/dist/data-table/data-row-window.cjs.map +1 -1
- package/dist/data-table/data-row-window.js +1 -0
- package/dist/data-table/data-row-window.js.map +1 -1
- package/dist/data-table/filter-engine.cjs +6 -2
- package/dist/data-table/filter-engine.cjs.map +1 -1
- package/dist/data-table/filter-engine.d.cts +1 -1
- package/dist/data-table/filter-engine.d.ts +1 -1
- package/dist/data-table/filter-engine.js +6 -2
- 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 +31 -36
- 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 +31 -36
- package/dist/data-table/index.js.map +1 -1
- 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.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/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.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-DaKPLM6T.d.cts → types-BgqIKbhq.d.cts} +12 -1
- package/dist/{types-D9b_LfTB.d.ts → types-emLxze_m.d.ts} +12 -1
- package/package.json +1 -1
|
@@ -216,8 +216,12 @@ function passesColumnFilter(value, filter) {
|
|
|
216
216
|
return Boolean(value) === filter.value;
|
|
217
217
|
}
|
|
218
218
|
case "number": {
|
|
219
|
-
const
|
|
220
|
-
if (
|
|
219
|
+
const raw = stringifyCellValue(value).trim();
|
|
220
|
+
if (raw === "") return false;
|
|
221
|
+
const normalized = raw.replace(/[^0-9.-]/g, "");
|
|
222
|
+
if (normalized === "") return false;
|
|
223
|
+
const n = typeof value === "number" ? value : Number(normalized);
|
|
224
|
+
if (!Number.isFinite(n)) return false;
|
|
221
225
|
if (filter.min !== void 0 && n < filter.min) return false;
|
|
222
226
|
if (filter.max !== void 0 && n > filter.max) return false;
|
|
223
227
|
return true;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/data-table/filter-engine.ts","../../src/data-table/layered-filters.ts"],"sourcesContent":["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","/**\n * Shared vocabulary for ordered, AND-combined table filters. The same rule\n * shape is URL-safe, can be evaluated by local tables, and can be handed to a\n * controlled table's server query without a surface inventing another query\n * builder.\n */\n\nimport { z } from \"zod\";\nimport type { ColumnFiltersState } from \"./types\";\n\nexport const LAYERED_FILTER_OPERATORS = [\n \"contains\",\n \"not_contains\",\n \"equals\",\n \"not_equals\",\n \"starts_with\",\n \"ends_with\",\n \"word\",\n \"not_word\",\n \"is_empty\",\n \"is_not_empty\",\n \"greater_than\",\n \"greater_or_equal\",\n \"less_than\",\n \"less_or_equal\",\n \"between\",\n] as const;\n\nexport type LayeredFilterOperator = (typeof LAYERED_FILTER_OPERATORS)[number];\n\nexport interface LayeredFilterRule {\n id: string;\n field: string;\n operator: LayeredFilterOperator;\n value: string;\n valueTo?: string | undefined;\n}\n\nexport type LayeredFilterField =\n | {\n id: string;\n label: string;\n kind: \"text\";\n operators?: LayeredFilterOperator[];\n }\n | {\n id: string;\n label: string;\n kind: \"number\";\n operators?: LayeredFilterOperator[];\n placeholder?: string;\n }\n | {\n id: string;\n label: string;\n kind: \"select\";\n operators?: LayeredFilterOperator[];\n options: Array<{ value: string; label: string }>;\n };\n\nconst layeredFilterRuleSchema = z.object({\n id: z.string().min(1),\n field: z.string().min(1),\n operator: z.enum(LAYERED_FILTER_OPERATORS),\n value: z.string(),\n valueTo: z.string().optional(),\n});\n\nconst layeredFilterRulesSchema = z.array(layeredFilterRuleSchema).max(20);\n\nconst TEXT_OPERATORS: LayeredFilterOperator[] = [\n \"contains\",\n \"not_contains\",\n \"equals\",\n \"not_equals\",\n \"starts_with\",\n \"ends_with\",\n \"word\",\n \"not_word\",\n \"is_empty\",\n \"is_not_empty\",\n];\n\nconst NUMBER_OPERATORS: LayeredFilterOperator[] = [\n \"equals\",\n \"not_equals\",\n \"greater_than\",\n \"greater_or_equal\",\n \"less_than\",\n \"less_or_equal\",\n \"between\",\n \"is_empty\",\n \"is_not_empty\",\n];\n\nconst SELECT_OPERATORS: LayeredFilterOperator[] = [\n \"equals\",\n \"not_equals\",\n \"is_empty\",\n \"is_not_empty\",\n];\n\nexport const LAYERED_FILTER_OPERATOR_LABELS: Record<\n LayeredFilterOperator,\n string\n> = {\n contains: \"contains\",\n not_contains: \"doesn't contain\",\n equals: \"is\",\n not_equals: \"is not\",\n starts_with: \"starts with\",\n ends_with: \"ends with\",\n word: \"has the word\",\n not_word: \"doesn't have the word\",\n is_empty: \"is empty\",\n is_not_empty: \"is not empty\",\n greater_than: \"is greater than\",\n greater_or_equal: \"is at least\",\n less_than: \"is less than\",\n less_or_equal: \"is at most\",\n between: \"is between\",\n};\n\nexport function operatorsForLayeredField(\n field: LayeredFilterField,\n): LayeredFilterOperator[] {\n if (field.operators) return field.operators;\n if (field.kind === \"number\") return NUMBER_OPERATORS;\n if (field.kind === \"select\") return SELECT_OPERATORS;\n return TEXT_OPERATORS;\n}\n\nexport function layeredFilterNeedsValue(\n operator: LayeredFilterOperator,\n): boolean {\n return operator !== \"is_empty\" && operator !== \"is_not_empty\";\n}\n\nexport function isCompleteLayeredFilterRule(rule: LayeredFilterRule): boolean {\n if (!layeredFilterNeedsValue(rule.operator)) return true;\n if (rule.value.trim() === \"\") return false;\n return rule.operator !== \"between\" || Boolean(rule.valueTo?.trim());\n}\n\nexport function completeLayeredFilterRules(\n rules: readonly LayeredFilterRule[] | undefined,\n): LayeredFilterRule[] {\n return (rules ?? []).filter(isCompleteLayeredFilterRule);\n}\n\nexport function isLayeredFilterOperator(\n value: string,\n): value is LayeredFilterOperator {\n return LAYERED_FILTER_OPERATORS.some((operator) => operator === value);\n}\n\n/** Decode URL-owned rules without trusting a hand-edited link. */\nexport function decodeLayeredFilterRules(\n raw: string | null,\n): LayeredFilterRule[] {\n if (!raw) return [];\n try {\n const parsed: unknown = JSON.parse(raw);\n const result = layeredFilterRulesSchema.safeParse(parsed);\n return result.success ? result.data : [];\n } catch {\n return [];\n }\n}\n\n/** Stable compact URL/wire form; rule order is preserved. */\nexport function encodeLayeredFilterRules(\n rules: readonly LayeredFilterRule[] | undefined,\n): string | null {\n return rules?.length ? JSON.stringify(rules) : null;\n}\n\n/**\n * Translate ordinary per-column controls into the same ordered rule language\n * used by the advanced builder. Controlled tables can send these rules to one\n * server evaluator instead of rebuilding numeric/text filter semantics.\n * Select filters are intentionally omitted because their multi-select control\n * is OR-combined and should stay in its native query parameter.\n */\nexport function columnFiltersToLayeredRules(\n filters: ColumnFiltersState,\n fieldIds: readonly string[],\n): LayeredFilterRule[] {\n const rules: LayeredFilterRule[] = [];\n for (const field of fieldIds) {\n const filter = filters[field];\n if (!filter) continue;\n\n if (filter.kind === \"text\") {\n rules.push({\n id: `column-${field}`,\n field,\n operator:\n filter.mode === \"empty\"\n ? \"is_empty\"\n : filter.mode === \"not_empty\"\n ? \"is_not_empty\"\n : \"contains\",\n value: filter.value,\n });\n continue;\n }\n\n if (filter.kind === \"number\") {\n if (filter.min !== undefined && filter.max !== undefined) {\n rules.push({\n id: `column-${field}`,\n field,\n operator: \"between\",\n value: String(filter.min),\n valueTo: String(filter.max),\n });\n } else if (filter.min !== undefined) {\n rules.push({\n id: `column-${field}`,\n field,\n operator: \"greater_or_equal\",\n value: String(filter.min),\n });\n } else if (filter.max !== undefined) {\n rules.push({\n id: `column-${field}`,\n field,\n operator: \"less_or_equal\",\n value: String(filter.max),\n });\n }\n continue;\n }\n\n if (filter.kind === \"boolean\") {\n rules.push({\n id: `column-${field}`,\n field,\n operator: \"equals\",\n value: String(filter.value),\n });\n }\n }\n return rules;\n}\n\nfunction normalizedText(value: unknown): string {\n if (value === null || value === undefined) return \"\";\n return String(value).trim().toLocaleLowerCase();\n}\n\nfunction words(value: string): string[] {\n return value.split(/[^\\p{L}\\p{N}]+/u).filter(Boolean);\n}\n\n/** Local-table evaluator. Controlled tables send the same rule shape server-side. */\nexport function layeredFilterMatchesValue(\n value: unknown,\n rule: LayeredFilterRule,\n): boolean {\n const actual = normalizedText(value);\n const expected = rule.value.trim().toLocaleLowerCase();\n const expectedTo = rule.valueTo?.trim().toLocaleLowerCase() ?? \"\";\n\n switch (rule.operator) {\n case \"contains\":\n return actual.includes(expected);\n case \"not_contains\":\n return !actual.includes(expected);\n case \"equals\":\n return actual === expected;\n case \"not_equals\":\n return actual !== expected;\n case \"starts_with\":\n return actual.startsWith(expected);\n case \"ends_with\":\n return actual.endsWith(expected);\n case \"word\":\n return words(actual).includes(expected);\n case \"not_word\":\n return !words(actual).includes(expected);\n case \"is_empty\":\n return actual === \"\";\n case \"is_not_empty\":\n return actual !== \"\";\n case \"greater_than\":\n return Number(actual) > Number(expected);\n case \"greater_or_equal\":\n return Number(actual) >= Number(expected);\n case \"less_than\":\n return Number(actual) < Number(expected);\n case \"less_or_equal\":\n return Number(actual) <= Number(expected);\n case \"between\": {\n const numeric = Number(actual);\n return numeric >= Number(expected) && numeric <= Number(expectedTo);\n }\n }\n}\n\nexport function layeredFilterRuleSummary(\n rule: LayeredFilterRule,\n fields: readonly LayeredFilterField[],\n): string {\n const field = fields.find((candidate) => candidate.id === rule.field);\n const fieldLabel = field?.label ?? rule.field;\n const operator = LAYERED_FILTER_OPERATOR_LABELS[rule.operator];\n if (!layeredFilterNeedsValue(rule.operator))\n return `${fieldLabel} ${operator}`;\n const selectedLabel =\n field?.kind === \"select\"\n ? field.options.find((option) => option.value === rule.value)?.label\n : undefined;\n const value = selectedLabel ?? rule.value;\n return rule.operator === \"between\"\n ? `${fieldLabel} ${operator} ${value} and ${rule.valueTo ?? \"\"}`\n : `${fieldLabel} ${operator} ${value}`;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/data-table/filter-engine.ts","../../src/data-table/layered-filters.ts"],"sourcesContent":["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 // A missing cell is not zero. Without this guard, Number(\"\") made a\n // blank numeric value pass filters whose range included zero.\n const raw = stringifyCellValue(value).trim();\n if (raw === \"\") return false;\n const normalized = raw.replace(/[^0-9.-]/g, \"\");\n if (normalized === \"\") return false;\n const n = typeof value === \"number\"\n ? value\n : Number(normalized);\n if (!Number.isFinite(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","/**\n * Shared vocabulary for ordered, AND-combined table filters. The same rule\n * shape is URL-safe, can be evaluated by local tables, and can be handed to a\n * controlled table's server query without a surface inventing another query\n * builder.\n */\n\nimport { z } from \"zod\";\nimport type { ColumnFiltersState } from \"./types\";\n\nexport const LAYERED_FILTER_OPERATORS = [\n \"contains\",\n \"not_contains\",\n \"equals\",\n \"not_equals\",\n \"starts_with\",\n \"ends_with\",\n \"word\",\n \"not_word\",\n \"is_empty\",\n \"is_not_empty\",\n \"greater_than\",\n \"greater_or_equal\",\n \"less_than\",\n \"less_or_equal\",\n \"between\",\n] as const;\n\nexport type LayeredFilterOperator = (typeof LAYERED_FILTER_OPERATORS)[number];\n\nexport interface LayeredFilterRule {\n id: string;\n field: string;\n operator: LayeredFilterOperator;\n value: string;\n valueTo?: string | undefined;\n}\n\nexport type LayeredFilterField =\n | {\n id: string;\n label: string;\n kind: \"text\";\n operators?: LayeredFilterOperator[];\n }\n | {\n id: string;\n label: string;\n kind: \"number\";\n operators?: LayeredFilterOperator[];\n placeholder?: string;\n }\n | {\n id: string;\n label: string;\n kind: \"select\";\n operators?: LayeredFilterOperator[];\n options: Array<{ value: string; label: string }>;\n };\n\nconst layeredFilterRuleSchema = z.object({\n id: z.string().min(1),\n field: z.string().min(1),\n operator: z.enum(LAYERED_FILTER_OPERATORS),\n value: z.string(),\n valueTo: z.string().optional(),\n});\n\nconst layeredFilterRulesSchema = z.array(layeredFilterRuleSchema).max(20);\n\nconst TEXT_OPERATORS: LayeredFilterOperator[] = [\n \"contains\",\n \"not_contains\",\n \"equals\",\n \"not_equals\",\n \"starts_with\",\n \"ends_with\",\n \"word\",\n \"not_word\",\n \"is_empty\",\n \"is_not_empty\",\n];\n\nconst NUMBER_OPERATORS: LayeredFilterOperator[] = [\n \"equals\",\n \"not_equals\",\n \"greater_than\",\n \"greater_or_equal\",\n \"less_than\",\n \"less_or_equal\",\n \"between\",\n \"is_empty\",\n \"is_not_empty\",\n];\n\nconst SELECT_OPERATORS: LayeredFilterOperator[] = [\n \"equals\",\n \"not_equals\",\n \"is_empty\",\n \"is_not_empty\",\n];\n\nexport const LAYERED_FILTER_OPERATOR_LABELS: Record<\n LayeredFilterOperator,\n string\n> = {\n contains: \"contains\",\n not_contains: \"doesn't contain\",\n equals: \"is\",\n not_equals: \"is not\",\n starts_with: \"starts with\",\n ends_with: \"ends with\",\n word: \"has the word\",\n not_word: \"doesn't have the word\",\n is_empty: \"is empty\",\n is_not_empty: \"is not empty\",\n greater_than: \"is greater than\",\n greater_or_equal: \"is at least\",\n less_than: \"is less than\",\n less_or_equal: \"is at most\",\n between: \"is between\",\n};\n\nexport function operatorsForLayeredField(\n field: LayeredFilterField,\n): LayeredFilterOperator[] {\n if (field.operators) return field.operators;\n if (field.kind === \"number\") return NUMBER_OPERATORS;\n if (field.kind === \"select\") return SELECT_OPERATORS;\n return TEXT_OPERATORS;\n}\n\nexport function layeredFilterNeedsValue(\n operator: LayeredFilterOperator,\n): boolean {\n return operator !== \"is_empty\" && operator !== \"is_not_empty\";\n}\n\nexport function isCompleteLayeredFilterRule(rule: LayeredFilterRule): boolean {\n if (!layeredFilterNeedsValue(rule.operator)) return true;\n if (rule.value.trim() === \"\") return false;\n return rule.operator !== \"between\" || Boolean(rule.valueTo?.trim());\n}\n\nexport function completeLayeredFilterRules(\n rules: readonly LayeredFilterRule[] | undefined,\n): LayeredFilterRule[] {\n return (rules ?? []).filter(isCompleteLayeredFilterRule);\n}\n\nexport function isLayeredFilterOperator(\n value: string,\n): value is LayeredFilterOperator {\n return LAYERED_FILTER_OPERATORS.some((operator) => operator === value);\n}\n\n/** Decode URL-owned rules without trusting a hand-edited link. */\nexport function decodeLayeredFilterRules(\n raw: string | null,\n): LayeredFilterRule[] {\n if (!raw) return [];\n try {\n const parsed: unknown = JSON.parse(raw);\n const result = layeredFilterRulesSchema.safeParse(parsed);\n return result.success ? result.data : [];\n } catch {\n return [];\n }\n}\n\n/** Stable compact URL/wire form; rule order is preserved. */\nexport function encodeLayeredFilterRules(\n rules: readonly LayeredFilterRule[] | undefined,\n): string | null {\n return rules?.length ? JSON.stringify(rules) : null;\n}\n\n/**\n * Translate ordinary per-column controls into the same ordered rule language\n * used by the advanced builder. Controlled tables can send these rules to one\n * server evaluator instead of rebuilding numeric/text filter semantics.\n * Select filters are intentionally omitted because their multi-select control\n * is OR-combined and should stay in its native query parameter.\n */\nexport function columnFiltersToLayeredRules(\n filters: ColumnFiltersState,\n fieldIds: readonly string[],\n): LayeredFilterRule[] {\n const rules: LayeredFilterRule[] = [];\n for (const field of fieldIds) {\n const filter = filters[field];\n if (!filter) continue;\n\n if (filter.kind === \"text\") {\n rules.push({\n id: `column-${field}`,\n field,\n operator:\n filter.mode === \"empty\"\n ? \"is_empty\"\n : filter.mode === \"not_empty\"\n ? \"is_not_empty\"\n : \"contains\",\n value: filter.value,\n });\n continue;\n }\n\n if (filter.kind === \"number\") {\n if (filter.min !== undefined && filter.max !== undefined) {\n rules.push({\n id: `column-${field}`,\n field,\n operator: \"between\",\n value: String(filter.min),\n valueTo: String(filter.max),\n });\n } else if (filter.min !== undefined) {\n rules.push({\n id: `column-${field}`,\n field,\n operator: \"greater_or_equal\",\n value: String(filter.min),\n });\n } else if (filter.max !== undefined) {\n rules.push({\n id: `column-${field}`,\n field,\n operator: \"less_or_equal\",\n value: String(filter.max),\n });\n }\n continue;\n }\n\n if (filter.kind === \"boolean\") {\n rules.push({\n id: `column-${field}`,\n field,\n operator: \"equals\",\n value: String(filter.value),\n });\n }\n }\n return rules;\n}\n\nfunction normalizedText(value: unknown): string {\n if (value === null || value === undefined) return \"\";\n return String(value).trim().toLocaleLowerCase();\n}\n\nfunction words(value: string): string[] {\n return value.split(/[^\\p{L}\\p{N}]+/u).filter(Boolean);\n}\n\n/** Local-table evaluator. Controlled tables send the same rule shape server-side. */\nexport function layeredFilterMatchesValue(\n value: unknown,\n rule: LayeredFilterRule,\n): boolean {\n const actual = normalizedText(value);\n const expected = rule.value.trim().toLocaleLowerCase();\n const expectedTo = rule.valueTo?.trim().toLocaleLowerCase() ?? \"\";\n\n switch (rule.operator) {\n case \"contains\":\n return actual.includes(expected);\n case \"not_contains\":\n return !actual.includes(expected);\n case \"equals\":\n return actual === expected;\n case \"not_equals\":\n return actual !== expected;\n case \"starts_with\":\n return actual.startsWith(expected);\n case \"ends_with\":\n return actual.endsWith(expected);\n case \"word\":\n return words(actual).includes(expected);\n case \"not_word\":\n return !words(actual).includes(expected);\n case \"is_empty\":\n return actual === \"\";\n case \"is_not_empty\":\n return actual !== \"\";\n case \"greater_than\":\n return Number(actual) > Number(expected);\n case \"greater_or_equal\":\n return Number(actual) >= Number(expected);\n case \"less_than\":\n return Number(actual) < Number(expected);\n case \"less_or_equal\":\n return Number(actual) <= Number(expected);\n case \"between\": {\n const numeric = Number(actual);\n return numeric >= Number(expected) && numeric <= Number(expectedTo);\n }\n }\n}\n\nexport function layeredFilterRuleSummary(\n rule: LayeredFilterRule,\n fields: readonly LayeredFilterField[],\n): string {\n const field = fields.find((candidate) => candidate.id === rule.field);\n const fieldLabel = field?.label ?? rule.field;\n const operator = LAYERED_FILTER_OPERATOR_LABELS[rule.operator];\n if (!layeredFilterNeedsValue(rule.operator))\n return `${fieldLabel} ${operator}`;\n const selectedLabel =\n field?.kind === \"select\"\n ? field.options.find((option) => option.value === rule.value)?.label\n : undefined;\n const value = selectedLabel ?? rule.value;\n return rule.operator === \"between\"\n ? `${fieldLabel} ${operator} ${value} and ${rule.valueTo ?? \"\"}`\n : `${fieldLabel} ${operator} ${value}`;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { M as MatrxColumnDef, C as ColumnFiltersState, S as SortState, L as LayeredFilterRule, T as TableSearchMatchMode, a as ColumnFilterValue } from '../types-
|
|
1
|
+
import { M as MatrxColumnDef, C as ColumnFiltersState, S as SortState, L as LayeredFilterRule, T as TableSearchMatchMode, a as ColumnFilterValue } from '../types-BgqIKbhq.cjs';
|
|
2
2
|
import '../content-transfer.cjs';
|
|
3
3
|
import 'react';
|
|
4
4
|
import '@ai-matrx/kit/content-transfer';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { M as MatrxColumnDef, C as ColumnFiltersState, S as SortState, L as LayeredFilterRule, T as TableSearchMatchMode, a as ColumnFilterValue } from '../types-
|
|
1
|
+
import { M as MatrxColumnDef, C as ColumnFiltersState, S as SortState, L as LayeredFilterRule, T as TableSearchMatchMode, a as ColumnFilterValue } from '../types-emLxze_m.js';
|
|
2
2
|
import '../content-transfer.js';
|
|
3
3
|
import 'react';
|
|
4
4
|
import '@ai-matrx/kit/content-transfer';
|
|
@@ -176,8 +176,12 @@ function passesColumnFilter(value, filter) {
|
|
|
176
176
|
return Boolean(value) === filter.value;
|
|
177
177
|
}
|
|
178
178
|
case "number": {
|
|
179
|
-
const
|
|
180
|
-
if (
|
|
179
|
+
const raw = stringifyCellValue(value).trim();
|
|
180
|
+
if (raw === "") return false;
|
|
181
|
+
const normalized = raw.replace(/[^0-9.-]/g, "");
|
|
182
|
+
if (normalized === "") return false;
|
|
183
|
+
const n = typeof value === "number" ? value : Number(normalized);
|
|
184
|
+
if (!Number.isFinite(n)) return false;
|
|
181
185
|
if (filter.min !== void 0 && n < filter.min) return false;
|
|
182
186
|
if (filter.max !== void 0 && n > filter.max) return false;
|
|
183
187
|
return true;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/data-table/layered-filters.ts","../../src/data-table/filter-engine.ts"],"sourcesContent":["/**\n * Shared vocabulary for ordered, AND-combined table filters. The same rule\n * shape is URL-safe, can be evaluated by local tables, and can be handed to a\n * controlled table's server query without a surface inventing another query\n * builder.\n */\n\nimport { z } from \"zod\";\nimport type { ColumnFiltersState } from \"./types\";\n\nexport const LAYERED_FILTER_OPERATORS = [\n \"contains\",\n \"not_contains\",\n \"equals\",\n \"not_equals\",\n \"starts_with\",\n \"ends_with\",\n \"word\",\n \"not_word\",\n \"is_empty\",\n \"is_not_empty\",\n \"greater_than\",\n \"greater_or_equal\",\n \"less_than\",\n \"less_or_equal\",\n \"between\",\n] as const;\n\nexport type LayeredFilterOperator = (typeof LAYERED_FILTER_OPERATORS)[number];\n\nexport interface LayeredFilterRule {\n id: string;\n field: string;\n operator: LayeredFilterOperator;\n value: string;\n valueTo?: string | undefined;\n}\n\nexport type LayeredFilterField =\n | {\n id: string;\n label: string;\n kind: \"text\";\n operators?: LayeredFilterOperator[];\n }\n | {\n id: string;\n label: string;\n kind: \"number\";\n operators?: LayeredFilterOperator[];\n placeholder?: string;\n }\n | {\n id: string;\n label: string;\n kind: \"select\";\n operators?: LayeredFilterOperator[];\n options: Array<{ value: string; label: string }>;\n };\n\nconst layeredFilterRuleSchema = z.object({\n id: z.string().min(1),\n field: z.string().min(1),\n operator: z.enum(LAYERED_FILTER_OPERATORS),\n value: z.string(),\n valueTo: z.string().optional(),\n});\n\nconst layeredFilterRulesSchema = z.array(layeredFilterRuleSchema).max(20);\n\nconst TEXT_OPERATORS: LayeredFilterOperator[] = [\n \"contains\",\n \"not_contains\",\n \"equals\",\n \"not_equals\",\n \"starts_with\",\n \"ends_with\",\n \"word\",\n \"not_word\",\n \"is_empty\",\n \"is_not_empty\",\n];\n\nconst NUMBER_OPERATORS: LayeredFilterOperator[] = [\n \"equals\",\n \"not_equals\",\n \"greater_than\",\n \"greater_or_equal\",\n \"less_than\",\n \"less_or_equal\",\n \"between\",\n \"is_empty\",\n \"is_not_empty\",\n];\n\nconst SELECT_OPERATORS: LayeredFilterOperator[] = [\n \"equals\",\n \"not_equals\",\n \"is_empty\",\n \"is_not_empty\",\n];\n\nexport const LAYERED_FILTER_OPERATOR_LABELS: Record<\n LayeredFilterOperator,\n string\n> = {\n contains: \"contains\",\n not_contains: \"doesn't contain\",\n equals: \"is\",\n not_equals: \"is not\",\n starts_with: \"starts with\",\n ends_with: \"ends with\",\n word: \"has the word\",\n not_word: \"doesn't have the word\",\n is_empty: \"is empty\",\n is_not_empty: \"is not empty\",\n greater_than: \"is greater than\",\n greater_or_equal: \"is at least\",\n less_than: \"is less than\",\n less_or_equal: \"is at most\",\n between: \"is between\",\n};\n\nexport function operatorsForLayeredField(\n field: LayeredFilterField,\n): LayeredFilterOperator[] {\n if (field.operators) return field.operators;\n if (field.kind === \"number\") return NUMBER_OPERATORS;\n if (field.kind === \"select\") return SELECT_OPERATORS;\n return TEXT_OPERATORS;\n}\n\nexport function layeredFilterNeedsValue(\n operator: LayeredFilterOperator,\n): boolean {\n return operator !== \"is_empty\" && operator !== \"is_not_empty\";\n}\n\nexport function isCompleteLayeredFilterRule(rule: LayeredFilterRule): boolean {\n if (!layeredFilterNeedsValue(rule.operator)) return true;\n if (rule.value.trim() === \"\") return false;\n return rule.operator !== \"between\" || Boolean(rule.valueTo?.trim());\n}\n\nexport function completeLayeredFilterRules(\n rules: readonly LayeredFilterRule[] | undefined,\n): LayeredFilterRule[] {\n return (rules ?? []).filter(isCompleteLayeredFilterRule);\n}\n\nexport function isLayeredFilterOperator(\n value: string,\n): value is LayeredFilterOperator {\n return LAYERED_FILTER_OPERATORS.some((operator) => operator === value);\n}\n\n/** Decode URL-owned rules without trusting a hand-edited link. */\nexport function decodeLayeredFilterRules(\n raw: string | null,\n): LayeredFilterRule[] {\n if (!raw) return [];\n try {\n const parsed: unknown = JSON.parse(raw);\n const result = layeredFilterRulesSchema.safeParse(parsed);\n return result.success ? result.data : [];\n } catch {\n return [];\n }\n}\n\n/** Stable compact URL/wire form; rule order is preserved. */\nexport function encodeLayeredFilterRules(\n rules: readonly LayeredFilterRule[] | undefined,\n): string | null {\n return rules?.length ? JSON.stringify(rules) : null;\n}\n\n/**\n * Translate ordinary per-column controls into the same ordered rule language\n * used by the advanced builder. Controlled tables can send these rules to one\n * server evaluator instead of rebuilding numeric/text filter semantics.\n * Select filters are intentionally omitted because their multi-select control\n * is OR-combined and should stay in its native query parameter.\n */\nexport function columnFiltersToLayeredRules(\n filters: ColumnFiltersState,\n fieldIds: readonly string[],\n): LayeredFilterRule[] {\n const rules: LayeredFilterRule[] = [];\n for (const field of fieldIds) {\n const filter = filters[field];\n if (!filter) continue;\n\n if (filter.kind === \"text\") {\n rules.push({\n id: `column-${field}`,\n field,\n operator:\n filter.mode === \"empty\"\n ? \"is_empty\"\n : filter.mode === \"not_empty\"\n ? \"is_not_empty\"\n : \"contains\",\n value: filter.value,\n });\n continue;\n }\n\n if (filter.kind === \"number\") {\n if (filter.min !== undefined && filter.max !== undefined) {\n rules.push({\n id: `column-${field}`,\n field,\n operator: \"between\",\n value: String(filter.min),\n valueTo: String(filter.max),\n });\n } else if (filter.min !== undefined) {\n rules.push({\n id: `column-${field}`,\n field,\n operator: \"greater_or_equal\",\n value: String(filter.min),\n });\n } else if (filter.max !== undefined) {\n rules.push({\n id: `column-${field}`,\n field,\n operator: \"less_or_equal\",\n value: String(filter.max),\n });\n }\n continue;\n }\n\n if (filter.kind === \"boolean\") {\n rules.push({\n id: `column-${field}`,\n field,\n operator: \"equals\",\n value: String(filter.value),\n });\n }\n }\n return rules;\n}\n\nfunction normalizedText(value: unknown): string {\n if (value === null || value === undefined) return \"\";\n return String(value).trim().toLocaleLowerCase();\n}\n\nfunction words(value: string): string[] {\n return value.split(/[^\\p{L}\\p{N}]+/u).filter(Boolean);\n}\n\n/** Local-table evaluator. Controlled tables send the same rule shape server-side. */\nexport function layeredFilterMatchesValue(\n value: unknown,\n rule: LayeredFilterRule,\n): boolean {\n const actual = normalizedText(value);\n const expected = rule.value.trim().toLocaleLowerCase();\n const expectedTo = rule.valueTo?.trim().toLocaleLowerCase() ?? \"\";\n\n switch (rule.operator) {\n case \"contains\":\n return actual.includes(expected);\n case \"not_contains\":\n return !actual.includes(expected);\n case \"equals\":\n return actual === expected;\n case \"not_equals\":\n return actual !== expected;\n case \"starts_with\":\n return actual.startsWith(expected);\n case \"ends_with\":\n return actual.endsWith(expected);\n case \"word\":\n return words(actual).includes(expected);\n case \"not_word\":\n return !words(actual).includes(expected);\n case \"is_empty\":\n return actual === \"\";\n case \"is_not_empty\":\n return actual !== \"\";\n case \"greater_than\":\n return Number(actual) > Number(expected);\n case \"greater_or_equal\":\n return Number(actual) >= Number(expected);\n case \"less_than\":\n return Number(actual) < Number(expected);\n case \"less_or_equal\":\n return Number(actual) <= Number(expected);\n case \"between\": {\n const numeric = Number(actual);\n return numeric >= Number(expected) && numeric <= Number(expectedTo);\n }\n }\n}\n\nexport function layeredFilterRuleSummary(\n rule: LayeredFilterRule,\n fields: readonly LayeredFilterField[],\n): string {\n const field = fields.find((candidate) => candidate.id === rule.field);\n const fieldLabel = field?.label ?? rule.field;\n const operator = LAYERED_FILTER_OPERATOR_LABELS[rule.operator];\n if (!layeredFilterNeedsValue(rule.operator))\n return `${fieldLabel} ${operator}`;\n const selectedLabel =\n field?.kind === \"select\"\n ? field.options.find((option) => option.value === rule.value)?.label\n : undefined;\n const value = selectedLabel ?? rule.value;\n return rule.operator === \"between\"\n ? `${fieldLabel} ${operator} ${value} and ${rule.valueTo ?? \"\"}`\n : `${fieldLabel} ${operator} ${value}`;\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":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/data-table/layered-filters.ts","../../src/data-table/filter-engine.ts"],"sourcesContent":["/**\n * Shared vocabulary for ordered, AND-combined table filters. The same rule\n * shape is URL-safe, can be evaluated by local tables, and can be handed to a\n * controlled table's server query without a surface inventing another query\n * builder.\n */\n\nimport { z } from \"zod\";\nimport type { ColumnFiltersState } from \"./types\";\n\nexport const LAYERED_FILTER_OPERATORS = [\n \"contains\",\n \"not_contains\",\n \"equals\",\n \"not_equals\",\n \"starts_with\",\n \"ends_with\",\n \"word\",\n \"not_word\",\n \"is_empty\",\n \"is_not_empty\",\n \"greater_than\",\n \"greater_or_equal\",\n \"less_than\",\n \"less_or_equal\",\n \"between\",\n] as const;\n\nexport type LayeredFilterOperator = (typeof LAYERED_FILTER_OPERATORS)[number];\n\nexport interface LayeredFilterRule {\n id: string;\n field: string;\n operator: LayeredFilterOperator;\n value: string;\n valueTo?: string | undefined;\n}\n\nexport type LayeredFilterField =\n | {\n id: string;\n label: string;\n kind: \"text\";\n operators?: LayeredFilterOperator[];\n }\n | {\n id: string;\n label: string;\n kind: \"number\";\n operators?: LayeredFilterOperator[];\n placeholder?: string;\n }\n | {\n id: string;\n label: string;\n kind: \"select\";\n operators?: LayeredFilterOperator[];\n options: Array<{ value: string; label: string }>;\n };\n\nconst layeredFilterRuleSchema = z.object({\n id: z.string().min(1),\n field: z.string().min(1),\n operator: z.enum(LAYERED_FILTER_OPERATORS),\n value: z.string(),\n valueTo: z.string().optional(),\n});\n\nconst layeredFilterRulesSchema = z.array(layeredFilterRuleSchema).max(20);\n\nconst TEXT_OPERATORS: LayeredFilterOperator[] = [\n \"contains\",\n \"not_contains\",\n \"equals\",\n \"not_equals\",\n \"starts_with\",\n \"ends_with\",\n \"word\",\n \"not_word\",\n \"is_empty\",\n \"is_not_empty\",\n];\n\nconst NUMBER_OPERATORS: LayeredFilterOperator[] = [\n \"equals\",\n \"not_equals\",\n \"greater_than\",\n \"greater_or_equal\",\n \"less_than\",\n \"less_or_equal\",\n \"between\",\n \"is_empty\",\n \"is_not_empty\",\n];\n\nconst SELECT_OPERATORS: LayeredFilterOperator[] = [\n \"equals\",\n \"not_equals\",\n \"is_empty\",\n \"is_not_empty\",\n];\n\nexport const LAYERED_FILTER_OPERATOR_LABELS: Record<\n LayeredFilterOperator,\n string\n> = {\n contains: \"contains\",\n not_contains: \"doesn't contain\",\n equals: \"is\",\n not_equals: \"is not\",\n starts_with: \"starts with\",\n ends_with: \"ends with\",\n word: \"has the word\",\n not_word: \"doesn't have the word\",\n is_empty: \"is empty\",\n is_not_empty: \"is not empty\",\n greater_than: \"is greater than\",\n greater_or_equal: \"is at least\",\n less_than: \"is less than\",\n less_or_equal: \"is at most\",\n between: \"is between\",\n};\n\nexport function operatorsForLayeredField(\n field: LayeredFilterField,\n): LayeredFilterOperator[] {\n if (field.operators) return field.operators;\n if (field.kind === \"number\") return NUMBER_OPERATORS;\n if (field.kind === \"select\") return SELECT_OPERATORS;\n return TEXT_OPERATORS;\n}\n\nexport function layeredFilterNeedsValue(\n operator: LayeredFilterOperator,\n): boolean {\n return operator !== \"is_empty\" && operator !== \"is_not_empty\";\n}\n\nexport function isCompleteLayeredFilterRule(rule: LayeredFilterRule): boolean {\n if (!layeredFilterNeedsValue(rule.operator)) return true;\n if (rule.value.trim() === \"\") return false;\n return rule.operator !== \"between\" || Boolean(rule.valueTo?.trim());\n}\n\nexport function completeLayeredFilterRules(\n rules: readonly LayeredFilterRule[] | undefined,\n): LayeredFilterRule[] {\n return (rules ?? []).filter(isCompleteLayeredFilterRule);\n}\n\nexport function isLayeredFilterOperator(\n value: string,\n): value is LayeredFilterOperator {\n return LAYERED_FILTER_OPERATORS.some((operator) => operator === value);\n}\n\n/** Decode URL-owned rules without trusting a hand-edited link. */\nexport function decodeLayeredFilterRules(\n raw: string | null,\n): LayeredFilterRule[] {\n if (!raw) return [];\n try {\n const parsed: unknown = JSON.parse(raw);\n const result = layeredFilterRulesSchema.safeParse(parsed);\n return result.success ? result.data : [];\n } catch {\n return [];\n }\n}\n\n/** Stable compact URL/wire form; rule order is preserved. */\nexport function encodeLayeredFilterRules(\n rules: readonly LayeredFilterRule[] | undefined,\n): string | null {\n return rules?.length ? JSON.stringify(rules) : null;\n}\n\n/**\n * Translate ordinary per-column controls into the same ordered rule language\n * used by the advanced builder. Controlled tables can send these rules to one\n * server evaluator instead of rebuilding numeric/text filter semantics.\n * Select filters are intentionally omitted because their multi-select control\n * is OR-combined and should stay in its native query parameter.\n */\nexport function columnFiltersToLayeredRules(\n filters: ColumnFiltersState,\n fieldIds: readonly string[],\n): LayeredFilterRule[] {\n const rules: LayeredFilterRule[] = [];\n for (const field of fieldIds) {\n const filter = filters[field];\n if (!filter) continue;\n\n if (filter.kind === \"text\") {\n rules.push({\n id: `column-${field}`,\n field,\n operator:\n filter.mode === \"empty\"\n ? \"is_empty\"\n : filter.mode === \"not_empty\"\n ? \"is_not_empty\"\n : \"contains\",\n value: filter.value,\n });\n continue;\n }\n\n if (filter.kind === \"number\") {\n if (filter.min !== undefined && filter.max !== undefined) {\n rules.push({\n id: `column-${field}`,\n field,\n operator: \"between\",\n value: String(filter.min),\n valueTo: String(filter.max),\n });\n } else if (filter.min !== undefined) {\n rules.push({\n id: `column-${field}`,\n field,\n operator: \"greater_or_equal\",\n value: String(filter.min),\n });\n } else if (filter.max !== undefined) {\n rules.push({\n id: `column-${field}`,\n field,\n operator: \"less_or_equal\",\n value: String(filter.max),\n });\n }\n continue;\n }\n\n if (filter.kind === \"boolean\") {\n rules.push({\n id: `column-${field}`,\n field,\n operator: \"equals\",\n value: String(filter.value),\n });\n }\n }\n return rules;\n}\n\nfunction normalizedText(value: unknown): string {\n if (value === null || value === undefined) return \"\";\n return String(value).trim().toLocaleLowerCase();\n}\n\nfunction words(value: string): string[] {\n return value.split(/[^\\p{L}\\p{N}]+/u).filter(Boolean);\n}\n\n/** Local-table evaluator. Controlled tables send the same rule shape server-side. */\nexport function layeredFilterMatchesValue(\n value: unknown,\n rule: LayeredFilterRule,\n): boolean {\n const actual = normalizedText(value);\n const expected = rule.value.trim().toLocaleLowerCase();\n const expectedTo = rule.valueTo?.trim().toLocaleLowerCase() ?? \"\";\n\n switch (rule.operator) {\n case \"contains\":\n return actual.includes(expected);\n case \"not_contains\":\n return !actual.includes(expected);\n case \"equals\":\n return actual === expected;\n case \"not_equals\":\n return actual !== expected;\n case \"starts_with\":\n return actual.startsWith(expected);\n case \"ends_with\":\n return actual.endsWith(expected);\n case \"word\":\n return words(actual).includes(expected);\n case \"not_word\":\n return !words(actual).includes(expected);\n case \"is_empty\":\n return actual === \"\";\n case \"is_not_empty\":\n return actual !== \"\";\n case \"greater_than\":\n return Number(actual) > Number(expected);\n case \"greater_or_equal\":\n return Number(actual) >= Number(expected);\n case \"less_than\":\n return Number(actual) < Number(expected);\n case \"less_or_equal\":\n return Number(actual) <= Number(expected);\n case \"between\": {\n const numeric = Number(actual);\n return numeric >= Number(expected) && numeric <= Number(expectedTo);\n }\n }\n}\n\nexport function layeredFilterRuleSummary(\n rule: LayeredFilterRule,\n fields: readonly LayeredFilterField[],\n): string {\n const field = fields.find((candidate) => candidate.id === rule.field);\n const fieldLabel = field?.label ?? rule.field;\n const operator = LAYERED_FILTER_OPERATOR_LABELS[rule.operator];\n if (!layeredFilterNeedsValue(rule.operator))\n return `${fieldLabel} ${operator}`;\n const selectedLabel =\n field?.kind === \"select\"\n ? field.options.find((option) => option.value === rule.value)?.label\n : undefined;\n const value = selectedLabel ?? rule.value;\n return rule.operator === \"between\"\n ? `${fieldLabel} ${operator} ${value} and ${rule.valueTo ?? \"\"}`\n : `${fieldLabel} ${operator} ${value}`;\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 // A missing cell is not zero. Without this guard, Number(\"\") made a\n // blank numeric value pass filters whose range included zero.\n const raw = stringifyCellValue(value).trim();\n if (raw === \"\") return false;\n const normalized = raw.replace(/[^0-9.-]/g, \"\");\n if (normalized === \"\") return false;\n const n = typeof value === \"number\"\n ? value\n : Number(normalized);\n if (!Number.isFinite(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":[]}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
import { ComponentType, ReactNode, MouseEventHandler } from 'react';
|
|
3
|
-
import { b as TableSavedViewsProps, c as MatrxDataTableRecordControls, d as MatrxDataTableDensity } from '../types-
|
|
3
|
+
import { b as TableSavedViewsProps, c as MatrxDataTableRecordControls, d as MatrxDataTableDensity } from '../types-BgqIKbhq.cjs';
|
|
4
4
|
import { CopyControlProps } from './copy-types.cjs';
|
|
5
5
|
export { AgentPayloadInput, AiCustomSource, AiVariant, CopyExportConfig } from './copy-types.cjs';
|
|
6
6
|
import '../content-transfer.cjs';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
import { ComponentType, ReactNode, MouseEventHandler } from 'react';
|
|
3
|
-
import { b as TableSavedViewsProps, c as MatrxDataTableRecordControls, d as MatrxDataTableDensity } from '../types-
|
|
3
|
+
import { b as TableSavedViewsProps, c as MatrxDataTableRecordControls, d as MatrxDataTableDensity } from '../types-emLxze_m.js';
|
|
4
4
|
import { CopyControlProps } from './copy-types.js';
|
|
5
5
|
export { AgentPayloadInput, AiCustomSource, AiVariant, CopyExportConfig } from './copy-types.js';
|
|
6
6
|
import '../content-transfer.js';
|
|
@@ -47,10 +47,12 @@ __export(data_table_exports, {
|
|
|
47
47
|
collectSelectOptions: () => collectSelectOptions,
|
|
48
48
|
downloadTableAsXlsx: () => downloadTableAsXlsx,
|
|
49
49
|
filterKindLabel: () => filterKindLabel,
|
|
50
|
+
inferTableSearchRole: () => inferTableSearchRole,
|
|
50
51
|
parseTableViewSnapshot: () => parseTableViewSnapshot,
|
|
51
52
|
rankTableSearchRows: () => rankTableSearchRows,
|
|
52
53
|
resolveColumnMarker: () => resolveColumnMarker,
|
|
53
54
|
resolveFilterKind: () => resolveFilterKind,
|
|
55
|
+
resolveTableSearchScopes: () => resolveTableSearchScopes,
|
|
54
56
|
rowsToCsvFromColumns: () => rowsToCsvFromColumns,
|
|
55
57
|
rowsToMarkdownTable: () => rowsToMarkdownTable,
|
|
56
58
|
rowsToRecordsFromColumns: () => rowsToRecordsFromColumns,
|
|
@@ -3179,6 +3181,7 @@ var outcomeMessage = (result) => {
|
|
|
3179
3181
|
const success = result;
|
|
3180
3182
|
return success.delivered === "action" ? success.message ?? "Action completed" : success.delivered === "download-started" ? "Download started" : success.message ?? "Copied";
|
|
3181
3183
|
}
|
|
3184
|
+
if (result.status === "queued") return `${result.message} ${result.remedy}`;
|
|
3182
3185
|
return result.status === "degraded" ? `Copied as plain text: ${result.reason}` : result.status === "cancelled" ? "Cancelled" : result.message;
|
|
3183
3186
|
};
|
|
3184
3187
|
var materialIds = /* @__PURE__ */ new WeakMap();
|
|
@@ -5139,8 +5142,12 @@ function passesColumnFilter(value, filter) {
|
|
|
5139
5142
|
return Boolean(value) === filter.value;
|
|
5140
5143
|
}
|
|
5141
5144
|
case "number": {
|
|
5142
|
-
const
|
|
5143
|
-
if (
|
|
5145
|
+
const raw = stringifyCellValue(value).trim();
|
|
5146
|
+
if (raw === "") return false;
|
|
5147
|
+
const normalized = raw.replace(/[^0-9.-]/g, "");
|
|
5148
|
+
if (normalized === "") return false;
|
|
5149
|
+
const n = typeof value === "number" ? value : Number(normalized);
|
|
5150
|
+
if (!Number.isFinite(n)) return false;
|
|
5144
5151
|
if (filter.min !== void 0 && n < filter.min) return false;
|
|
5145
5152
|
if (filter.max !== void 0 && n > filter.max) return false;
|
|
5146
5153
|
return true;
|
|
@@ -8946,7 +8953,7 @@ function MatrxDataTableCore({
|
|
|
8946
8953
|
setInternalPage(next);
|
|
8947
8954
|
};
|
|
8948
8955
|
const [internalPageSize, setInternalPageSize] = (0, import_react20.useState)(
|
|
8949
|
-
defaultPageSize
|
|
8956
|
+
defaultPageSize
|
|
8950
8957
|
);
|
|
8951
8958
|
const pageSize = controlledQuery ? controlledQuery.state.pageSize : internalPageSize;
|
|
8952
8959
|
const setPageSize = (next) => {
|
|
@@ -10445,23 +10452,6 @@ function MatrxDataTableCore({
|
|
|
10445
10452
|
"data-matrx-table-actions": true,
|
|
10446
10453
|
className: "ml-auto flex min-w-0 flex-wrap items-center gap-0",
|
|
10447
10454
|
children: [
|
|
10448
|
-
sourceTotal !== void 0 ? /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)(
|
|
10449
|
-
"span",
|
|
10450
|
-
{
|
|
10451
|
-
"data-matrx-table-source-total": true,
|
|
10452
|
-
className: "text-xs tabular-nums text-muted-foreground",
|
|
10453
|
-
children: [
|
|
10454
|
-
data.length.toLocaleString(),
|
|
10455
|
-
" loaded of",
|
|
10456
|
-
" ",
|
|
10457
|
-
Math.max(0, sourceTotal).toLocaleString(),
|
|
10458
|
-
" \xB7",
|
|
10459
|
-
" ",
|
|
10460
|
-
processed.length.toLocaleString(),
|
|
10461
|
-
" matched"
|
|
10462
|
-
]
|
|
10463
|
-
}
|
|
10464
|
-
) : null,
|
|
10465
10455
|
/* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
|
|
10466
10456
|
TableToolbarAction,
|
|
10467
10457
|
{
|
|
@@ -11294,14 +11284,14 @@ function MatrxDataTableCore({
|
|
|
11294
11284
|
"div",
|
|
11295
11285
|
{
|
|
11296
11286
|
"data-matrx-table-footer": true,
|
|
11297
|
-
className: "flex min-h-
|
|
11287
|
+
className: "flex min-h-8 shrink-0 flex-wrap items-center gap-2 border-t border-border",
|
|
11298
11288
|
"aria-busy": isLoading || isFetching,
|
|
11299
11289
|
children: [
|
|
11300
11290
|
/* @__PURE__ */ (0, import_jsx_runtime41.jsxs)(
|
|
11301
11291
|
"div",
|
|
11302
11292
|
{
|
|
11303
11293
|
...isFetching && !isLoading ? {} : { role: "status", "aria-live": "polite" },
|
|
11304
|
-
className: "flex w-full min-w-0 items-center gap-2 text-xs text-muted-foreground sm:w-auto sm:flex-1
|
|
11294
|
+
className: "flex w-full min-w-0 items-center gap-2 text-xs text-muted-foreground sm:w-auto sm:flex-1",
|
|
11305
11295
|
children: [
|
|
11306
11296
|
isLoading ? /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
|
|
11307
11297
|
Loader2,
|
|
@@ -11311,15 +11301,19 @@ function MatrxDataTableCore({
|
|
|
11311
11301
|
className: "h-5 w-5 shrink-0 animate-spin text-primary motion-reduce:animate-none"
|
|
11312
11302
|
}
|
|
11313
11303
|
) : null,
|
|
11314
|
-
/* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("div", { className: "min-w-0 flex-1", children: [
|
|
11315
|
-
/* @__PURE__ */ (0, import_jsx_runtime41.jsx)("p", { className: "font-medium text-foreground", children: isLoading ? "Loading rows\u2026" : `${(0, import_format3.formatCount)(data.length)} loaded${appendQuery.pagination.totalItems === void 0 ? "" : ` of ${(0, import_format3.formatCount)(appendQuery.pagination.totalItems)}`}` }),
|
|
11316
|
-
appendQuery.pagination.sourceUntil ? /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)(
|
|
11317
|
-
"Source
|
|
11318
|
-
|
|
11319
|
-
|
|
11320
|
-
|
|
11304
|
+
/* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("div", { className: "flex min-w-0 flex-1 flex-wrap items-center gap-x-2", children: [
|
|
11305
|
+
/* @__PURE__ */ (0, import_jsx_runtime41.jsx)("p", { className: "min-w-0 break-words font-medium text-foreground", children: isLoading ? "Loading rows\u2026" : appendQuery.pagination.sourceRecords ? `${(0, import_format3.formatCount)(processed.length)} ${appendQuery.pagination.sourceRecords.rowLabel}${appendQuery.pagination.sourceRecords.matchedRecords === void 0 ? "" : ` (${(0, import_format3.formatCount)(appendQuery.pagination.sourceRecords.matchedRecords)} matching ${appendQuery.pagination.sourceRecords.label})`} \xB7 ${(0, import_format3.formatCount)(appendQuery.pagination.sourceRecords.loaded)}${appendQuery.pagination.sourceRecords.total === void 0 ? "" : ` / ${(0, import_format3.formatCount)(appendQuery.pagination.sourceRecords.total)}`} ${appendQuery.pagination.sourceRecords.label} loaded` : `${(0, import_format3.formatCount)(data.length)} loaded${appendQuery.pagination.totalItems === void 0 ? "" : ` of ${(0, import_format3.formatCount)(appendQuery.pagination.totalItems)}`}` }),
|
|
11306
|
+
appendQuery.pagination.sourceUntil || appendUsesLocalProcessing ? /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)(Tooltip, { children: [
|
|
11307
|
+
/* @__PURE__ */ (0, import_jsx_runtime41.jsx)(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("button", { type: "button", "aria-label": "Source coverage", className: "shrink-0 text-xs text-muted-foreground underline decoration-dotted underline-offset-4", children: "Source info" }) }),
|
|
11308
|
+
/* @__PURE__ */ (0, import_jsx_runtime41.jsxs)(TooltipContent, { children: [
|
|
11309
|
+
appendQuery.pagination.sourceUntil ? /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("p", { children: [
|
|
11310
|
+
"Source fixed through ",
|
|
11311
|
+
new Date(appendQuery.pagination.sourceUntil).toLocaleString(),
|
|
11312
|
+
"."
|
|
11313
|
+
] }) : null,
|
|
11314
|
+
/* @__PURE__ */ (0, import_jsx_runtime41.jsx)("p", { children: !isLoading && !isFetching && !appendQuery.pagination.loadAll?.isLoading && !scrollPagination.error && !appendQuery.pagination.hasNextPage && appendQuery.pagination.totalItems !== void 0 && data.length === appendQuery.pagination.totalItems ? "All source rows are loaded." : appendUsesLocalProcessing ? "Local search and filters cover loaded rows." : "Source paging is active." })
|
|
11315
|
+
] })
|
|
11321
11316
|
] }) : null,
|
|
11322
|
-
!isLoading && !isFetching && !appendQuery.pagination.loadAll?.isLoading && !scrollPagination.error && !appendQuery.pagination.hasNextPage && appendQuery.pagination.totalItems !== void 0 && data.length === appendQuery.pagination.totalItems ? /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("p", { children: "All source rows are loaded." }) : appendUsesLocalProcessing ? /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("p", { children: "Local search and filters cover loaded rows." }) : null,
|
|
11323
11317
|
scrollPagination.error ? /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("p", { role: "alert", className: "text-destructive", children: scrollPagination.error.message }) : null,
|
|
11324
11318
|
appendScrollPolicy?.mode === "suspended" ? /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("p", { role: "status", children: [
|
|
11325
11319
|
appendScrollPolicy.reason,
|
|
@@ -11339,7 +11333,7 @@ function MatrxDataTableCore({
|
|
|
11339
11333
|
SelectTrigger,
|
|
11340
11334
|
{
|
|
11341
11335
|
"aria-label": "Rows per load",
|
|
11342
|
-
className: "h-
|
|
11336
|
+
className: "h-8 min-w-20 shrink-0 rounded-md border border-border bg-card px-3 text-sm [&_svg]:size-5",
|
|
11343
11337
|
disabled: isLoading || isFetching || appendQuery.pagination.loadAll?.isLoading,
|
|
11344
11338
|
children: /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(SelectValue, {})
|
|
11345
11339
|
}
|
|
@@ -11353,7 +11347,7 @@ function MatrxDataTableCore({
|
|
|
11353
11347
|
{
|
|
11354
11348
|
type: "button",
|
|
11355
11349
|
variant: "outline",
|
|
11356
|
-
className: "h-
|
|
11350
|
+
className: "h-8 shrink-0 px-3 text-sm",
|
|
11357
11351
|
onClick: appendQuery.pagination.loadAll.stop,
|
|
11358
11352
|
children: "Stop loading"
|
|
11359
11353
|
}
|
|
@@ -11362,7 +11356,7 @@ function MatrxDataTableCore({
|
|
|
11362
11356
|
{
|
|
11363
11357
|
type: "button",
|
|
11364
11358
|
variant: "outline",
|
|
11365
|
-
className: "h-
|
|
11359
|
+
className: "h-8 shrink-0 px-3 text-sm",
|
|
11366
11360
|
onClick: appendQuery.pagination.retrySource ?? (appendQuery.pagination.hasNextPage ? appendQuery.pagination.loadNextPage : appendQuery.pagination.refresh),
|
|
11367
11361
|
children: appendQuery.pagination.retrySource ? "Retry source" : appendQuery.pagination.hasNextPage ? "Retry" : "Reload rows"
|
|
11368
11362
|
}
|
|
@@ -11372,7 +11366,7 @@ function MatrxDataTableCore({
|
|
|
11372
11366
|
{
|
|
11373
11367
|
type: "button",
|
|
11374
11368
|
variant: "outline",
|
|
11375
|
-
className: "h-
|
|
11369
|
+
className: "h-8 px-3 text-sm",
|
|
11376
11370
|
disabled: isLoading || isFetching,
|
|
11377
11371
|
onClick: () => void scrollPagination.requestNextPage(),
|
|
11378
11372
|
children: "Load more"
|
|
@@ -11383,7 +11377,7 @@ function MatrxDataTableCore({
|
|
|
11383
11377
|
{
|
|
11384
11378
|
type: "button",
|
|
11385
11379
|
variant: "ghost",
|
|
11386
|
-
className: "h-
|
|
11380
|
+
className: "h-8 px-3 text-sm",
|
|
11387
11381
|
disabled: isLoading || isFetching,
|
|
11388
11382
|
onClick: appendQuery.pagination.loadAll.request,
|
|
11389
11383
|
children: "Load all"
|
|
@@ -11392,10 +11386,11 @@ function MatrxDataTableCore({
|
|
|
11392
11386
|
] }) : !isLoading && appendQuery.pagination.totalItems !== void 0 && data.length === appendQuery.pagination.totalItems ? /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("span", { className: "min-w-0 max-w-full break-words whitespace-normal px-3 text-xs text-muted-foreground sm:text-sm", children: "All rows loaded" }) : null
|
|
11393
11387
|
]
|
|
11394
11388
|
}
|
|
11395
|
-
) : !hidePagination && totalItems > 0 ? /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("div", { className: "shrink-0", children: /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
|
|
11389
|
+
) : !hidePagination && (totalItems > 0 || sourceTotal !== void 0) ? /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("div", { className: "shrink-0", children: /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
|
|
11396
11390
|
GenericTablePagination,
|
|
11397
11391
|
{
|
|
11398
11392
|
totalItems,
|
|
11393
|
+
...sourceTotal === void 0 ? {} : { labelFormat: (start, end, total) => `${(0, import_format3.formatCount)(start)}\u2013${(0, import_format3.formatCount)(end)} of ${(0, import_format3.formatCount)(total)} matching \xB7 ${(0, import_format3.formatCount)(data.length)} / ${(0, import_format3.formatCount)(sourceTotal)} loaded` },
|
|
11399
11394
|
...localProgressiveEnabled ? { progressive: {
|
|
11400
11395
|
shownCount: progressiveRows.shownCount,
|
|
11401
11396
|
loadedCount: data.length,
|