@lotics/ui 2.6.1 → 3.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/package.json +2 -15
- package/src/react_native.d.ts +2 -2
- package/src/segmented_control.tsx +201 -0
- package/src/cell_date.tsx +0 -30
- package/src/cell_date_format.test.ts +0 -32
- package/src/cell_date_format.ts +0 -73
- package/src/cell_number.test.ts +0 -42
- package/src/cell_number.tsx +0 -25
- package/src/cell_number_format.ts +0 -42
- package/src/cell_select.tsx +0 -68
- package/src/cell_text.tsx +0 -45
- package/src/grid/data_grid.tsx +0 -2003
- package/src/grid/data_grid_columns.test.ts +0 -72
- package/src/grid/data_grid_columns.ts +0 -30
- package/src/grid/data_grid_context.ts +0 -119
- package/src/grid/dispatch_safely.ts +0 -39
- package/src/grid/engine.module.css +0 -114
- package/src/grid/engine.tsx +0 -1042
- package/src/grid/helpers.ts +0 -205
- package/src/grid/layout.test.ts +0 -515
- package/src/grid/layout.ts +0 -425
- package/src/grid/recycling.test.ts +0 -236
- package/src/grid/recycling.ts +0 -172
- package/src/grid/row_cell.module.css +0 -105
- package/src/grid/row_cell.tsx +0 -313
- package/src/grid/search_highlight.ts +0 -71
- package/src/grid/select_cell.tsx +0 -58
- package/src/grid/select_group_summary_cell.tsx +0 -76
- package/src/grid/select_header_cell.tsx +0 -32
- package/src/grid/skeleton_row.module.css +0 -34
- package/src/grid/skeleton_row.tsx +0 -20
- package/src/grid/use_grid_groups.ts +0 -311
- package/src/grid/use_scroll_to_cell.ts +0 -135
- package/src/grid/use_virtual_grid.ts +0 -383
- package/src/grid/visibility.test.ts +0 -208
- package/src/grid/visibility.ts +0 -77
- package/src/kanban/constants.ts +0 -18
- package/src/kanban/default_renderers.tsx +0 -160
- package/src/kanban/drag_preview.tsx +0 -157
- package/src/kanban/index.ts +0 -13
- package/src/kanban/insert_card_zone.tsx +0 -135
- package/src/kanban/kanban_board.tsx +0 -635
- package/src/kanban/kanban_card.tsx +0 -321
- package/src/kanban/kanban_column.tsx +0 -499
- package/src/kanban/placeholders.tsx +0 -54
- package/src/kanban/types.ts +0 -116
package/src/grid/layout.ts
DELETED
|
@@ -1,425 +0,0 @@
|
|
|
1
|
-
import { useMemo } from "react";
|
|
2
|
-
import { isEmpty, sum, sumBy } from "./helpers";
|
|
3
|
-
|
|
4
|
-
interface UseGridLayoutParams {
|
|
5
|
-
groups: GridGroup[];
|
|
6
|
-
leafRowHeight: number;
|
|
7
|
-
groupHeadingHeight: number;
|
|
8
|
-
groupSummaryHeight: number;
|
|
9
|
-
spacerHeight: number;
|
|
10
|
-
columns: number[];
|
|
11
|
-
frozenColumnCount: number;
|
|
12
|
-
collapsedRows?: GroupPathKey[] | null;
|
|
13
|
-
/** Maximum width for frozen columns. If frozen columns exceed this, they will be scaled down proportionally. */
|
|
14
|
-
maxFrozenColumnsWidth?: number;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
interface GridLayoutData {
|
|
18
|
-
contentHeight: number;
|
|
19
|
-
contentWidth: number;
|
|
20
|
-
frozenColumns: GridColumn[];
|
|
21
|
-
frozenColumnsWidth: number;
|
|
22
|
-
scrollableColumns: GridColumn[];
|
|
23
|
-
scrollableColumnsWidth: number;
|
|
24
|
-
rows: GridRow[];
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export interface Column {
|
|
28
|
-
column: number;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export interface GridColumn extends Column {
|
|
32
|
-
width: number;
|
|
33
|
-
x: number;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Group path: array representing the positional path to a group in the hierarchy.
|
|
38
|
-
* Example: [0, 1] means the second child of the first top-level group.
|
|
39
|
-
* This is a grid-internal positioning concept, NOT a data identifier.
|
|
40
|
-
*/
|
|
41
|
-
export type GroupPath = number[];
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Row path: GroupPath + row index within that group.
|
|
45
|
-
* Example: [0, 1, 2] means row at index 2 in group [0, 1].
|
|
46
|
-
* This is a grid-internal positioning concept, NOT a data identifier.
|
|
47
|
-
*/
|
|
48
|
-
export type RowPath = number[];
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* String key for GroupPath, used as Map/Set keys for efficient lookups.
|
|
52
|
-
* Created by `groupPathToKey()`. Example: "0,1"
|
|
53
|
-
*/
|
|
54
|
-
export type GroupPathKey = string;
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* String key for RowPath, used as Map/Set keys for efficient lookups.
|
|
58
|
-
* Created by `rowPathToKey()`. Example: "0,1,2"
|
|
59
|
-
*/
|
|
60
|
-
export type RowPathKey = string;
|
|
61
|
-
|
|
62
|
-
export interface DataRow {
|
|
63
|
-
rowPath: RowPath;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export interface GridDataRow extends DataRow {
|
|
67
|
-
type: "row";
|
|
68
|
-
height: number;
|
|
69
|
-
y: number;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export interface GroupRow {
|
|
73
|
-
groupPath: GroupPath;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export interface GridGroupHeadingRow extends GroupRow {
|
|
77
|
-
type: "group_heading";
|
|
78
|
-
height: number;
|
|
79
|
-
y: number;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
export interface GridGroupSummaryRow extends GroupRow {
|
|
83
|
-
type: "group_summary";
|
|
84
|
-
height: number;
|
|
85
|
-
y: number;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export interface GridGroupRow extends GroupRow {
|
|
89
|
-
type: "group";
|
|
90
|
-
height: number;
|
|
91
|
-
y: number;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
export interface GridSpacerRow {
|
|
95
|
-
type: "spacer";
|
|
96
|
-
y: number;
|
|
97
|
-
height: number;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
export type GridRow = GridDataRow | GridGroupHeadingRow | GridGroupSummaryRow | GridSpacerRow;
|
|
101
|
-
|
|
102
|
-
export function useGridLayout(params: UseGridLayoutParams): GridLayoutData {
|
|
103
|
-
const {
|
|
104
|
-
columns,
|
|
105
|
-
frozenColumnCount,
|
|
106
|
-
groups,
|
|
107
|
-
groupHeadingHeight,
|
|
108
|
-
groupSummaryHeight,
|
|
109
|
-
leafRowHeight,
|
|
110
|
-
spacerHeight,
|
|
111
|
-
collapsedRows,
|
|
112
|
-
maxFrozenColumnsWidth,
|
|
113
|
-
} = params;
|
|
114
|
-
|
|
115
|
-
const rawStickyColumnWidths = useMemo(
|
|
116
|
-
() => columns.slice(0, frozenColumnCount),
|
|
117
|
-
[columns, frozenColumnCount],
|
|
118
|
-
);
|
|
119
|
-
|
|
120
|
-
// Scale down frozen column widths if they exceed maxFrozenColumnsWidth
|
|
121
|
-
// The first column (select checkbox) is preserved at its original width;
|
|
122
|
-
// only data columns are scaled.
|
|
123
|
-
const stickyColumnWidths = useMemo(() => {
|
|
124
|
-
if (maxFrozenColumnsWidth === undefined) return rawStickyColumnWidths;
|
|
125
|
-
if (rawStickyColumnWidths.length <= 1) return rawStickyColumnWidths;
|
|
126
|
-
|
|
127
|
-
// Preserve the first column (select checkbox) width
|
|
128
|
-
const firstColumnWidth = rawStickyColumnWidths[0];
|
|
129
|
-
const dataColumnWidths = rawStickyColumnWidths.slice(1);
|
|
130
|
-
const dataColumnsTotal = sum(dataColumnWidths);
|
|
131
|
-
|
|
132
|
-
// Calculate remaining space for data columns after reserving first column
|
|
133
|
-
const remainingWidth = maxFrozenColumnsWidth - firstColumnWidth;
|
|
134
|
-
if (remainingWidth <= 0 || dataColumnsTotal <= remainingWidth) {
|
|
135
|
-
return rawStickyColumnWidths;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// Scale only the data columns
|
|
139
|
-
const scale = remainingWidth / dataColumnsTotal;
|
|
140
|
-
const scaledDataColumns = dataColumnWidths.map((w) => Math.floor(w * scale));
|
|
141
|
-
|
|
142
|
-
return [firstColumnWidth, ...scaledDataColumns];
|
|
143
|
-
}, [rawStickyColumnWidths, maxFrozenColumnsWidth]);
|
|
144
|
-
|
|
145
|
-
const scrollableColumnWidths = useMemo(
|
|
146
|
-
() => columns.slice(frozenColumnCount),
|
|
147
|
-
[columns, frozenColumnCount],
|
|
148
|
-
);
|
|
149
|
-
const frozenColumnsWidth = useMemo(() => sum(stickyColumnWidths), [stickyColumnWidths]);
|
|
150
|
-
const scrollableColumnsWidth = useMemo(
|
|
151
|
-
() => sum(scrollableColumnWidths),
|
|
152
|
-
[scrollableColumnWidths],
|
|
153
|
-
);
|
|
154
|
-
const frozenColumns = useMemo(() => getColumns(stickyColumnWidths), [stickyColumnWidths]);
|
|
155
|
-
const scrollableColumns = useMemo(
|
|
156
|
-
() => getColumns(scrollableColumnWidths, frozenColumnCount),
|
|
157
|
-
[scrollableColumnWidths, frozenColumnCount],
|
|
158
|
-
);
|
|
159
|
-
const rows = useMemo(
|
|
160
|
-
() =>
|
|
161
|
-
getRows(
|
|
162
|
-
groups,
|
|
163
|
-
groupHeadingHeight,
|
|
164
|
-
groupSummaryHeight,
|
|
165
|
-
leafRowHeight,
|
|
166
|
-
spacerHeight,
|
|
167
|
-
collapsedRows,
|
|
168
|
-
),
|
|
169
|
-
[groups, groupHeadingHeight, groupSummaryHeight, leafRowHeight, spacerHeight, collapsedRows],
|
|
170
|
-
);
|
|
171
|
-
const contentHeight = useMemo(() => sumBy(rows, (row) => row.height), [rows]);
|
|
172
|
-
const contentWidth = frozenColumnsWidth + scrollableColumnsWidth;
|
|
173
|
-
|
|
174
|
-
return {
|
|
175
|
-
contentHeight,
|
|
176
|
-
contentWidth,
|
|
177
|
-
frozenColumns,
|
|
178
|
-
frozenColumnsWidth,
|
|
179
|
-
scrollableColumns,
|
|
180
|
-
scrollableColumnsWidth,
|
|
181
|
-
rows,
|
|
182
|
-
};
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
export interface RowGridGroup {
|
|
186
|
-
type: "row_group";
|
|
187
|
-
rowCount: number;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
export interface NestedGridGroup {
|
|
191
|
-
type: "group";
|
|
192
|
-
children: GridGroup[];
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
export type GridGroup = RowGridGroup | NestedGridGroup;
|
|
196
|
-
|
|
197
|
-
/**
|
|
198
|
-
* Converts a GroupPath to a GroupPathKey string for efficient lookups.
|
|
199
|
-
*/
|
|
200
|
-
export function groupPathToKey(path: GroupPath): GroupPathKey {
|
|
201
|
-
return path.join(",");
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/**
|
|
205
|
-
* Converts a RowPath to a RowPathKey string for efficient lookups.
|
|
206
|
-
*/
|
|
207
|
-
export function rowPathToKey(path: RowPath): RowPathKey {
|
|
208
|
-
return path.join(",");
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
export function getRows(
|
|
212
|
-
groups: GridGroup[],
|
|
213
|
-
groupHeadingHeight: number,
|
|
214
|
-
groupSummaryHeight: number,
|
|
215
|
-
leafRowHeight: number,
|
|
216
|
-
spacerHeight: number,
|
|
217
|
-
collapsedGroupKeys?: GroupPathKey[] | null,
|
|
218
|
-
): GridRow[] {
|
|
219
|
-
const collapsedSet = new Set<GroupPathKey>();
|
|
220
|
-
if (collapsedGroupKeys) {
|
|
221
|
-
for (const groupPathKey of collapsedGroupKeys) {
|
|
222
|
-
collapsedSet.add(groupPathKey);
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// Check if this is a flat list (single row_group at root level)
|
|
227
|
-
const isFlatList = groups.length === 1 && groups[0].type === "row_group";
|
|
228
|
-
|
|
229
|
-
const descriptors = flattenGroupHierarchy({
|
|
230
|
-
groups,
|
|
231
|
-
parentGroupPath: [],
|
|
232
|
-
collapsedSet,
|
|
233
|
-
isFlatList,
|
|
234
|
-
spacerHeight,
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
return applyLayoutToDescriptors({
|
|
238
|
-
descriptors,
|
|
239
|
-
groupHeadingHeight,
|
|
240
|
-
groupSummaryHeight,
|
|
241
|
-
leafRowHeight,
|
|
242
|
-
spacerHeight,
|
|
243
|
-
});
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
type RowDescriptor =
|
|
247
|
-
| { kind: "group_heading"; groupPath: GroupPath }
|
|
248
|
-
| { kind: "group_summary"; groupPath: GroupPath }
|
|
249
|
-
| { kind: "row"; rowPath: RowPath }
|
|
250
|
-
| { kind: "spacer" };
|
|
251
|
-
|
|
252
|
-
interface FlattenGroupHierarchyParams {
|
|
253
|
-
groups: GridGroup[];
|
|
254
|
-
parentGroupPath: GroupPath;
|
|
255
|
-
collapsedSet: Set<GroupPathKey>;
|
|
256
|
-
spacerHeight: number;
|
|
257
|
-
isFlatList?: boolean;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
function flattenGroupHierarchy(params: FlattenGroupHierarchyParams): RowDescriptor[] {
|
|
261
|
-
const { groups, parentGroupPath, collapsedSet, spacerHeight, isFlatList = false } = params;
|
|
262
|
-
|
|
263
|
-
if (isEmpty(groups)) {
|
|
264
|
-
return [];
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
// For flat lists, emit only row descriptors without group structure
|
|
268
|
-
if (isFlatList && groups.length === 1 && groups[0].type === "row_group") {
|
|
269
|
-
return createRowDescriptors([0], groups[0].rowCount);
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
const descriptors: RowDescriptor[] = [];
|
|
273
|
-
|
|
274
|
-
for (let index = 0; index < groups.length; index++) {
|
|
275
|
-
const group = groups[index];
|
|
276
|
-
const groupPath: GroupPath = [...parentGroupPath, index];
|
|
277
|
-
const collapsed = collapsedSet.has(groupPathToKey(groupPath));
|
|
278
|
-
|
|
279
|
-
descriptors.push({ kind: "group_heading", groupPath });
|
|
280
|
-
// Always show group summary row (even when collapsed)
|
|
281
|
-
descriptors.push({ kind: "group_summary", groupPath });
|
|
282
|
-
|
|
283
|
-
// Only show data rows when not collapsed
|
|
284
|
-
if (collapsed === false) {
|
|
285
|
-
if (group.type === "row_group") {
|
|
286
|
-
descriptors.push(...createRowDescriptors(groupPath, group.rowCount));
|
|
287
|
-
} else {
|
|
288
|
-
descriptors.push(
|
|
289
|
-
...flattenGroupHierarchy({
|
|
290
|
-
groups: group.children,
|
|
291
|
-
parentGroupPath: groupPath,
|
|
292
|
-
collapsedSet,
|
|
293
|
-
spacerHeight,
|
|
294
|
-
}),
|
|
295
|
-
);
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
if (groupPath.length === 1 && spacerHeight > 0) {
|
|
300
|
-
descriptors.push({ kind: "spacer" });
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
return descriptors;
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
function createRowDescriptors(groupPath: GroupPath, rowCount: number): RowDescriptor[] {
|
|
308
|
-
if (rowCount === 0) {
|
|
309
|
-
return [];
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
const descriptors: RowDescriptor[] = [];
|
|
313
|
-
|
|
314
|
-
for (let rowIndex = 0; rowIndex < rowCount; rowIndex++) {
|
|
315
|
-
const rowPath: RowPath = [...groupPath, rowIndex];
|
|
316
|
-
descriptors.push({
|
|
317
|
-
kind: "row",
|
|
318
|
-
rowPath,
|
|
319
|
-
});
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
return descriptors;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
interface ApplyLayoutToDescriptorsParams {
|
|
326
|
-
descriptors: RowDescriptor[];
|
|
327
|
-
groupHeadingHeight: number;
|
|
328
|
-
groupSummaryHeight: number;
|
|
329
|
-
leafRowHeight: number;
|
|
330
|
-
spacerHeight: number;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
function applyLayoutToDescriptors(params: ApplyLayoutToDescriptorsParams): GridRow[] {
|
|
334
|
-
const { descriptors, groupHeadingHeight, groupSummaryHeight, leafRowHeight, spacerHeight } =
|
|
335
|
-
params;
|
|
336
|
-
|
|
337
|
-
let offset = 0;
|
|
338
|
-
|
|
339
|
-
return descriptors.map((descriptor) => {
|
|
340
|
-
const height = getDescriptorHeight(descriptor, {
|
|
341
|
-
groupHeadingHeight,
|
|
342
|
-
groupSummaryHeight,
|
|
343
|
-
leafRowHeight,
|
|
344
|
-
spacerHeight,
|
|
345
|
-
});
|
|
346
|
-
|
|
347
|
-
const row =
|
|
348
|
-
descriptor.kind === "group_heading"
|
|
349
|
-
? {
|
|
350
|
-
type: "group_heading" as const,
|
|
351
|
-
height,
|
|
352
|
-
y: offset,
|
|
353
|
-
groupPath: descriptor.groupPath,
|
|
354
|
-
}
|
|
355
|
-
: descriptor.kind === "group_summary"
|
|
356
|
-
? {
|
|
357
|
-
type: "group_summary" as const,
|
|
358
|
-
height,
|
|
359
|
-
y: offset,
|
|
360
|
-
groupPath: descriptor.groupPath,
|
|
361
|
-
}
|
|
362
|
-
: descriptor.kind === "row"
|
|
363
|
-
? {
|
|
364
|
-
type: "row" as const,
|
|
365
|
-
height,
|
|
366
|
-
y: offset,
|
|
367
|
-
rowPath: descriptor.rowPath,
|
|
368
|
-
}
|
|
369
|
-
: {
|
|
370
|
-
type: "spacer" as const,
|
|
371
|
-
height,
|
|
372
|
-
y: offset,
|
|
373
|
-
};
|
|
374
|
-
|
|
375
|
-
offset += height;
|
|
376
|
-
return row;
|
|
377
|
-
});
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
interface GetDescriptorHeightParams {
|
|
381
|
-
groupHeadingHeight: number;
|
|
382
|
-
groupSummaryHeight: number;
|
|
383
|
-
leafRowHeight: number;
|
|
384
|
-
spacerHeight: number;
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
function getDescriptorHeight(
|
|
388
|
-
descriptor: RowDescriptor,
|
|
389
|
-
heights: GetDescriptorHeightParams,
|
|
390
|
-
): number {
|
|
391
|
-
const { groupHeadingHeight, groupSummaryHeight, leafRowHeight, spacerHeight } = heights;
|
|
392
|
-
|
|
393
|
-
switch (descriptor.kind) {
|
|
394
|
-
case "group_heading":
|
|
395
|
-
return groupHeadingHeight;
|
|
396
|
-
case "group_summary":
|
|
397
|
-
return groupSummaryHeight;
|
|
398
|
-
case "row":
|
|
399
|
-
return leafRowHeight;
|
|
400
|
-
case "spacer":
|
|
401
|
-
return spacerHeight;
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
export function getColumns(columnWidths: number[], offset = 0): GridColumn[] {
|
|
406
|
-
const result: GridColumn[] = [];
|
|
407
|
-
|
|
408
|
-
if (columnWidths.length === 0) {
|
|
409
|
-
return result;
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
let cumulativeX = 0;
|
|
413
|
-
|
|
414
|
-
for (let i = 0; i < columnWidths.length; i++) {
|
|
415
|
-
const width = columnWidths[i];
|
|
416
|
-
result.push({
|
|
417
|
-
width,
|
|
418
|
-
x: cumulativeX,
|
|
419
|
-
column: i + offset,
|
|
420
|
-
});
|
|
421
|
-
cumulativeX += width;
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
return result;
|
|
425
|
-
}
|
|
@@ -1,236 +0,0 @@
|
|
|
1
|
-
import { recycleItems } from "./recycling";
|
|
2
|
-
|
|
3
|
-
interface Item {
|
|
4
|
-
value: number;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
interface RecycledItem {
|
|
8
|
-
key: number;
|
|
9
|
-
value: number;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function recycle(items: Item[], prevItems: RecycledItem[]) {
|
|
13
|
-
return recycleItems({
|
|
14
|
-
items,
|
|
15
|
-
prevItems,
|
|
16
|
-
toRecycledItem: (item, key) => ({ ...item, key }),
|
|
17
|
-
getValue: (item) => item.value,
|
|
18
|
-
getKey: (item) => item.key,
|
|
19
|
-
});
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
test("recycleItems - with empty previous items", () => {
|
|
23
|
-
const items: Item[] = [{ value: 1 }, { value: 2 }, { value: 3 }];
|
|
24
|
-
|
|
25
|
-
const expected: RecycledItem[] = [
|
|
26
|
-
{ value: 1, key: 0 },
|
|
27
|
-
{ value: 2, key: 1 },
|
|
28
|
-
{ value: 3, key: 2 },
|
|
29
|
-
];
|
|
30
|
-
|
|
31
|
-
expect(recycle(items, [])).toEqual(expected);
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
test("recycleItems - with previous items, in middle", () => {
|
|
35
|
-
const items: Item[] = [{ value: 2 }, { value: 3 }, { value: 4 }];
|
|
36
|
-
|
|
37
|
-
const prevItems: RecycledItem[] = [
|
|
38
|
-
{ value: 1, key: 1 },
|
|
39
|
-
{ value: 2, key: 2 },
|
|
40
|
-
{ value: 3, key: 3 },
|
|
41
|
-
];
|
|
42
|
-
|
|
43
|
-
const expected: RecycledItem[] = [
|
|
44
|
-
{ value: 2, key: 2 },
|
|
45
|
-
{ value: 3, key: 3 },
|
|
46
|
-
{ value: 4, key: 1 },
|
|
47
|
-
];
|
|
48
|
-
|
|
49
|
-
expect(recycle(items, prevItems)).toEqual(expected);
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
test("recycleItems - with previous items, at the end", () => {
|
|
53
|
-
const items: Item[] = [{ value: 4 }, { value: 5 }, { value: 6 }];
|
|
54
|
-
|
|
55
|
-
const prevItems: RecycledItem[] = [
|
|
56
|
-
{ value: 1, key: 1 },
|
|
57
|
-
{ value: 2, key: 2 },
|
|
58
|
-
{ value: 3, key: 3 },
|
|
59
|
-
];
|
|
60
|
-
|
|
61
|
-
const expected: RecycledItem[] = [
|
|
62
|
-
{ value: 4, key: 1 },
|
|
63
|
-
{ value: 5, key: 2 },
|
|
64
|
-
{ value: 6, key: 3 },
|
|
65
|
-
];
|
|
66
|
-
|
|
67
|
-
expect(recycle(items, prevItems)).toEqual(expected);
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
test("recycleItems - with no matching prev items", () => {
|
|
71
|
-
const items: Item[] = [{ value: 1 }, { value: 2 }, { value: 3 }];
|
|
72
|
-
|
|
73
|
-
const prevItems: RecycledItem[] = [
|
|
74
|
-
{ value: 10, key: 1 },
|
|
75
|
-
{ value: 20, key: 2 },
|
|
76
|
-
{ value: 30, key: 3 },
|
|
77
|
-
];
|
|
78
|
-
|
|
79
|
-
const expected: RecycledItem[] = [
|
|
80
|
-
{ value: 1, key: 1 },
|
|
81
|
-
{ value: 2, key: 2 },
|
|
82
|
-
{ value: 3, key: 3 },
|
|
83
|
-
];
|
|
84
|
-
|
|
85
|
-
expect(recycle(items, prevItems)).toEqual(expected);
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
test("recycleItems - more items than previous", () => {
|
|
89
|
-
const items: Item[] = [{ value: 1 }, { value: 2 }, { value: 3 }, { value: 4 }, { value: 5 }];
|
|
90
|
-
|
|
91
|
-
const prevItems: RecycledItem[] = [
|
|
92
|
-
{ value: 1, key: 1 },
|
|
93
|
-
{ value: 2, key: 2 },
|
|
94
|
-
{ value: 3, key: 3 },
|
|
95
|
-
];
|
|
96
|
-
|
|
97
|
-
const expected: RecycledItem[] = [
|
|
98
|
-
{ value: 1, key: 1 },
|
|
99
|
-
{ value: 2, key: 2 },
|
|
100
|
-
{ value: 3, key: 3 },
|
|
101
|
-
{ value: 4, key: 4 },
|
|
102
|
-
{ value: 5, key: 5 },
|
|
103
|
-
];
|
|
104
|
-
|
|
105
|
-
expect(recycle(items, prevItems)).toEqual(expected);
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
test("recycleItems - with changed other value", () => {
|
|
109
|
-
interface ComplexItem {
|
|
110
|
-
value: number;
|
|
111
|
-
other: string;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
interface RecycledComplexItem {
|
|
115
|
-
key: number;
|
|
116
|
-
value: number;
|
|
117
|
-
other: string;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const items: ComplexItem[] = [
|
|
121
|
-
{ value: 1, other: "a" },
|
|
122
|
-
{ value: 2, other: "b-changed" },
|
|
123
|
-
{ value: 3, other: "c" },
|
|
124
|
-
];
|
|
125
|
-
|
|
126
|
-
const prevItems: RecycledComplexItem[] = [
|
|
127
|
-
{ value: 1, key: 1, other: "a" },
|
|
128
|
-
{ value: 2, key: 2, other: "b" },
|
|
129
|
-
{ value: 3, key: 3, other: "c" },
|
|
130
|
-
];
|
|
131
|
-
|
|
132
|
-
const expected: RecycledComplexItem[] = [
|
|
133
|
-
{ value: 1, key: 1, other: "a" },
|
|
134
|
-
{ value: 2, key: 2, other: "b-changed" },
|
|
135
|
-
{ value: 3, key: 3, other: "c" },
|
|
136
|
-
];
|
|
137
|
-
|
|
138
|
-
const result = recycleItems({
|
|
139
|
-
items,
|
|
140
|
-
prevItems,
|
|
141
|
-
toRecycledItem: (item, key) => ({ ...item, key }),
|
|
142
|
-
getValue: (item) => item.value,
|
|
143
|
-
getKey: (item) => item.key,
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
expect(result).toEqual(expected);
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
test("recycleItems - fewer items than previous", () => {
|
|
150
|
-
const items: Item[] = [{ value: 2 }];
|
|
151
|
-
|
|
152
|
-
const prevItems: RecycledItem[] = [
|
|
153
|
-
{ value: 1, key: 1 },
|
|
154
|
-
{ value: 2, key: 2 },
|
|
155
|
-
{ value: 3, key: 3 },
|
|
156
|
-
];
|
|
157
|
-
|
|
158
|
-
const expected: RecycledItem[] = [{ value: 2, key: 2 }];
|
|
159
|
-
|
|
160
|
-
expect(recycle(items, prevItems)).toEqual(expected);
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
test("recycleItems - reuses keys from middle items", () => {
|
|
164
|
-
const items: Item[] = [{ value: 1 }, { value: 5 }, { value: 3 }];
|
|
165
|
-
|
|
166
|
-
const prevItems: RecycledItem[] = [
|
|
167
|
-
{ value: 1, key: 1 },
|
|
168
|
-
{ value: 2, key: 2 },
|
|
169
|
-
{ value: 3, key: 3 },
|
|
170
|
-
];
|
|
171
|
-
|
|
172
|
-
const expected: RecycledItem[] = [
|
|
173
|
-
{ value: 1, key: 1 },
|
|
174
|
-
{ value: 3, key: 3 },
|
|
175
|
-
{ value: 5, key: 2 }, // Reuses key from removed item
|
|
176
|
-
];
|
|
177
|
-
|
|
178
|
-
expect(recycle(items, prevItems)).toEqual(expected);
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
test("recycleItems - maintains sort order by value", () => {
|
|
182
|
-
const items: Item[] = [{ value: 3 }, { value: 1 }, { value: 2 }];
|
|
183
|
-
|
|
184
|
-
const prevItems: RecycledItem[] = [
|
|
185
|
-
{ value: 1, key: 10 },
|
|
186
|
-
{ value: 2, key: 20 },
|
|
187
|
-
{ value: 3, key: 30 },
|
|
188
|
-
];
|
|
189
|
-
|
|
190
|
-
const result = recycle(items, prevItems);
|
|
191
|
-
|
|
192
|
-
// Should be sorted by value (1, 2, 3)
|
|
193
|
-
expect(result[0].value).toBe(1);
|
|
194
|
-
expect(result[1].value).toBe(2);
|
|
195
|
-
expect(result[2].value).toBe(3);
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
test("recycleItems - all items replaced", () => {
|
|
199
|
-
const items: Item[] = [{ value: 10 }, { value: 20 }];
|
|
200
|
-
|
|
201
|
-
const prevItems: RecycledItem[] = [
|
|
202
|
-
{ value: 1, key: 1 },
|
|
203
|
-
{ value: 2, key: 2 },
|
|
204
|
-
];
|
|
205
|
-
|
|
206
|
-
const expected: RecycledItem[] = [
|
|
207
|
-
{ value: 10, key: 1 },
|
|
208
|
-
{ value: 20, key: 2 },
|
|
209
|
-
];
|
|
210
|
-
|
|
211
|
-
expect(recycle(items, prevItems)).toEqual(expected);
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
test("recycleItems - partial overlap", () => {
|
|
215
|
-
const items: Item[] = [{ value: 2 }, { value: 3 }, { value: 5 }];
|
|
216
|
-
|
|
217
|
-
const prevItems: RecycledItem[] = [
|
|
218
|
-
{ value: 1, key: 10 },
|
|
219
|
-
{ value: 2, key: 20 },
|
|
220
|
-
{ value: 3, key: 30 },
|
|
221
|
-
{ value: 4, key: 40 },
|
|
222
|
-
];
|
|
223
|
-
|
|
224
|
-
const result = recycle(items, prevItems);
|
|
225
|
-
|
|
226
|
-
// Should reuse keys 20 and 30 for values 2 and 3
|
|
227
|
-
const item2 = result.find((item) => item.value === 2);
|
|
228
|
-
const item3 = result.find((item) => item.value === 3);
|
|
229
|
-
const item5 = result.find((item) => item.value === 5);
|
|
230
|
-
|
|
231
|
-
expect(item2?.key).toBe(20);
|
|
232
|
-
expect(item3?.key).toBe(30);
|
|
233
|
-
expect(item5?.key).toBeDefined();
|
|
234
|
-
// item5 should recycle from removed items (10 or 40)
|
|
235
|
-
expect([10, 40]).toContain(item5?.key);
|
|
236
|
-
});
|