Skip to content

Commit 1b18047

Browse files
committed
feat: Adds support for table data grouping
1 parent 3ca9920 commit 1b18047

15 files changed

+894
-132
lines changed

package.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
"test": "npm run test-08:00 && npm run test+08:00 && npm run test-local",
1717
"clean": "rimraf ./lib",
1818
"prebuild": "npm run clean",
19-
"build": "tsc -p ./tsconfig.json && tsc -p ./tsconfig.cjs.json && node ./scripts/generate-deep-package.js",
19+
"build": "tsc -p ./tsconfig.json && tsc -p ./tsconfig.cjs.json && node ./scripts/generate-deep-package.js && node ./scripts/generate-root-dts-shims.js",
2020
"postbuild": "cp package.json README.md LICENSE NOTICE lib",
2121
"prepare": "husky"
2222
},
@@ -32,11 +32,18 @@
3232
"./operations": {
3333
"require": "./cjs/operations.js",
3434
"default": "./mjs/operations.js"
35+
},
36+
"./internal-do-not-use": {
37+
"require": "./cjs/internal-do-not-use.js",
38+
"default": "./mjs/internal-do-not-use.js"
3539
}
3640
},
3741
"files": [
3842
"cjs",
3943
"mjs",
44+
"index.d.ts",
45+
"operations.d.ts",
46+
"internal-do-not-use.d.ts",
4047
"package.json",
4148
"README.md",
4249
"LICENSE",

scripts/generate-root-dts-shims.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
#!/usr/bin/env node
2+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
// SPDX-License-Identifier: Apache-2.0
4+
5+
import fs from 'node:fs';
6+
import { fileURLToPath } from 'url';
7+
import { join, dirname } from 'path';
8+
9+
const root = join(dirname(fileURLToPath(import.meta.url)), '..');
10+
11+
const shims = [
12+
{ target: './lib/mjs/index.d.ts', out: './lib/index.d.ts' },
13+
{ target: './lib/mjs/operations.d.ts', out: './lib/operations.d.ts' },
14+
{ target: './lib/mjs/internal-do-not-use.d.ts', out: './lib/internal-do-not-use.d.ts' },
15+
];
16+
17+
for (const { out, target } of shims) {
18+
if (!fs.existsSync(join(root, target))) {
19+
throw new Error(`Missing target declaration file: ${target}`);
20+
}
21+
fs.writeFileSync(join(root, out), `export * from "${target.replace(/\.d\.ts$/, '')}";\n`, 'utf8');
22+
}
Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import { test, expect } from 'vitest';
5+
import { SelectionTree } from '../operations/selection-tree';
6+
import { computeTreeItems } from '../operations/items-tree';
7+
import { GroupSelectionState, TrackBy } from '../interfaces';
8+
9+
type Item = string;
10+
11+
const getId = (item: Item) => item;
12+
const getParentId = (item: Item): null | string => {
13+
const parts = item.split('.');
14+
return parts.length === 1 ? null : parts.slice(0, -1).join('.');
15+
};
16+
17+
const createSelectionTree = (
18+
allItems: Item[],
19+
state: GroupSelectionState<Item>,
20+
{ trackBy, isComplete }: { trackBy?: TrackBy<Item>; isComplete?: (item: null | Item) => boolean } = {}
21+
) => {
22+
const { items, getChildren } = computeTreeItems(allItems, { getId, getParentId, dataGrouping: true }, null, null);
23+
return new SelectionTree(items, { getChildren, trackBy, isComplete }, state);
24+
};
25+
26+
test.each<GroupSelectionState<Item>>([
27+
{ inverted: true, toggledItems: [] },
28+
{ inverted: false, toggledItems: [] },
29+
{ inverted: true, toggledItems: ['x'] },
30+
{ inverted: false, toggledItems: ['x'] },
31+
])('creates empty selection when items are empty, state=%s', state => {
32+
const tree = createSelectionTree([], state);
33+
expect(tree.getState()).toEqual({ inverted: state.inverted, toggledItems: [] });
34+
});
35+
36+
test.each<GroupSelectionState<Item>>([
37+
{ inverted: false, toggledItems: ['a', 'b.1.1', 'c', 'c.2'] },
38+
{ inverted: true, toggledItems: ['b', 'b.1.1', 'c.2'] },
39+
])('item selection getters produce expected result, state=%s', state => {
40+
const tree = createSelectionTree(['a', 'a.1', 'b', 'b.1', 'b.1.1', 'b.1.2', 'c', 'c.1', 'c.2'], state);
41+
const getItemState = (item: Item) => ({
42+
s: tree.isItemSelected(item),
43+
i: tree.isItemIndeterminate(item),
44+
c: tree.getSelectedItemsCount(item),
45+
});
46+
expect(getItemState('a')).toEqual({ s: true, i: false, c: 1 });
47+
expect(getItemState('a.1')).toEqual({ s: true, i: false, c: 1 });
48+
expect(getItemState('b')).toEqual({ s: false, i: true, c: 1 });
49+
expect(getItemState('b.1')).toEqual({ s: false, i: true, c: 1 });
50+
expect(getItemState('b.1.1')).toEqual({ s: true, i: false, c: 1 });
51+
expect(getItemState('b.1.2')).toEqual({ s: false, i: false, c: 0 });
52+
expect(getItemState('c')).toEqual({ s: true, i: true, c: 1 });
53+
expect(getItemState('c.1')).toEqual({ s: true, i: false, c: 1 });
54+
expect(getItemState('c.2')).toEqual({ s: false, i: false, c: 0 });
55+
});
56+
57+
test('can call item selection getters on missing items', () => {
58+
const tree = createSelectionTree(['a', 'a.1', 'b'], { inverted: false, toggledItems: ['a.1'] });
59+
expect(tree.isItemSelected('x')).toBe(false);
60+
expect(tree.isItemIndeterminate('x')).toBe(false);
61+
expect(tree.getSelectedItemsCount('x')).toBe(0);
62+
});
63+
64+
test.each<[Item[], GroupSelectionState<Item>, [boolean, boolean]]>([
65+
[[], { inverted: false, toggledItems: [] }, [false, false]],
66+
[[], { inverted: true, toggledItems: [] }, [false, false]],
67+
[['a'], { inverted: false, toggledItems: [] }, [false, false]],
68+
[['a'], { inverted: true, toggledItems: [] }, [true, false]],
69+
[['a'], { inverted: false, toggledItems: ['a'] }, [true, false]],
70+
[['a'], { inverted: true, toggledItems: ['a'] }, [false, false]],
71+
[['a', 'b'], { inverted: false, toggledItems: ['a'] }, [false, true]],
72+
[['a', 'b'], { inverted: true, toggledItems: ['a'] }, [false, true]],
73+
])('computes all items selected, params: [%s, %s, %s]', (items, state, [allSelected, allIndeterminate]) => {
74+
const tree = createSelectionTree(items, state);
75+
expect(tree.isAllItemsSelected()).toBe(allSelected);
76+
expect(tree.isAllItemsIndeterminate()).toBe(allIndeterminate);
77+
});
78+
79+
test.each<GroupSelectionState<Item>>([
80+
{ inverted: false, toggledItems: ['a', 'b.1.1', 'c', 'c.2'] },
81+
{ inverted: true, toggledItems: ['b', 'b.1.1', 'c.2'] },
82+
])('computes selected leaf items, state=%s', state => {
83+
const tree = createSelectionTree(['a', 'a.1', 'b', 'b.1', 'b.1.1', 'b.1.2', 'c', 'c.1', 'c.2'], state);
84+
expect(tree.getSelectedItems()).toEqual(['a.1', 'b.1.1', 'c.1']);
85+
});
86+
87+
test.each<[GroupSelectionState<Item>, GroupSelectionState<Item>]>([
88+
[
89+
{ inverted: false, toggledItems: [] },
90+
{ inverted: true, toggledItems: [] },
91+
],
92+
[
93+
{ inverted: false, toggledItems: ['b.1.1'] },
94+
{ inverted: true, toggledItems: [] },
95+
],
96+
[
97+
{ inverted: true, toggledItems: ['b.1.1'] },
98+
{ inverted: true, toggledItems: [] },
99+
],
100+
[
101+
{ inverted: true, toggledItems: [] },
102+
{ inverted: false, toggledItems: [] },
103+
],
104+
])('toggles all, from: %s, to: %s', (from, to) => {
105+
const tree = createSelectionTree(['a', 'a.1', 'b', 'b.1', 'b.1.1', 'b.1.2', 'c', 'c.1', 'c.2'], from);
106+
expect(tree.toggleAll().getState()).toEqual(to);
107+
});
108+
109+
test.each<[GroupSelectionState<Item>, GroupSelectionState<Item>]>([
110+
[
111+
{ inverted: false, toggledItems: [] },
112+
{ inverted: false, toggledItems: [] },
113+
],
114+
[
115+
{ inverted: true, toggledItems: [] },
116+
{ inverted: true, toggledItems: [] },
117+
],
118+
[
119+
{ inverted: false, toggledItems: ['a.1'] },
120+
{ inverted: true, toggledItems: ['b', 'c'] },
121+
],
122+
[
123+
{ inverted: true, toggledItems: ['b', 'c'] },
124+
{ inverted: false, toggledItems: ['a'] },
125+
],
126+
])('inverts all, from: %s, to: %s', (from, to) => {
127+
const tree = createSelectionTree(['a', 'a.1', 'b', 'b.1', 'b.1.1', 'b.1.2', 'c', 'c.1', 'c.2'], from);
128+
expect(tree.invertAll().getState()).toEqual(to);
129+
});
130+
131+
test.each<[Item, GroupSelectionState<Item>, GroupSelectionState<Item>]>([
132+
['a', { inverted: false, toggledItems: [] }, { inverted: false, toggledItems: ['a'] }],
133+
['a', { inverted: true, toggledItems: [] }, { inverted: true, toggledItems: ['a'] }],
134+
['a', { inverted: false, toggledItems: ['a'] }, { inverted: false, toggledItems: [] }],
135+
['a', { inverted: true, toggledItems: ['a'] }, { inverted: true, toggledItems: [] }],
136+
['b.1.1', { inverted: false, toggledItems: [] }, { inverted: false, toggledItems: ['b.1.1'] }],
137+
['b.1.1', { inverted: true, toggledItems: [] }, { inverted: true, toggledItems: ['b.1.1'] }],
138+
['b.1.1', { inverted: false, toggledItems: ['b.1.1'] }, { inverted: false, toggledItems: [] }],
139+
['b.1.1', { inverted: true, toggledItems: ['b.1.1'] }, { inverted: true, toggledItems: [] }],
140+
])('toggles item %s, %s -> %s', (item, from, to) => {
141+
const tree = createSelectionTree(['a', 'a.1', 'b', 'b.1', 'b.1.1', 'b.1.2', 'c', 'c.1', 'c.2'], from);
142+
expect(tree.toggleSome([item]).getState()).toEqual(to);
143+
});
144+
145+
test.each<[Item, GroupSelectionState<Item>, GroupSelectionState<Item>]>([
146+
['a.1', { inverted: false, toggledItems: [] }, { inverted: false, toggledItems: [] }],
147+
['a.1', { inverted: true, toggledItems: [] }, { inverted: true, toggledItems: [] }],
148+
['a.1.1', { inverted: false, toggledItems: [] }, { inverted: false, toggledItems: ['a.1.1'] }],
149+
['a.1.1', { inverted: true, toggledItems: [] }, { inverted: true, toggledItems: ['a.1.1'] }],
150+
['a.1', { inverted: false, toggledItems: ['a.1.1'] }, { inverted: false, toggledItems: ['a.1', 'a.1.2'] }],
151+
['a.1', { inverted: true, toggledItems: ['a.1.1'] }, { inverted: true, toggledItems: ['a.1', 'a.1.2'] }],
152+
])('inverts item %s, %s -> %s', (item, from, to) => {
153+
const tree = createSelectionTree(['a', 'a.1', 'a.1.1', 'a.1.2', 'a.2'], from);
154+
expect(tree.invertOne(item).getState()).toEqual(to);
155+
});
156+
157+
test('computes indeterminate state deeply', () => {
158+
const tree = createSelectionTree(['a', 'a.1', 'a.1.1', 'a.1.2'], { inverted: true, toggledItems: ['a.1.1'] });
159+
expect(tree.isItemSelected('a.1.1')).toBe(false);
160+
expect(tree.isItemSelected('a.1.2')).toBe(true);
161+
expect(tree.isItemIndeterminate('a.1')).toBe(true);
162+
expect(tree.isItemIndeterminate('a')).toBe(true);
163+
expect(tree.isAllItemsIndeterminate()).toBe(true);
164+
});
165+
166+
test.each<[Item[], GroupSelectionState<Item>, GroupSelectionState<Item>]>([
167+
[['a', 'b', 'c'], { inverted: false, toggledItems: ['a', 'b', 'c'] }, { inverted: true, toggledItems: [] }],
168+
[['a', 'b', 'c'], { inverted: true, toggledItems: ['a', 'b', 'c'] }, { inverted: false, toggledItems: [] }],
169+
[['a', 'a.1', 'b'], { inverted: false, toggledItems: ['a.1'] }, { inverted: false, toggledItems: ['a'] }],
170+
[['a', 'a.1', 'b'], { inverted: true, toggledItems: ['a.1'] }, { inverted: true, toggledItems: ['a'] }],
171+
[['a', 'a.1', 'a.2'], { inverted: false, toggledItems: ['a.1', 'a.2'] }, { inverted: true, toggledItems: [] }],
172+
[['a', 'a.1', 'a.2'], { inverted: true, toggledItems: ['a.1', 'a.2'] }, { inverted: false, toggledItems: [] }],
173+
])('normalizes state [%s] %s -> %s', (items, state, normalizedState) => {
174+
const tree = createSelectionTree(items, state);
175+
expect(tree.getState()).toEqual(normalizedState);
176+
});
177+
178+
test.each<[Item[], GroupSelectionState<Item>]>([
179+
[['a', 'b'], { inverted: false, toggledItems: ['a', 'b'] }],
180+
[['a', 'b'], { inverted: true, toggledItems: ['a', 'b'] }],
181+
])('skips normalization for incomplete root [%s] %s', (items, state) => {
182+
const tree = createSelectionTree(items, state, { isComplete: item => (!item ? false : true) });
183+
expect(tree.getState()).toEqual(state);
184+
});
185+
186+
test.each<[Item[], GroupSelectionState<Item>]>([
187+
[['a', 'a.1', 'b'], { inverted: false, toggledItems: ['a.1'] }],
188+
[['a', 'a.1', 'b'], { inverted: true, toggledItems: ['a.1'] }],
189+
[['a', 'a.1', 'a.2'], { inverted: false, toggledItems: ['a.1', 'a.2'] }],
190+
[['a', 'a.1', 'a.2'], { inverted: true, toggledItems: ['a.1', 'a.2'] }],
191+
])('skips normalization for incomplete group [%s] %s', (items, state) => {
192+
const tree = createSelectionTree(items, state, { isComplete: item => (item === 'a' ? false : true) });
193+
expect(tree.getState()).toEqual(state);
194+
});
195+
196+
test('can trigger selection actions on missing items', () => {
197+
const tree = createSelectionTree(['a', 'a.1', 'b'], { inverted: false, toggledItems: ['a.1'] });
198+
expect(tree.toggleSome(['x']).getState()).toEqual({ inverted: false, toggledItems: ['a'] });
199+
expect(tree.invertOne('x').getState()).toEqual({ inverted: false, toggledItems: ['a'] });
200+
});
201+
202+
test('tracks items by reference', () => {
203+
const allItems = [{ id: 'a' }, { id: 'b' }, { id: 'c' }];
204+
const getId = (item: { id: string }) => item.id;
205+
const getParentId = () => null;
206+
const { items, getChildren } = computeTreeItems(allItems, { getId, getParentId, dataGrouping: true }, null, null);
207+
const tree = new SelectionTree(items, { getChildren }, { inverted: true, toggledItems: [items[0], { id: 'b' }] });
208+
expect(tree.getState()).toEqual({ inverted: true, toggledItems: [{ id: 'a' }] });
209+
});
210+
211+
test('tracks items with trackBy', () => {
212+
const allItems = [{ id: 'a' }, { id: 'b' }, { id: 'c' }];
213+
const getId = (item: { id: string }) => item.id;
214+
const getParentId = () => null;
215+
const { items, getChildren } = computeTreeItems(allItems, { getId, getParentId, dataGrouping: true }, null, null);
216+
const tree = new SelectionTree(
217+
items,
218+
{ getChildren, trackBy: getId },
219+
{ inverted: true, toggledItems: [items[0], { id: 'b' }] }
220+
);
221+
expect(tree.getState()).toEqual({ inverted: true, toggledItems: [{ id: 'a' }, { id: 'b' }] });
222+
});

0 commit comments

Comments
 (0)