@jielga/tmdatagrid 0.3.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 +947 -64
- package/dist/index.js +2474 -1423
- 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 +1 -1
- 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 +10 -0
- package/src/tmdatagrid/components/TMDataGrid.tsx +23 -1
- package/src/tmdatagrid/components/TMDataGridColumnsPanel.tsx +5 -5
- package/src/tmdatagrid/components/TMDataGridDetailsColumn.module.css +33 -0
- package/src/tmdatagrid/components/TMDataGridDetailsColumn.tsx +160 -0
- package/src/tmdatagrid/components/TMDataGridFilterPanel.tsx +111 -33
- package/src/tmdatagrid/components/TMDataGridFilterPills.tsx +5 -4
- package/src/tmdatagrid/components/TMDataGridFooter.tsx +11 -19
- package/src/tmdatagrid/components/TMDataGridGroupColumn.tsx +23 -11
- package/src/tmdatagrid/components/TMDataGridHeaderCell.module.css +15 -3
- package/src/tmdatagrid/components/TMDataGridHeaderCell.tsx +181 -52
- package/src/tmdatagrid/components/TMDataGridSearch.tsx +122 -0
- package/src/tmdatagrid/components/TMDataGridSelectColumn.tsx +23 -13
- package/src/tmdatagrid/components/TMDataGridTable.module.css +127 -6
- package/src/tmdatagrid/components/TMDataGridTable.tsx +1003 -30
- package/src/tmdatagrid/components/TMDataGridToolbar.tsx +30 -7
- package/src/tmdatagrid/components/icons.ts +1 -0
- package/src/tmdatagrid/components/sticky.module.css +77 -16
- package/src/tmdatagrid/core/autosize.ts +125 -0
- package/src/tmdatagrid/core/capabilities.ts +41 -0
- 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 +176 -15
- package/src/tmdatagrid/core/labels.ts +211 -0
- package/src/tmdatagrid/core/labelsSv.ts +108 -0
- package/src/tmdatagrid/core/summary.ts +35 -0
- package/src/tmdatagrid/index.ts +70 -0
- package/src/tmdatagrid/useTMDataGrid.tsx +328 -12
|
@@ -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,7 +192,10 @@ 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() !== "";
|
|
107
199
|
}
|
|
108
200
|
|
|
109
201
|
/**
|
|
@@ -117,34 +209,96 @@ export function formatFilterLabel({
|
|
|
117
209
|
label,
|
|
118
210
|
type,
|
|
119
211
|
filter,
|
|
212
|
+
operatorLabels = FILTER_OPERATOR_LABELS,
|
|
120
213
|
}: {
|
|
121
214
|
label: string;
|
|
122
215
|
type: TMDataGridColumnType;
|
|
123
216
|
filter: TMDataGridFilterValue;
|
|
217
|
+
/** Localized operator names — `labels.operators`. Defaults to English. */
|
|
218
|
+
operatorLabels?: Record<TMDataGridFilterOperator, string>;
|
|
124
219
|
}): string {
|
|
125
220
|
const { operator, value } = filter;
|
|
126
221
|
if (!operatorNeedsValue(operator)) {
|
|
127
|
-
return `${label} ${
|
|
222
|
+
return `${label} ${operatorLabels[operator]}`;
|
|
128
223
|
}
|
|
129
|
-
const text = value.trim();
|
|
224
|
+
const text = Array.isArray(value) ? value.join(", ") : String(value).trim();
|
|
130
225
|
if (operator === getDefaultOperator(type)) return `${label}: ${text}`;
|
|
131
|
-
return `${label} ${
|
|
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
|
+
);
|
|
132
256
|
}
|
|
133
257
|
|
|
134
258
|
export function matchesFilter(
|
|
135
259
|
cellValue: unknown,
|
|
136
260
|
{ operator, value }: TMDataGridFilterValue,
|
|
137
261
|
): boolean {
|
|
138
|
-
if (operator === "isEmpty")
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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;
|
|
143
277
|
}
|
|
144
278
|
|
|
145
|
-
const filterText =
|
|
279
|
+
const filterText =
|
|
280
|
+
typeof value === "string" ? value.trim() : value.join(",").trim();
|
|
146
281
|
if (filterText === "") return true;
|
|
147
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
|
+
|
|
148
302
|
const numericCell = typeof cellValue === "number" ? cellValue : Number.NaN;
|
|
149
303
|
const numericFilter = Number(filterText);
|
|
150
304
|
const compareNumeric =
|
|
@@ -163,7 +317,14 @@ export function matchesFilter(
|
|
|
163
317
|
break;
|
|
164
318
|
}
|
|
165
319
|
|
|
166
|
-
|
|
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();
|
|
167
328
|
const needle = filterText.toLowerCase();
|
|
168
329
|
|
|
169
330
|
switch (operator) {
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { TMDataGridLabels } from "./labels";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The complete Swedish dictionary. Typed as {@link TMDataGridLabels} rather
|
|
5
|
+
* than as an override, so a label added to the grid without a Swedish
|
|
6
|
+
* translation is a compile error here instead of English quietly leaking
|
|
7
|
+
* through.
|
|
8
|
+
*
|
|
9
|
+
* ```tsx
|
|
10
|
+
* useTMDataGrid({ data, columns, labels: TMDATAGRID_LABELS_SV });
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
export const TMDATAGRID_LABELS_SV: TMDataGridLabels = {
|
|
14
|
+
operators: {
|
|
15
|
+
contains: "innehåller",
|
|
16
|
+
equals: "är lika med",
|
|
17
|
+
notEquals: "är inte lika med",
|
|
18
|
+
startsWith: "börjar med",
|
|
19
|
+
endsWith: "slutar med",
|
|
20
|
+
greaterThan: "är större än",
|
|
21
|
+
greaterThanOrEqual: "är större än eller lika med",
|
|
22
|
+
lessThan: "är mindre än",
|
|
23
|
+
lessThanOrEqual: "är mindre än eller lika med",
|
|
24
|
+
before: "är före",
|
|
25
|
+
after: "är efter",
|
|
26
|
+
onOrBefore: "är på eller före",
|
|
27
|
+
onOrAfter: "är på eller efter",
|
|
28
|
+
isAnyOf: "är någon av",
|
|
29
|
+
isNoneOf: "är ingen av",
|
|
30
|
+
isEmpty: "är tom",
|
|
31
|
+
isNotEmpty: "är inte tom",
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
manageColumns: "Hantera kolumner",
|
|
35
|
+
filters: "Filter",
|
|
36
|
+
searchPlaceholder: "Sök",
|
|
37
|
+
searchLabel: "Sök rader",
|
|
38
|
+
clearSearch: "Rensa sökningen",
|
|
39
|
+
|
|
40
|
+
columnsSearchPlaceholder: "Sök",
|
|
41
|
+
columnsNoMatch: (search) => `Inga kolumner matchar ”${search}”`,
|
|
42
|
+
columnsShowHideAll: "Visa/dölj alla",
|
|
43
|
+
columnsReset: "ÅTERSTÄLL",
|
|
44
|
+
|
|
45
|
+
filterColumn: "Kolumn",
|
|
46
|
+
filterOperator: "Operator",
|
|
47
|
+
filterValue: "Värde",
|
|
48
|
+
filterValuePlaceholder: "Filtervärde",
|
|
49
|
+
booleanTrue: "Ja",
|
|
50
|
+
booleanFalse: "Nej",
|
|
51
|
+
addFilter: "Lägg till filter",
|
|
52
|
+
clearAllFilters: "Rensa alla",
|
|
53
|
+
closeFilters: "Stäng filter",
|
|
54
|
+
removeFilter: "Ta bort filter",
|
|
55
|
+
|
|
56
|
+
activeFilters: "Aktiva filter",
|
|
57
|
+
clearFilter: (column) => `Rensa filtret för ${column}`,
|
|
58
|
+
|
|
59
|
+
sortAsc: "Sortera stigande",
|
|
60
|
+
sortDesc: "Sortera fallande",
|
|
61
|
+
filterMenuItem: "Filtrera",
|
|
62
|
+
groupBy: (column) => `Gruppera på ${column}`,
|
|
63
|
+
ungroup: (column) => `Avgruppera ${column}`,
|
|
64
|
+
expandAllGroups: "Expandera alla grupper",
|
|
65
|
+
collapseAllGroups: "Fäll ihop alla grupper",
|
|
66
|
+
pinLeft: "Fäst till vänster",
|
|
67
|
+
pinRight: "Fäst till höger",
|
|
68
|
+
unpin: "Lossa",
|
|
69
|
+
moveLeft: "Flytta åt vänster",
|
|
70
|
+
moveRight: "Flytta åt höger",
|
|
71
|
+
autosizeColumn: "Anpassa kolumnbredd",
|
|
72
|
+
hideColumn: "Dölj kolumn",
|
|
73
|
+
filterOn: (column) => `Filtrera på ${column}`,
|
|
74
|
+
sortColumn: (column) => `Sortera ${column}`,
|
|
75
|
+
columnMenu: (column) => `Kolumnmeny för ${column}`,
|
|
76
|
+
|
|
77
|
+
rowsPerPage: "Rader per sida:",
|
|
78
|
+
pageRange: ({ from, to, total }) => `${from}–${to} av ${total}`,
|
|
79
|
+
groupedAllRows: (total) => `Grupperad · alla ${total} rader`,
|
|
80
|
+
pagingSuspendedHint:
|
|
81
|
+
"Sidindelningen är avstängd medan raderna är grupperade: hela trädet renderas och virtualiseras. Avgruppera för att bläddra igen.",
|
|
82
|
+
previousPage: "Föregående sida",
|
|
83
|
+
nextPage: "Nästa sida",
|
|
84
|
+
|
|
85
|
+
noResults: "Inga rader matchar dina filter",
|
|
86
|
+
loading: "Laddar",
|
|
87
|
+
|
|
88
|
+
cellCount: (count) => (count === 1 ? "1 cell" : `${count} celler`),
|
|
89
|
+
copy: "Kopiera",
|
|
90
|
+
exportCsv: "Exportera som CSV för Excel",
|
|
91
|
+
includeHeaders: "Inkludera rubriker",
|
|
92
|
+
|
|
93
|
+
selectColumnLabel: "Kryssrutemarkering",
|
|
94
|
+
selectAllRows: "Markera alla rader",
|
|
95
|
+
selectRow: "Markera rad",
|
|
96
|
+
selectGroup: "Markera grupp",
|
|
97
|
+
|
|
98
|
+
detailsColumnLabel: "Detaljer",
|
|
99
|
+
showDetails: "Visa detaljer",
|
|
100
|
+
hideDetails: "Dölj detaljer",
|
|
101
|
+
expandAllDetails: "Expandera alla detaljer",
|
|
102
|
+
collapseAllDetails: "Fäll ihop alla detaljer",
|
|
103
|
+
|
|
104
|
+
groupColumnLabel: "Grupp",
|
|
105
|
+
blankGroupValue: "(Tom)",
|
|
106
|
+
expandGroup: (label) => `Expandera ${label}`,
|
|
107
|
+
collapseGroup: (label) => `Fäll ihop ${label}`,
|
|
108
|
+
};
|