@jielga/tmdatagrid 0.1.0
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/LICENSE +21 -0
- package/README.md +162 -0
- package/dist/index.d.ts +592 -0
- package/dist/index.js +1590 -0
- package/dist/index.js.map +1 -0
- package/dist/styles.css +2 -0
- package/package.json +111 -0
- package/skills/columns/SKILL.md +195 -0
- package/skills/features/SKILL.md +214 -0
- package/skills/getting-started/SKILL.md +209 -0
- package/skills/options/SKILL.md +208 -0
- package/skills/server-side/SKILL.md +150 -0
- package/src/tmdatagrid/TMDataGrid.module.css +410 -0
- package/src/tmdatagrid/TMDataGrid.tsx +101 -0
- package/src/tmdatagrid/TMDataGridColumnsPanel.tsx +91 -0
- package/src/tmdatagrid/TMDataGridContext.ts +32 -0
- package/src/tmdatagrid/TMDataGridFilterPanel.tsx +182 -0
- package/src/tmdatagrid/TMDataGridFooter.tsx +74 -0
- package/src/tmdatagrid/TMDataGridHeaderCell.tsx +466 -0
- package/src/tmdatagrid/TMDataGridTable.tsx +297 -0
- package/src/tmdatagrid/TMDataGridToolbar.tsx +131 -0
- package/src/tmdatagrid/capabilities.ts +117 -0
- package/src/tmdatagrid/columnOrdering.ts +140 -0
- package/src/tmdatagrid/columnUtils.ts +43 -0
- package/src/tmdatagrid/filterOperators.ts +171 -0
- package/src/tmdatagrid/icons.ts +25 -0
- package/src/tmdatagrid/index.ts +68 -0
- package/src/tmdatagrid/persistence.ts +187 -0
- package/src/tmdatagrid/sizes.ts +29 -0
- package/src/tmdatagrid/useTMDataGrid.tsx +378 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { TMDataGridColumnType } from "./filterOperators";
|
|
2
|
+
import type { TMDataGridColumnMeta } from "./useTMDataGrid";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Structural shape of the column bits the chrome reads. Kept minimal so these
|
|
6
|
+
* helpers work with any `Column`/`Header` instance without dragging generics
|
|
7
|
+
* through every call site.
|
|
8
|
+
*/
|
|
9
|
+
type ColumnLike = {
|
|
10
|
+
id: string;
|
|
11
|
+
columnDef: { header?: unknown; meta?: TMDataGridColumnMeta };
|
|
12
|
+
/** Set on a leaf that sits inside a header group. */
|
|
13
|
+
parent?: unknown;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/** Menu- and panel-facing column name. */
|
|
17
|
+
export function getColumnLabel(column: ColumnLike): string {
|
|
18
|
+
const label = column.columnDef.meta?.label;
|
|
19
|
+
if (label) return label;
|
|
20
|
+
const header = column.columnDef.header;
|
|
21
|
+
return typeof header === "string" ? header : column.id;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function getColumnType(column: ColumnLike): TMDataGridColumnType {
|
|
25
|
+
return column.columnDef.meta?.type ?? "string";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function getColumnAlign(column: ColumnLike): "left" | "right" | "center" {
|
|
29
|
+
return column.columnDef.meta?.align ?? "left";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Whether a column may be moved. Ordering is the one column feature TanStack
|
|
34
|
+
* has no column option for, so the switch lives in `meta.enableOrdering`.
|
|
35
|
+
*
|
|
36
|
+
* A leaf inside a header group is never movable: `columnOrder` sequences leaf
|
|
37
|
+
* columns, so moving one out of its group would leave the group header spanning
|
|
38
|
+
* columns that no longer belong to it.
|
|
39
|
+
*/
|
|
40
|
+
export function isColumnReorderable(column: ColumnLike): boolean {
|
|
41
|
+
if (column.columnDef.meta?.enableOrdering === false) return false;
|
|
42
|
+
return column.parent === undefined;
|
|
43
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import type { Row, RowData, TableFeatures } from "@tanstack/react-table";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The value shape stored in `columnFilters` for every TMDataGrid column.
|
|
5
|
+
*
|
|
6
|
+
* TanStack resolves `filterFn` statically per column, so the operator travels
|
|
7
|
+
* inside the filter *value* instead. That keeps the whole filter model plain,
|
|
8
|
+
* serialisable JSON — which is what makes it portable to a server-side
|
|
9
|
+
* `manualFiltering` table (just forward `columnFilters` to the API).
|
|
10
|
+
*/
|
|
11
|
+
export type TMDataGridFilterValue = {
|
|
12
|
+
operator: TMDataGridFilterOperator;
|
|
13
|
+
value: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type TMDataGridColumnType = "string" | "number";
|
|
17
|
+
|
|
18
|
+
export type TMDataGridFilterOperator =
|
|
19
|
+
| "contains"
|
|
20
|
+
| "equals"
|
|
21
|
+
| "notEquals"
|
|
22
|
+
| "startsWith"
|
|
23
|
+
| "endsWith"
|
|
24
|
+
| "greaterThan"
|
|
25
|
+
| "greaterThanOrEqual"
|
|
26
|
+
| "lessThan"
|
|
27
|
+
| "lessThanOrEqual"
|
|
28
|
+
| "isEmpty"
|
|
29
|
+
| "isNotEmpty";
|
|
30
|
+
|
|
31
|
+
export const FILTER_OPERATOR_LABELS: Record<TMDataGridFilterOperator, string> = {
|
|
32
|
+
contains: "contains",
|
|
33
|
+
equals: "equals",
|
|
34
|
+
notEquals: "does not equal",
|
|
35
|
+
startsWith: "starts with",
|
|
36
|
+
endsWith: "ends with",
|
|
37
|
+
greaterThan: "is greater than",
|
|
38
|
+
greaterThanOrEqual: "is greater than or equal to",
|
|
39
|
+
lessThan: "is less than",
|
|
40
|
+
lessThanOrEqual: "is less than or equal to",
|
|
41
|
+
isEmpty: "is empty",
|
|
42
|
+
isNotEmpty: "is not empty",
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const STRING_OPERATORS: readonly TMDataGridFilterOperator[] = [
|
|
46
|
+
"contains",
|
|
47
|
+
"equals",
|
|
48
|
+
"notEquals",
|
|
49
|
+
"startsWith",
|
|
50
|
+
"endsWith",
|
|
51
|
+
"isEmpty",
|
|
52
|
+
"isNotEmpty",
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
const NUMBER_OPERATORS: readonly TMDataGridFilterOperator[] = [
|
|
56
|
+
"equals",
|
|
57
|
+
"notEquals",
|
|
58
|
+
"greaterThan",
|
|
59
|
+
"greaterThanOrEqual",
|
|
60
|
+
"lessThan",
|
|
61
|
+
"lessThanOrEqual",
|
|
62
|
+
"isEmpty",
|
|
63
|
+
"isNotEmpty",
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
/** Operators that ignore the value input (the input is disabled for these). */
|
|
67
|
+
const VALUELESS_OPERATORS: readonly TMDataGridFilterOperator[] = [
|
|
68
|
+
"isEmpty",
|
|
69
|
+
"isNotEmpty",
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
export function getOperatorsForType(
|
|
73
|
+
type: TMDataGridColumnType,
|
|
74
|
+
): readonly TMDataGridFilterOperator[] {
|
|
75
|
+
return type === "number" ? NUMBER_OPERATORS : STRING_OPERATORS;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function getDefaultOperator(
|
|
79
|
+
type: TMDataGridColumnType,
|
|
80
|
+
): TMDataGridFilterOperator {
|
|
81
|
+
return type === "number" ? "equals" : "contains";
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function operatorNeedsValue(operator: TMDataGridFilterOperator): boolean {
|
|
85
|
+
return !VALUELESS_OPERATORS.includes(operator);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function isTMDataGridFilterValue(
|
|
89
|
+
value: unknown,
|
|
90
|
+
): value is TMDataGridFilterValue {
|
|
91
|
+
return (
|
|
92
|
+
typeof value === "object" &&
|
|
93
|
+
value !== null &&
|
|
94
|
+
"operator" in value &&
|
|
95
|
+
typeof (value as TMDataGridFilterValue).operator === "string"
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* A filter only narrows the row set once it has something to compare against.
|
|
101
|
+
* Half-typed filters stay in state (so the panel keeps rendering their row) but
|
|
102
|
+
* are treated as inactive for the funnel indicator and for row matching.
|
|
103
|
+
*/
|
|
104
|
+
export function isFilterActive(value: unknown): boolean {
|
|
105
|
+
if (!isTMDataGridFilterValue(value)) return false;
|
|
106
|
+
return !operatorNeedsValue(value.operator) || value.value.trim() !== "";
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function matchesFilter(
|
|
110
|
+
cellValue: unknown,
|
|
111
|
+
{ operator, value }: TMDataGridFilterValue,
|
|
112
|
+
): boolean {
|
|
113
|
+
if (operator === "isEmpty") {
|
|
114
|
+
return cellValue === null || cellValue === undefined || cellValue === "";
|
|
115
|
+
}
|
|
116
|
+
if (operator === "isNotEmpty") {
|
|
117
|
+
return cellValue !== null && cellValue !== undefined && cellValue !== "";
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const filterText = value.trim();
|
|
121
|
+
if (filterText === "") return true;
|
|
122
|
+
|
|
123
|
+
const numericCell = typeof cellValue === "number" ? cellValue : Number.NaN;
|
|
124
|
+
const numericFilter = Number(filterText);
|
|
125
|
+
const compareNumeric =
|
|
126
|
+
!Number.isNaN(numericCell) && !Number.isNaN(numericFilter);
|
|
127
|
+
|
|
128
|
+
switch (operator) {
|
|
129
|
+
case "greaterThan":
|
|
130
|
+
return compareNumeric && numericCell > numericFilter;
|
|
131
|
+
case "greaterThanOrEqual":
|
|
132
|
+
return compareNumeric && numericCell >= numericFilter;
|
|
133
|
+
case "lessThan":
|
|
134
|
+
return compareNumeric && numericCell < numericFilter;
|
|
135
|
+
case "lessThanOrEqual":
|
|
136
|
+
return compareNumeric && numericCell <= numericFilter;
|
|
137
|
+
default:
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const cellText = String(cellValue ?? "").toLowerCase();
|
|
142
|
+
const needle = filterText.toLowerCase();
|
|
143
|
+
|
|
144
|
+
switch (operator) {
|
|
145
|
+
case "contains":
|
|
146
|
+
return cellText.includes(needle);
|
|
147
|
+
case "equals":
|
|
148
|
+
return compareNumeric ? numericCell === numericFilter : cellText === needle;
|
|
149
|
+
case "notEquals":
|
|
150
|
+
return compareNumeric ? numericCell !== numericFilter : cellText !== needle;
|
|
151
|
+
case "startsWith":
|
|
152
|
+
return cellText.startsWith(needle);
|
|
153
|
+
case "endsWith":
|
|
154
|
+
return cellText.endsWith(needle);
|
|
155
|
+
default:
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* The single filter function every TMDataGrid column uses. It dispatches on the
|
|
162
|
+
* operator carried by the filter value, which is what lets one column offer
|
|
163
|
+
* "contains" / "does not equal" / "is empty" without redefining the column.
|
|
164
|
+
*/
|
|
165
|
+
export function tmDataGridFilterFn<
|
|
166
|
+
TFeatures extends TableFeatures,
|
|
167
|
+
TData extends RowData,
|
|
168
|
+
>(row: Row<TFeatures, TData>, columnId: string, filterValue: unknown): boolean {
|
|
169
|
+
if (!isTMDataGridFilterValue(filterValue)) return true;
|
|
170
|
+
return matchesFilter(row.getValue(columnId), filterValue);
|
|
171
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The grid's icon set, aliased in one place so swapping icon libraries is a
|
|
3
|
+
* single-file change.
|
|
4
|
+
*
|
|
5
|
+
* `@tabler/icons-react` ships one ES module per icon and declares
|
|
6
|
+
* `sideEffects: false`, so only the icons re-exported here reach the bundle.
|
|
7
|
+
*/
|
|
8
|
+
export {
|
|
9
|
+
IconArrowDown as ArrowDownIcon,
|
|
10
|
+
IconArrowLeft as MoveLeftIcon,
|
|
11
|
+
IconArrowRight as MoveRightIcon,
|
|
12
|
+
IconArrowUp as ArrowUpIcon,
|
|
13
|
+
IconChevronLeft as ChevronLeftIcon,
|
|
14
|
+
IconChevronRight as ChevronRightIcon,
|
|
15
|
+
IconColumns3 as ColumnsIcon,
|
|
16
|
+
IconDotsVertical as DotsVerticalIcon,
|
|
17
|
+
IconEyeOff as EyeOffIcon,
|
|
18
|
+
IconFilter as FilterIcon,
|
|
19
|
+
IconArrowBarToLeft as PinLeftIcon,
|
|
20
|
+
IconArrowBarToRight as PinRightIcon,
|
|
21
|
+
IconMenu2 as BurgerIcon,
|
|
22
|
+
IconPinnedOff as PinOffIcon,
|
|
23
|
+
IconSearch as SearchIcon,
|
|
24
|
+
IconX as CloseIcon,
|
|
25
|
+
} from "@tabler/icons-react";
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
export { TMDataGrid, type TMDataGridProps } from "./TMDataGrid";
|
|
2
|
+
export {
|
|
3
|
+
useTMDataGridContext,
|
|
4
|
+
type TMDataGridContextValue,
|
|
5
|
+
type TMDataGridRowData,
|
|
6
|
+
} from "./TMDataGridContext";
|
|
7
|
+
export {
|
|
8
|
+
createTMDataGridColumnHelper,
|
|
9
|
+
openColumnFilter,
|
|
10
|
+
SELECT_COLUMN_ID,
|
|
11
|
+
tmDataGridFeatures,
|
|
12
|
+
type TMDataGridApi,
|
|
13
|
+
type TMDataGridColumnMeta,
|
|
14
|
+
type TMDataGridFeatures,
|
|
15
|
+
type TMDataGridTable,
|
|
16
|
+
type TMDataGridTableMeta,
|
|
17
|
+
type TMDataGridUiActions,
|
|
18
|
+
type TMDataGridUiState,
|
|
19
|
+
type TMDataGridUiStore,
|
|
20
|
+
useTMDataGrid,
|
|
21
|
+
type UseTMDataGridOptions,
|
|
22
|
+
} from "./useTMDataGrid";
|
|
23
|
+
export {
|
|
24
|
+
type TMDataGridColumnType,
|
|
25
|
+
type TMDataGridFilterOperator,
|
|
26
|
+
type TMDataGridFilterValue,
|
|
27
|
+
FILTER_OPERATOR_LABELS,
|
|
28
|
+
isFilterActive,
|
|
29
|
+
} from "./filterOperators";
|
|
30
|
+
export {
|
|
31
|
+
getColumnLabel,
|
|
32
|
+
getColumnType,
|
|
33
|
+
isColumnReorderable,
|
|
34
|
+
} from "./columnUtils";
|
|
35
|
+
export {
|
|
36
|
+
getColumnRegion,
|
|
37
|
+
getStepTargetColumn,
|
|
38
|
+
moveColumn,
|
|
39
|
+
moveColumnByStep,
|
|
40
|
+
type ColumnStepArgs,
|
|
41
|
+
type MoveColumnArgs,
|
|
42
|
+
type TMDataGridColumnRegion,
|
|
43
|
+
type TMDataGridDropSide,
|
|
44
|
+
} from "./columnOrdering";
|
|
45
|
+
export {
|
|
46
|
+
getColumnCapabilities,
|
|
47
|
+
getGridCapabilities,
|
|
48
|
+
readFeatureFlags,
|
|
49
|
+
type TMDataGridCapabilities,
|
|
50
|
+
type TMDataGridColumnCapabilities,
|
|
51
|
+
type TMDataGridFeatureFlags,
|
|
52
|
+
} from "./capabilities";
|
|
53
|
+
export {
|
|
54
|
+
DEFAULT_TMDATAGRID_SIZE,
|
|
55
|
+
SIZE_CONTROL_SIZE,
|
|
56
|
+
SIZE_ROW_HEIGHT,
|
|
57
|
+
type TMDataGridSize,
|
|
58
|
+
} from "./sizes";
|
|
59
|
+
export {
|
|
60
|
+
DATA_STATE_SLICES,
|
|
61
|
+
SETTINGS_STATE_SLICES,
|
|
62
|
+
type TMDataGridDataSlice,
|
|
63
|
+
type TMDataGridPersistence,
|
|
64
|
+
type TMDataGridPersistKey,
|
|
65
|
+
type TMDataGridSettingsSlice,
|
|
66
|
+
type TMDataGridStorageMode,
|
|
67
|
+
} from "./persistence";
|
|
68
|
+
export type { TMDataGridColumnLayout } from "./TMDataGridTable";
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import type { TableState } from "@tanstack/react-table";
|
|
2
|
+
import type { TMDataGridFeatures } from "./useTMDataGrid";
|
|
3
|
+
|
|
4
|
+
type GridState = TableState<TMDataGridFeatures>;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Persistence deliberately does not use Mantine's `useLocalStorage`.
|
|
8
|
+
*
|
|
9
|
+
* That hook owns a piece of state and returns `[value, setValue]`. Here the
|
|
10
|
+
* table already owns the state and storage only mirrors it, so routing writes
|
|
11
|
+
* through the hook would keep a second copy and trigger a React state update on
|
|
12
|
+
* every change, including every pointer move during a column resize. Its
|
|
13
|
+
* defaults also work against this use: `getInitialValueInEffect: true` delivers
|
|
14
|
+
* the stored value after mount, while `initialState` is only read on the first
|
|
15
|
+
* render, and `sync: true` would let two open tabs overwrite each other's
|
|
16
|
+
* column layout.
|
|
17
|
+
*
|
|
18
|
+
* The option vocabulary follows Mantine's: `StorageType` is its type, and
|
|
19
|
+
* `serialize` / `deserialize` behave as they do in `UseStorageOptions`.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* State slices describing what the user is looking at. Derived from the data,
|
|
24
|
+
* so they can go stale as it changes.
|
|
25
|
+
*/
|
|
26
|
+
export const DATA_STATE_SLICES = [
|
|
27
|
+
"columnFilters",
|
|
28
|
+
"globalFilter",
|
|
29
|
+
"sorting",
|
|
30
|
+
"pagination",
|
|
31
|
+
] as const satisfies ReadonlyArray<keyof GridState>;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* State slices describing how the user configured the grid. Independent of the
|
|
35
|
+
* data, and the ones users notice losing.
|
|
36
|
+
*/
|
|
37
|
+
export const SETTINGS_STATE_SLICES = [
|
|
38
|
+
"columnVisibility",
|
|
39
|
+
"columnSizing",
|
|
40
|
+
"columnOrder",
|
|
41
|
+
"columnPinning",
|
|
42
|
+
] as const satisfies ReadonlyArray<keyof GridState>;
|
|
43
|
+
|
|
44
|
+
export type TMDataGridDataSlice = (typeof DATA_STATE_SLICES)[number];
|
|
45
|
+
export type TMDataGridSettingsSlice = (typeof SETTINGS_STATE_SLICES)[number];
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* A storage key, optionally narrowed to a subset of its group's slices.
|
|
49
|
+
*
|
|
50
|
+
* - `"employees.data"` — persists every slice in the group.
|
|
51
|
+
* - `["employees.data", ["sorting"]]` — persists only the listed slices.
|
|
52
|
+
*/
|
|
53
|
+
export type TMDataGridPersistKey<TSlice extends string> =
|
|
54
|
+
| string
|
|
55
|
+
| readonly [key: string, slices: readonly TSlice[]];
|
|
56
|
+
|
|
57
|
+
/** Same values as Mantine's `StorageType`, which the package does not re-export. */
|
|
58
|
+
export type TMDataGridStorageMode = "localStorage" | "sessionStorage";
|
|
59
|
+
|
|
60
|
+
export type TMDataGridPersistence = {
|
|
61
|
+
/** Key for filters, global filter, sorting and pagination. */
|
|
62
|
+
dataKey?: TMDataGridPersistKey<TMDataGridDataSlice>;
|
|
63
|
+
/** Key for column visibility, sizing, order and pinning. */
|
|
64
|
+
settingsKey?: TMDataGridPersistKey<TMDataGridSettingsSlice>;
|
|
65
|
+
/** Which storage area to use. Defaults to `"localStorage"`. */
|
|
66
|
+
storageMode?: TMDataGridStorageMode;
|
|
67
|
+
/** Serializes a payload before it is stored. Defaults to `JSON.stringify`. */
|
|
68
|
+
serialize?: (value: Partial<GridState>) => string;
|
|
69
|
+
/** Parses a stored payload. Defaults to `JSON.parse`. */
|
|
70
|
+
deserialize?: (value: string) => unknown;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
type ResolvedKey = { key: string; slices: ReadonlyArray<keyof GridState> };
|
|
74
|
+
|
|
75
|
+
function resolveKey<TSlice extends string>(
|
|
76
|
+
entry: TMDataGridPersistKey<TSlice> | undefined,
|
|
77
|
+
defaultSlices: ReadonlyArray<keyof GridState>,
|
|
78
|
+
): ResolvedKey | null {
|
|
79
|
+
if (!entry) return null;
|
|
80
|
+
if (typeof entry === "string") return { key: entry, slices: defaultSlices };
|
|
81
|
+
|
|
82
|
+
const [key, slices] = entry;
|
|
83
|
+
if (!key) return null;
|
|
84
|
+
// An empty list means nothing was selected, which is not the same as "all".
|
|
85
|
+
return { key, slices: slices as ReadonlyArray<keyof GridState> };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function resolveStorage(
|
|
89
|
+
persistence: TMDataGridPersistence | undefined,
|
|
90
|
+
): Storage | null {
|
|
91
|
+
if (!persistence) return null;
|
|
92
|
+
// Absent under SSR, and access throws outright in some privacy modes.
|
|
93
|
+
try {
|
|
94
|
+
if (typeof window === "undefined") return null;
|
|
95
|
+
return persistence.storageMode === "sessionStorage"
|
|
96
|
+
? window.sessionStorage
|
|
97
|
+
: window.localStorage;
|
|
98
|
+
} catch {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function readSlices(
|
|
104
|
+
storage: Storage | null,
|
|
105
|
+
resolved: ResolvedKey | null,
|
|
106
|
+
deserialize: (value: string) => unknown,
|
|
107
|
+
): Partial<GridState> {
|
|
108
|
+
if (!storage || !resolved) return {};
|
|
109
|
+
try {
|
|
110
|
+
const raw = storage.getItem(resolved.key);
|
|
111
|
+
if (!raw) return {};
|
|
112
|
+
const parsed = deserialize(raw);
|
|
113
|
+
if (typeof parsed !== "object" || parsed === null) return {};
|
|
114
|
+
|
|
115
|
+
const record = parsed as Record<string, unknown>;
|
|
116
|
+
const restored: Record<string, unknown> = {};
|
|
117
|
+
// Only copy the selected slices. A payload written before the selection was
|
|
118
|
+
// narrowed cannot reintroduce slices the caller has since opted out of.
|
|
119
|
+
for (const slice of resolved.slices) {
|
|
120
|
+
if (slice in record) restored[slice] = record[slice];
|
|
121
|
+
}
|
|
122
|
+
return restored as Partial<GridState>;
|
|
123
|
+
} catch {
|
|
124
|
+
return {};
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function writeSlices(
|
|
129
|
+
storage: Storage | null,
|
|
130
|
+
resolved: ResolvedKey | null,
|
|
131
|
+
state: GridState,
|
|
132
|
+
serialize: (value: Partial<GridState>) => string,
|
|
133
|
+
): void {
|
|
134
|
+
if (!storage || !resolved) return;
|
|
135
|
+
const payload: Record<string, unknown> = {};
|
|
136
|
+
for (const slice of resolved.slices) payload[slice] = state[slice];
|
|
137
|
+
try {
|
|
138
|
+
storage.setItem(resolved.key, serialize(payload as Partial<GridState>));
|
|
139
|
+
} catch {
|
|
140
|
+
// Quota exceeded or storage disabled. Persistence is best effort.
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function resolveKeys(persistence: TMDataGridPersistence | undefined) {
|
|
145
|
+
return {
|
|
146
|
+
data: resolveKey(persistence?.dataKey, DATA_STATE_SLICES),
|
|
147
|
+
settings: resolveKey(persistence?.settingsKey, SETTINGS_STATE_SLICES),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** True when at least one key is configured. */
|
|
152
|
+
export function hasPersistenceKeys(
|
|
153
|
+
persistence: TMDataGridPersistence | undefined,
|
|
154
|
+
): boolean {
|
|
155
|
+
const { data, settings } = resolveKeys(persistence);
|
|
156
|
+
return data !== null || settings !== null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const defaultDeserialize = (value: string): unknown => JSON.parse(value);
|
|
160
|
+
const defaultSerialize = (value: Partial<GridState>): string =>
|
|
161
|
+
JSON.stringify(value);
|
|
162
|
+
|
|
163
|
+
/** State to merge into `initialState`, read once on mount. */
|
|
164
|
+
export function readPersistedState(
|
|
165
|
+
persistence?: TMDataGridPersistence,
|
|
166
|
+
): Partial<GridState> {
|
|
167
|
+
const storage = resolveStorage(persistence);
|
|
168
|
+
const { data, settings } = resolveKeys(persistence);
|
|
169
|
+
const deserialize = persistence?.deserialize ?? defaultDeserialize;
|
|
170
|
+
return {
|
|
171
|
+
...readSlices(storage, settings, deserialize),
|
|
172
|
+
...readSlices(storage, data, deserialize),
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Writes both payloads. Called on every table state change. */
|
|
177
|
+
export function writePersistedState(
|
|
178
|
+
state: GridState,
|
|
179
|
+
persistence?: TMDataGridPersistence,
|
|
180
|
+
): void {
|
|
181
|
+
const storage = resolveStorage(persistence);
|
|
182
|
+
if (!storage) return;
|
|
183
|
+
const { data, settings } = resolveKeys(persistence);
|
|
184
|
+
const serialize = persistence?.serialize ?? defaultSerialize;
|
|
185
|
+
writeSlices(storage, data, state, serialize);
|
|
186
|
+
writeSlices(storage, settings, state, serialize);
|
|
187
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { MantineSize } from "@mantine/core";
|
|
2
|
+
|
|
3
|
+
export type TMDataGridSize = MantineSize;
|
|
4
|
+
|
|
5
|
+
export const DEFAULT_TMDATAGRID_SIZE: TMDataGridSize = "md";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Row height per size, in px.
|
|
9
|
+
*
|
|
10
|
+
* The virtualizer needs this as a number — it cannot read the CSS variable —
|
|
11
|
+
* so the scale lives here and `TMDataGrid.module.css` mirrors it. `meta.rowHeight`
|
|
12
|
+
* overrides it when a grid needs a height the scale doesn't offer.
|
|
13
|
+
*/
|
|
14
|
+
export const SIZE_ROW_HEIGHT: Record<TMDataGridSize, number> = {
|
|
15
|
+
xs: 34,
|
|
16
|
+
sm: 42,
|
|
17
|
+
md: 52,
|
|
18
|
+
lg: 62,
|
|
19
|
+
xl: 72,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/** Size of the Mantine controls rendered inside the grid chrome. */
|
|
23
|
+
export const SIZE_CONTROL_SIZE: Record<TMDataGridSize, MantineSize> = {
|
|
24
|
+
xs: "xs",
|
|
25
|
+
sm: "xs",
|
|
26
|
+
md: "sm",
|
|
27
|
+
lg: "sm",
|
|
28
|
+
xl: "md",
|
|
29
|
+
};
|