@jielga/tmdatagrid 0.2.0 → 0.5.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/dist/index.d.ts +1337 -94
- package/dist/index.js +2812 -1189
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +2 -2
- package/skills/columns/SKILL.md +1 -1
- package/skills/features/SKILL.md +61 -11
- package/skills/getting-started/SKILL.md +41 -6
- package/skills/options/SKILL.md +12 -2
- package/skills/server-side/SKILL.md +1 -1
- package/src/tmdatagrid/TMDataGridContext.ts +26 -0
- package/src/tmdatagrid/components/TMDataGrid.module.css +21 -1
- package/src/tmdatagrid/components/TMDataGrid.tsx +29 -1
- package/src/tmdatagrid/components/TMDataGridColumnsPanel.tsx +12 -6
- package/src/tmdatagrid/components/TMDataGridDetailsColumn.module.css +33 -0
- package/src/tmdatagrid/components/TMDataGridDetailsColumn.tsx +160 -0
- package/src/tmdatagrid/components/TMDataGridFilterPanel.module.css +15 -0
- package/src/tmdatagrid/components/TMDataGridFilterPanel.tsx +192 -37
- package/src/tmdatagrid/components/TMDataGridFilterPills.module.css +35 -0
- package/src/tmdatagrid/components/TMDataGridFilterPills.tsx +136 -0
- package/src/tmdatagrid/components/TMDataGridFooter.module.css +8 -0
- package/src/tmdatagrid/components/TMDataGridFooter.tsx +63 -43
- package/src/tmdatagrid/components/TMDataGridGroupColumn.module.css +48 -0
- package/src/tmdatagrid/components/TMDataGridGroupColumn.tsx +159 -0
- package/src/tmdatagrid/components/TMDataGridHeaderCell.module.css +18 -0
- package/src/tmdatagrid/components/TMDataGridHeaderCell.tsx +249 -44
- package/src/tmdatagrid/components/TMDataGridSearch.tsx +122 -0
- package/src/tmdatagrid/components/TMDataGridSelectColumn.tsx +103 -16
- package/src/tmdatagrid/components/TMDataGridTable.module.css +160 -5
- package/src/tmdatagrid/components/TMDataGridTable.tsx +1380 -43
- package/src/tmdatagrid/components/TMDataGridToolbar.tsx +33 -7
- package/src/tmdatagrid/components/icons.ts +6 -0
- package/src/tmdatagrid/components/sticky.module.css +77 -16
- package/src/tmdatagrid/core/autosize.ts +125 -0
- package/src/tmdatagrid/core/capabilities.ts +122 -11
- package/src/tmdatagrid/core/cellExport.ts +321 -0
- package/src/tmdatagrid/core/cellNavigation.ts +103 -0
- package/src/tmdatagrid/core/cellRange.ts +118 -0
- package/src/tmdatagrid/core/columnOptions.ts +123 -0
- package/src/tmdatagrid/core/columnUtils.ts +18 -0
- package/src/tmdatagrid/core/expanding.ts +93 -0
- package/src/tmdatagrid/core/filterOperators.ts +198 -12
- package/src/tmdatagrid/core/grouping.ts +21 -0
- package/src/tmdatagrid/core/labels.ts +211 -0
- package/src/tmdatagrid/core/labelsSv.ts +108 -0
- package/src/tmdatagrid/core/persistence.ts +12 -0
- package/src/tmdatagrid/core/rowSelection.ts +202 -0
- package/src/tmdatagrid/core/summary.ts +35 -0
- package/src/tmdatagrid/index.ts +96 -2
- package/src/tmdatagrid/useTMDataGrid.tsx +558 -37
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { ExpandedState, Row, RowData } from "@tanstack/react-table";
|
|
2
|
+
import type { TMDataGridFeatures } from "../useTMDataGrid";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Which rows an expand-all control is about.
|
|
6
|
+
*
|
|
7
|
+
* TanStack keeps one `expanded` state, and the grid opens two unrelated things
|
|
8
|
+
* out of it: a group row opens into its children, a data row opens into its
|
|
9
|
+
* detail panel. So `toggleAllRowsExpanded` — which writes the state's
|
|
10
|
+
* whole-table form — is the wrong verb for either control. "Expand all groups"
|
|
11
|
+
* in the tree menu would open every panel in the grid, and the details lane's
|
|
12
|
+
* chevron would unfold the whole tree.
|
|
13
|
+
*
|
|
14
|
+
* These helpers narrow both to the rows their control is for, and leave the
|
|
15
|
+
* other kind exactly as it was.
|
|
16
|
+
*/
|
|
17
|
+
export type TMDataGridExpandTarget = "groups" | "details";
|
|
18
|
+
|
|
19
|
+
function isTarget<TData extends RowData>(
|
|
20
|
+
row: Row<TMDataGridFeatures, TData>,
|
|
21
|
+
target: TMDataGridExpandTarget,
|
|
22
|
+
): boolean {
|
|
23
|
+
return target === "groups" ? row.getIsGrouped() : !row.getIsGrouped();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The state as a map. `true` is the whole-table form, which has to be written
|
|
28
|
+
* out before one kind of row can be taken back out of it — the same thing
|
|
29
|
+
* TanStack does before toggling a single row.
|
|
30
|
+
*/
|
|
31
|
+
function toExpandedMap<TData extends RowData>(
|
|
32
|
+
rows: ReadonlyArray<Row<TMDataGridFeatures, TData>>,
|
|
33
|
+
expanded: ExpandedState,
|
|
34
|
+
): Record<string, boolean> {
|
|
35
|
+
if (expanded !== true) return { ...expanded };
|
|
36
|
+
const all: Record<string, boolean> = {};
|
|
37
|
+
for (const row of rows) all[row.id] = true;
|
|
38
|
+
return all;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type TMDataGridExpandAllArgs<TData extends RowData> = {
|
|
42
|
+
/**
|
|
43
|
+
* Every row in the model, groups and records alike — `flatRows` off any of
|
|
44
|
+
* them. A grouped model lists its leaves both under their group and in the
|
|
45
|
+
* flat list, so this may repeat rows; both helpers are written so that a
|
|
46
|
+
* repeat costs nothing.
|
|
47
|
+
*/
|
|
48
|
+
rows: ReadonlyArray<Row<TMDataGridFeatures, TData>>;
|
|
49
|
+
expanded: ExpandedState;
|
|
50
|
+
target: TMDataGridExpandTarget;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Whether every row of this kind is open — what the control shows, and what a
|
|
55
|
+
* bare toggle inverts.
|
|
56
|
+
*
|
|
57
|
+
* `false` when there are none of them, matching TanStack's own
|
|
58
|
+
* `getIsAllRowsExpanded()` on an empty state: an empty set is nothing to
|
|
59
|
+
* collapse.
|
|
60
|
+
*/
|
|
61
|
+
export function areAllRowsExpanded<TData extends RowData>({
|
|
62
|
+
rows,
|
|
63
|
+
expanded,
|
|
64
|
+
target,
|
|
65
|
+
}: TMDataGridExpandAllArgs<TData>): boolean {
|
|
66
|
+
let found = false;
|
|
67
|
+
for (const row of rows) {
|
|
68
|
+
if (!isTarget(row, target)) continue;
|
|
69
|
+
found = true;
|
|
70
|
+
if (expanded === true) continue;
|
|
71
|
+
if (expanded[row.id] !== true) return false;
|
|
72
|
+
}
|
|
73
|
+
return found;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The next `expanded` state after an expand-all or collapse-all on one kind of
|
|
78
|
+
* row. Rows of the other kind keep whatever they had.
|
|
79
|
+
*/
|
|
80
|
+
export function resolveExpandAll<TData extends RowData>({
|
|
81
|
+
rows,
|
|
82
|
+
expanded,
|
|
83
|
+
target,
|
|
84
|
+
expand,
|
|
85
|
+
}: TMDataGridExpandAllArgs<TData> & { expand: boolean }): ExpandedState {
|
|
86
|
+
const next = toExpandedMap(rows, expanded);
|
|
87
|
+
for (const row of rows) {
|
|
88
|
+
if (!isTarget(row, target)) continue;
|
|
89
|
+
if (expand) next[row.id] = true;
|
|
90
|
+
else delete next[row.id];
|
|
91
|
+
}
|
|
92
|
+
return next;
|
|
93
|
+
}
|
|
@@ -7,13 +7,23 @@ import type { Row, RowData, TableFeatures } from "@tanstack/react-table";
|
|
|
7
7
|
* inside the filter *value* instead. That keeps the whole filter model plain,
|
|
8
8
|
* serialisable JSON — which is what makes it portable to a server-side
|
|
9
9
|
* `manualFiltering` table (just forward `columnFilters` to the API).
|
|
10
|
+
*
|
|
11
|
+
* `value` is a string array under `isAnyOf` / `isNoneOf` (the set the cell is
|
|
12
|
+
* tested against) and a single string everywhere else — dates travel as ISO
|
|
13
|
+
* `YYYY-MM-DD` strings, booleans as `"true"` / `"false"`. Still plain JSON.
|
|
10
14
|
*/
|
|
11
15
|
export type TMDataGridFilterValue = {
|
|
12
16
|
operator: TMDataGridFilterOperator;
|
|
13
|
-
value: string
|
|
17
|
+
value: string | ReadonlyArray<string>;
|
|
14
18
|
};
|
|
15
19
|
|
|
16
|
-
export type TMDataGridColumnType =
|
|
20
|
+
export type TMDataGridColumnType =
|
|
21
|
+
| "string"
|
|
22
|
+
| "number"
|
|
23
|
+
| "boolean"
|
|
24
|
+
| "date"
|
|
25
|
+
| "select"
|
|
26
|
+
| "multiSelect";
|
|
17
27
|
|
|
18
28
|
export type TMDataGridFilterOperator =
|
|
19
29
|
| "contains"
|
|
@@ -25,6 +35,12 @@ export type TMDataGridFilterOperator =
|
|
|
25
35
|
| "greaterThanOrEqual"
|
|
26
36
|
| "lessThan"
|
|
27
37
|
| "lessThanOrEqual"
|
|
38
|
+
| "before"
|
|
39
|
+
| "after"
|
|
40
|
+
| "onOrBefore"
|
|
41
|
+
| "onOrAfter"
|
|
42
|
+
| "isAnyOf"
|
|
43
|
+
| "isNoneOf"
|
|
28
44
|
| "isEmpty"
|
|
29
45
|
| "isNotEmpty";
|
|
30
46
|
|
|
@@ -38,6 +54,12 @@ export const FILTER_OPERATOR_LABELS: Record<TMDataGridFilterOperator, string> =
|
|
|
38
54
|
greaterThanOrEqual: "is greater than or equal to",
|
|
39
55
|
lessThan: "is less than",
|
|
40
56
|
lessThanOrEqual: "is less than or equal to",
|
|
57
|
+
before: "is before",
|
|
58
|
+
after: "is after",
|
|
59
|
+
onOrBefore: "is on or before",
|
|
60
|
+
onOrAfter: "is on or after",
|
|
61
|
+
isAnyOf: "is any of",
|
|
62
|
+
isNoneOf: "is none of",
|
|
41
63
|
isEmpty: "is empty",
|
|
42
64
|
isNotEmpty: "is not empty",
|
|
43
65
|
};
|
|
@@ -63,28 +85,95 @@ const NUMBER_OPERATORS: readonly TMDataGridFilterOperator[] = [
|
|
|
63
85
|
"isNotEmpty",
|
|
64
86
|
];
|
|
65
87
|
|
|
88
|
+
// `equals` doubles as a date's "is": it compares by calendar day whenever the
|
|
89
|
+
// cell holds a Date (see matchesFilter), so a separate `is` operator would be
|
|
90
|
+
// the same comparison under a second name.
|
|
91
|
+
const DATE_OPERATORS: readonly TMDataGridFilterOperator[] = [
|
|
92
|
+
"equals",
|
|
93
|
+
"notEquals",
|
|
94
|
+
"before",
|
|
95
|
+
"after",
|
|
96
|
+
"onOrBefore",
|
|
97
|
+
"onOrAfter",
|
|
98
|
+
"isEmpty",
|
|
99
|
+
"isNotEmpty",
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
const BOOLEAN_OPERATORS: readonly TMDataGridFilterOperator[] = [
|
|
103
|
+
"equals",
|
|
104
|
+
"notEquals",
|
|
105
|
+
"isEmpty",
|
|
106
|
+
"isNotEmpty",
|
|
107
|
+
];
|
|
108
|
+
|
|
109
|
+
// One set for both select types: the operators are about membership in the
|
|
110
|
+
// filter's set, which reads the same whether the cell holds one value or many.
|
|
111
|
+
const SELECT_OPERATORS: readonly TMDataGridFilterOperator[] = [
|
|
112
|
+
"isAnyOf",
|
|
113
|
+
"isNoneOf",
|
|
114
|
+
"isEmpty",
|
|
115
|
+
"isNotEmpty",
|
|
116
|
+
];
|
|
117
|
+
|
|
66
118
|
/** Operators that ignore the value input (the input is disabled for these). */
|
|
67
119
|
const VALUELESS_OPERATORS: readonly TMDataGridFilterOperator[] = [
|
|
68
120
|
"isEmpty",
|
|
69
121
|
"isNotEmpty",
|
|
70
122
|
];
|
|
71
123
|
|
|
124
|
+
/** Operators whose value is a set of strings rather than one. */
|
|
125
|
+
const ARRAY_OPERATORS: readonly TMDataGridFilterOperator[] = [
|
|
126
|
+
"isAnyOf",
|
|
127
|
+
"isNoneOf",
|
|
128
|
+
];
|
|
129
|
+
|
|
130
|
+
const OPERATORS_BY_TYPE: Record<
|
|
131
|
+
TMDataGridColumnType,
|
|
132
|
+
readonly TMDataGridFilterOperator[]
|
|
133
|
+
> = {
|
|
134
|
+
string: STRING_OPERATORS,
|
|
135
|
+
number: NUMBER_OPERATORS,
|
|
136
|
+
boolean: BOOLEAN_OPERATORS,
|
|
137
|
+
date: DATE_OPERATORS,
|
|
138
|
+
select: SELECT_OPERATORS,
|
|
139
|
+
multiSelect: SELECT_OPERATORS,
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const DEFAULT_OPERATOR_BY_TYPE: Record<
|
|
143
|
+
TMDataGridColumnType,
|
|
144
|
+
TMDataGridFilterOperator
|
|
145
|
+
> = {
|
|
146
|
+
string: "contains",
|
|
147
|
+
number: "equals",
|
|
148
|
+
boolean: "equals",
|
|
149
|
+
date: "equals",
|
|
150
|
+
select: "isAnyOf",
|
|
151
|
+
multiSelect: "isAnyOf",
|
|
152
|
+
};
|
|
153
|
+
|
|
72
154
|
export function getOperatorsForType(
|
|
73
155
|
type: TMDataGridColumnType,
|
|
74
156
|
): readonly TMDataGridFilterOperator[] {
|
|
75
|
-
return type
|
|
157
|
+
return OPERATORS_BY_TYPE[type];
|
|
76
158
|
}
|
|
77
159
|
|
|
78
160
|
export function getDefaultOperator(
|
|
79
161
|
type: TMDataGridColumnType,
|
|
80
162
|
): TMDataGridFilterOperator {
|
|
81
|
-
return type
|
|
163
|
+
return DEFAULT_OPERATOR_BY_TYPE[type];
|
|
82
164
|
}
|
|
83
165
|
|
|
84
166
|
export function operatorNeedsValue(operator: TMDataGridFilterOperator): boolean {
|
|
85
167
|
return !VALUELESS_OPERATORS.includes(operator);
|
|
86
168
|
}
|
|
87
169
|
|
|
170
|
+
/** Whether the operator's value is a string array — `isAnyOf` / `isNoneOf`. */
|
|
171
|
+
export function operatorTakesArrayValue(
|
|
172
|
+
operator: TMDataGridFilterOperator,
|
|
173
|
+
): boolean {
|
|
174
|
+
return ARRAY_OPERATORS.includes(operator);
|
|
175
|
+
}
|
|
176
|
+
|
|
88
177
|
export function isTMDataGridFilterValue(
|
|
89
178
|
value: unknown,
|
|
90
179
|
): value is TMDataGridFilterValue {
|
|
@@ -103,23 +192,113 @@ export function isTMDataGridFilterValue(
|
|
|
103
192
|
*/
|
|
104
193
|
export function isFilterActive(value: unknown): boolean {
|
|
105
194
|
if (!isTMDataGridFilterValue(value)) return false;
|
|
106
|
-
|
|
195
|
+
if (!operatorNeedsValue(value.operator)) return true;
|
|
196
|
+
return Array.isArray(value.value)
|
|
197
|
+
? value.value.length > 0
|
|
198
|
+
: typeof value.value === "string" && value.value.trim() !== "";
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* One-line description of a single filter, as shown on a filter pill.
|
|
203
|
+
*
|
|
204
|
+
* The type's default operator is left implicit — "First name: Sofia" reads the
|
|
205
|
+
* way a person would say it — while any other operator is spelled out, since
|
|
206
|
+
* that is the part a reader cannot guess.
|
|
207
|
+
*/
|
|
208
|
+
export function formatFilterLabel({
|
|
209
|
+
label,
|
|
210
|
+
type,
|
|
211
|
+
filter,
|
|
212
|
+
operatorLabels = FILTER_OPERATOR_LABELS,
|
|
213
|
+
}: {
|
|
214
|
+
label: string;
|
|
215
|
+
type: TMDataGridColumnType;
|
|
216
|
+
filter: TMDataGridFilterValue;
|
|
217
|
+
/** Localized operator names — `labels.operators`. Defaults to English. */
|
|
218
|
+
operatorLabels?: Record<TMDataGridFilterOperator, string>;
|
|
219
|
+
}): string {
|
|
220
|
+
const { operator, value } = filter;
|
|
221
|
+
if (!operatorNeedsValue(operator)) {
|
|
222
|
+
return `${label} ${operatorLabels[operator]}`;
|
|
223
|
+
}
|
|
224
|
+
const text = Array.isArray(value) ? value.join(", ") : String(value).trim();
|
|
225
|
+
if (operator === getDefaultOperator(type)) return `${label}: ${text}`;
|
|
226
|
+
return `${label} ${operatorLabels[operator]} ${text}`;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* A value's calendar day as `YYYY-MM-DD`, or `null` for anything that has no
|
|
231
|
+
* day to speak of. A `Date` is read in local time — `sv-SE` formats as ISO,
|
|
232
|
+
* which is the shape `<input type="date">` produced on the other side — and a
|
|
233
|
+
* string contributes its leading ISO day, so both `2026-07-31` and a full
|
|
234
|
+
* timestamp compare by day.
|
|
235
|
+
*/
|
|
236
|
+
function toIsoDay(value: unknown): string | null {
|
|
237
|
+
if (value instanceof Date) {
|
|
238
|
+
return Number.isNaN(value.getTime())
|
|
239
|
+
? null
|
|
240
|
+
: value.toLocaleDateString("sv-SE");
|
|
241
|
+
}
|
|
242
|
+
if (typeof value === "string") {
|
|
243
|
+
const match = /^(\d{4}-\d{2}-\d{2})/.exec(value.trim());
|
|
244
|
+
return match ? match[1] : null;
|
|
245
|
+
}
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function isEmptyCell(cellValue: unknown): boolean {
|
|
250
|
+
return (
|
|
251
|
+
cellValue === null ||
|
|
252
|
+
cellValue === undefined ||
|
|
253
|
+
cellValue === "" ||
|
|
254
|
+
(Array.isArray(cellValue) && cellValue.length === 0)
|
|
255
|
+
);
|
|
107
256
|
}
|
|
108
257
|
|
|
109
258
|
export function matchesFilter(
|
|
110
259
|
cellValue: unknown,
|
|
111
260
|
{ operator, value }: TMDataGridFilterValue,
|
|
112
261
|
): boolean {
|
|
113
|
-
if (operator === "isEmpty")
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
262
|
+
if (operator === "isEmpty") return isEmptyCell(cellValue);
|
|
263
|
+
if (operator === "isNotEmpty") return !isEmptyCell(cellValue);
|
|
264
|
+
|
|
265
|
+
// Set membership. The cell side is also a list — a multiSelect cell holds an
|
|
266
|
+
// array — so "any of" is an intersection test and "none of" its complement.
|
|
267
|
+
if (operator === "isAnyOf" || operator === "isNoneOf") {
|
|
268
|
+
const set = (Array.isArray(value) ? value : [value])
|
|
269
|
+
.map((entry) => String(entry).toLowerCase())
|
|
270
|
+
.filter((entry) => entry !== "");
|
|
271
|
+
if (set.length === 0) return true;
|
|
272
|
+
const cellEntries = Array.isArray(cellValue) ? cellValue : [cellValue];
|
|
273
|
+
const hit = cellEntries.some((entry) =>
|
|
274
|
+
set.includes(String(entry ?? "").toLowerCase()),
|
|
275
|
+
);
|
|
276
|
+
return operator === "isAnyOf" ? hit : !hit;
|
|
118
277
|
}
|
|
119
278
|
|
|
120
|
-
const filterText =
|
|
279
|
+
const filterText =
|
|
280
|
+
typeof value === "string" ? value.trim() : value.join(",").trim();
|
|
121
281
|
if (filterText === "") return true;
|
|
122
282
|
|
|
283
|
+
// Day ordering. ISO days compare correctly as strings, which keeps this
|
|
284
|
+
// free of timezone arithmetic — the whole comparison lives in local days.
|
|
285
|
+
switch (operator) {
|
|
286
|
+
case "before":
|
|
287
|
+
case "after":
|
|
288
|
+
case "onOrBefore":
|
|
289
|
+
case "onOrAfter": {
|
|
290
|
+
const cellDay = toIsoDay(cellValue);
|
|
291
|
+
const filterDay = toIsoDay(filterText);
|
|
292
|
+
if (cellDay === null || filterDay === null) return false;
|
|
293
|
+
if (operator === "before") return cellDay < filterDay;
|
|
294
|
+
if (operator === "after") return cellDay > filterDay;
|
|
295
|
+
if (operator === "onOrBefore") return cellDay <= filterDay;
|
|
296
|
+
return cellDay >= filterDay;
|
|
297
|
+
}
|
|
298
|
+
default:
|
|
299
|
+
break;
|
|
300
|
+
}
|
|
301
|
+
|
|
123
302
|
const numericCell = typeof cellValue === "number" ? cellValue : Number.NaN;
|
|
124
303
|
const numericFilter = Number(filterText);
|
|
125
304
|
const compareNumeric =
|
|
@@ -138,7 +317,14 @@ export function matchesFilter(
|
|
|
138
317
|
break;
|
|
139
318
|
}
|
|
140
319
|
|
|
141
|
-
|
|
320
|
+
// A Date compares by its calendar day, which is what makes `equals` on a
|
|
321
|
+
// date column mean "is this day" rather than a doomed string comparison
|
|
322
|
+
// against the Date's default stringification.
|
|
323
|
+
const cellText = (
|
|
324
|
+
cellValue instanceof Date
|
|
325
|
+
? (toIsoDay(cellValue) ?? "")
|
|
326
|
+
: String(cellValue ?? "")
|
|
327
|
+
).toLowerCase();
|
|
142
328
|
const needle = filterText.toLowerCase();
|
|
143
329
|
|
|
144
330
|
switch (operator) {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Row, RowData } from "@tanstack/react-table";
|
|
2
|
+
import type { TMDataGridFeatures } from "../useTMDataGrid";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The data rows under a group row, at any depth.
|
|
6
|
+
*
|
|
7
|
+
* Not `row.getLeafRows()`, despite the name: that flattens the whole subtree
|
|
8
|
+
* and keeps the branches, so a group nested two deep reports its sub-groups
|
|
9
|
+
* alongside the records. Grouping `city` then `name` would have Stockholm
|
|
10
|
+
* counting four rows and three names as seven, and one tick on it selecting
|
|
11
|
+
* ids that hold no record.
|
|
12
|
+
*
|
|
13
|
+
* A row with no subRows is a data row, which also makes this the identity on an
|
|
14
|
+
* ungrouped grid.
|
|
15
|
+
*/
|
|
16
|
+
export function getGroupDataRows<TData extends RowData>(
|
|
17
|
+
row: Row<TMDataGridFeatures, TData>,
|
|
18
|
+
): Array<Row<TMDataGridFeatures, TData>> {
|
|
19
|
+
if (row.subRows.length === 0) return [row];
|
|
20
|
+
return row.getLeafRows().filter((leaf) => leaf.subRows.length === 0);
|
|
21
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import {
|
|
2
|
+
FILTER_OPERATOR_LABELS,
|
|
3
|
+
type TMDataGridFilterOperator,
|
|
4
|
+
} from "./filterOperators";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Every user-facing string in the grid — menu items, panels, tooltips, the
|
|
8
|
+
* pager, and the `aria-label`s a screen reader speaks. One flat object, so an
|
|
9
|
+
* override is a spread rather than a deep merge; `operators` is the single
|
|
10
|
+
* nested record and {@link mergeLabels} folds it separately.
|
|
11
|
+
*
|
|
12
|
+
* Labels that carry a value are functions rather than template strings, so a
|
|
13
|
+
* language is free to put the value wherever its grammar wants it.
|
|
14
|
+
*/
|
|
15
|
+
export type TMDataGridLabels = {
|
|
16
|
+
/** Filter operator names, as shown in the operator dropdown and on pills. */
|
|
17
|
+
operators: Record<TMDataGridFilterOperator, string>;
|
|
18
|
+
|
|
19
|
+
// Toolbar
|
|
20
|
+
/** "Manage columns" — the burger button and the header menu item. */
|
|
21
|
+
manageColumns: string;
|
|
22
|
+
/** "Filters" — the funnel button and the filter panel's title. */
|
|
23
|
+
filters: string;
|
|
24
|
+
searchPlaceholder: string;
|
|
25
|
+
searchLabel: string;
|
|
26
|
+
clearSearch: string;
|
|
27
|
+
|
|
28
|
+
// Columns panel
|
|
29
|
+
columnsSearchPlaceholder: string;
|
|
30
|
+
columnsNoMatch: (search: string) => string;
|
|
31
|
+
columnsShowHideAll: string;
|
|
32
|
+
columnsReset: string;
|
|
33
|
+
|
|
34
|
+
// Filter panel
|
|
35
|
+
filterColumn: string;
|
|
36
|
+
filterOperator: string;
|
|
37
|
+
filterValue: string;
|
|
38
|
+
filterValuePlaceholder: string;
|
|
39
|
+
/** What a boolean column's `true` reads as — its filter choice and cell text. */
|
|
40
|
+
booleanTrue: string;
|
|
41
|
+
booleanFalse: string;
|
|
42
|
+
addFilter: string;
|
|
43
|
+
clearAllFilters: string;
|
|
44
|
+
closeFilters: string;
|
|
45
|
+
removeFilter: string;
|
|
46
|
+
|
|
47
|
+
// Filter pills
|
|
48
|
+
activeFilters: string;
|
|
49
|
+
clearFilter: (column: string) => string;
|
|
50
|
+
|
|
51
|
+
// Header menu
|
|
52
|
+
sortAsc: string;
|
|
53
|
+
sortDesc: string;
|
|
54
|
+
filterMenuItem: string;
|
|
55
|
+
groupBy: (column: string) => string;
|
|
56
|
+
ungroup: (column: string) => string;
|
|
57
|
+
expandAllGroups: string;
|
|
58
|
+
collapseAllGroups: string;
|
|
59
|
+
pinLeft: string;
|
|
60
|
+
pinRight: string;
|
|
61
|
+
unpin: string;
|
|
62
|
+
moveLeft: string;
|
|
63
|
+
moveRight: string;
|
|
64
|
+
autosizeColumn: string;
|
|
65
|
+
hideColumn: string;
|
|
66
|
+
filterOn: (column: string) => string;
|
|
67
|
+
sortColumn: (column: string) => string;
|
|
68
|
+
columnMenu: (column: string) => string;
|
|
69
|
+
|
|
70
|
+
// Footer / pager
|
|
71
|
+
rowsPerPage: string;
|
|
72
|
+
pageRange: (args: { from: number; to: number; total: number }) => string;
|
|
73
|
+
groupedAllRows: (total: number) => string;
|
|
74
|
+
pagingSuspendedHint: string;
|
|
75
|
+
previousPage: string;
|
|
76
|
+
nextPage: string;
|
|
77
|
+
|
|
78
|
+
// Body
|
|
79
|
+
noResults: string;
|
|
80
|
+
loading: string;
|
|
81
|
+
|
|
82
|
+
// Cell selection menu
|
|
83
|
+
cellCount: (count: number) => string;
|
|
84
|
+
copy: string;
|
|
85
|
+
exportCsv: string;
|
|
86
|
+
includeHeaders: string;
|
|
87
|
+
|
|
88
|
+
// Generated checkbox lane
|
|
89
|
+
selectColumnLabel: string;
|
|
90
|
+
selectAllRows: string;
|
|
91
|
+
selectRow: string;
|
|
92
|
+
selectGroup: string;
|
|
93
|
+
|
|
94
|
+
// Generated details lane
|
|
95
|
+
detailsColumnLabel: string;
|
|
96
|
+
showDetails: string;
|
|
97
|
+
hideDetails: string;
|
|
98
|
+
expandAllDetails: string;
|
|
99
|
+
collapseAllDetails: string;
|
|
100
|
+
|
|
101
|
+
// Generated tree lane
|
|
102
|
+
groupColumnLabel: string;
|
|
103
|
+
blankGroupValue: string;
|
|
104
|
+
expandGroup: (label: string) => string;
|
|
105
|
+
collapseGroup: (label: string) => string;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* What the `labels` option accepts: any subset, merged over the English
|
|
110
|
+
* defaults — so `{ noResults: "Inga rader" }` is a complete configuration.
|
|
111
|
+
*/
|
|
112
|
+
export type TMDataGridLabelsOverride = Partial<
|
|
113
|
+
Omit<TMDataGridLabels, "operators">
|
|
114
|
+
> & {
|
|
115
|
+
operators?: Partial<Record<TMDataGridFilterOperator, string>>;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
/** The built-in defaults. Also the reference for what a preset must cover. */
|
|
119
|
+
export const TMDATAGRID_LABELS_EN: TMDataGridLabels = {
|
|
120
|
+
operators: FILTER_OPERATOR_LABELS,
|
|
121
|
+
|
|
122
|
+
manageColumns: "Manage columns",
|
|
123
|
+
filters: "Filters",
|
|
124
|
+
searchPlaceholder: "Search",
|
|
125
|
+
searchLabel: "Search rows",
|
|
126
|
+
clearSearch: "Clear search",
|
|
127
|
+
|
|
128
|
+
columnsSearchPlaceholder: "Search",
|
|
129
|
+
columnsNoMatch: (search) => `No columns match “${search}”`,
|
|
130
|
+
columnsShowHideAll: "Show/Hide All",
|
|
131
|
+
columnsReset: "RESET",
|
|
132
|
+
|
|
133
|
+
filterColumn: "Column",
|
|
134
|
+
filterOperator: "Operator",
|
|
135
|
+
filterValue: "Value",
|
|
136
|
+
filterValuePlaceholder: "Filter value",
|
|
137
|
+
booleanTrue: "Yes",
|
|
138
|
+
booleanFalse: "No",
|
|
139
|
+
addFilter: "Add filter",
|
|
140
|
+
clearAllFilters: "Clear all",
|
|
141
|
+
closeFilters: "Close filters",
|
|
142
|
+
removeFilter: "Remove filter",
|
|
143
|
+
|
|
144
|
+
activeFilters: "Active filters",
|
|
145
|
+
clearFilter: (column) => `Clear ${column} filter`,
|
|
146
|
+
|
|
147
|
+
sortAsc: "Sort by ASC",
|
|
148
|
+
sortDesc: "Sort by DESC",
|
|
149
|
+
filterMenuItem: "Filter",
|
|
150
|
+
groupBy: (column) => `Group by ${column}`,
|
|
151
|
+
ungroup: (column) => `Ungroup ${column}`,
|
|
152
|
+
expandAllGroups: "Expand all groups",
|
|
153
|
+
collapseAllGroups: "Collapse all groups",
|
|
154
|
+
pinLeft: "Pin to left",
|
|
155
|
+
pinRight: "Pin to right",
|
|
156
|
+
unpin: "Unpin",
|
|
157
|
+
moveLeft: "Move left",
|
|
158
|
+
moveRight: "Move right",
|
|
159
|
+
autosizeColumn: "Autosize column",
|
|
160
|
+
hideColumn: "Hide column",
|
|
161
|
+
filterOn: (column) => `Filter on ${column}`,
|
|
162
|
+
sortColumn: (column) => `Sort ${column}`,
|
|
163
|
+
columnMenu: (column) => `${column} column menu`,
|
|
164
|
+
|
|
165
|
+
rowsPerPage: "Rows per page:",
|
|
166
|
+
pageRange: ({ from, to, total }) => `${from}–${to} of ${total}`,
|
|
167
|
+
groupedAllRows: (total) => `Grouped · all ${total} rows`,
|
|
168
|
+
pagingSuspendedHint:
|
|
169
|
+
"Paging is off while the rows are grouped: the whole tree is rendered and virtualized. Ungroup to page again.",
|
|
170
|
+
previousPage: "Previous page",
|
|
171
|
+
nextPage: "Next page",
|
|
172
|
+
|
|
173
|
+
noResults: "No rows match your filters",
|
|
174
|
+
loading: "Loading",
|
|
175
|
+
|
|
176
|
+
cellCount: (count) => (count === 1 ? "1 cell" : `${count} cells`),
|
|
177
|
+
copy: "Copy",
|
|
178
|
+
exportCsv: "Export as CSV for Excel",
|
|
179
|
+
includeHeaders: "Include headers",
|
|
180
|
+
|
|
181
|
+
selectColumnLabel: "Checkbox selection",
|
|
182
|
+
selectAllRows: "Select all rows",
|
|
183
|
+
selectRow: "Select row",
|
|
184
|
+
selectGroup: "Select group",
|
|
185
|
+
|
|
186
|
+
detailsColumnLabel: "Details",
|
|
187
|
+
showDetails: "Show details",
|
|
188
|
+
hideDetails: "Hide details",
|
|
189
|
+
expandAllDetails: "Expand all details",
|
|
190
|
+
collapseAllDetails: "Collapse all details",
|
|
191
|
+
|
|
192
|
+
groupColumnLabel: "Group",
|
|
193
|
+
blankGroupValue: "(Blank)",
|
|
194
|
+
expandGroup: (label) => `Expand ${label}`,
|
|
195
|
+
collapseGroup: (label) => `Collapse ${label}`,
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
/** A partial override folded over the English defaults. */
|
|
199
|
+
export function mergeLabels(
|
|
200
|
+
override?: TMDataGridLabelsOverride,
|
|
201
|
+
): TMDataGridLabels {
|
|
202
|
+
if (!override) return TMDATAGRID_LABELS_EN;
|
|
203
|
+
return {
|
|
204
|
+
...TMDATAGRID_LABELS_EN,
|
|
205
|
+
...override,
|
|
206
|
+
operators: {
|
|
207
|
+
...TMDATAGRID_LABELS_EN.operators,
|
|
208
|
+
...override.operators,
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
}
|