Skip to content

Commit 020c44d

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

14 files changed

+832
-127
lines changed

package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@
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": [
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
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 = (allItems: Item[], state: GroupSelectionState<Item>, trackBy?: TrackBy<Item>) => {
18+
const { items, getChildren } = computeTreeItems(allItems, { getId, getParentId, dataGrouping: true }, null, null);
19+
return new SelectionTree(items, { getChildren, trackBy }, state);
20+
};
21+
22+
test.each<GroupSelectionState<Item>>([
23+
{ baseline: 'all', toggledItems: [] },
24+
{ baseline: 'none', toggledItems: [] },
25+
{ baseline: 'all', toggledItems: ['x'] },
26+
{ baseline: 'none', toggledItems: ['x'] },
27+
])('creates empty selection when items are empty, state=%s', state => {
28+
const tree = createSelectionTree([], state);
29+
expect(tree.getState()).toEqual({ baseline: state.baseline, toggledItems: [] });
30+
});
31+
32+
test.each<GroupSelectionState<Item>>([
33+
{ baseline: 'none', toggledItems: ['a', 'a.1'] },
34+
{ baseline: 'none', toggledItems: ['b', 'b.1.1'] },
35+
{ baseline: 'none', toggledItems: ['c', 'c.1', 'c.2'] },
36+
{ baseline: 'all', toggledItems: ['a', 'b', 'c'] },
37+
{ baseline: 'all', toggledItems: ['a', 'b', 'b.1', 'b.1.1', 'c'] },
38+
])('creates empty selection when given selection is exclusive, state=%s', state => {
39+
const tree = createSelectionTree(['a', 'a.1', 'b', 'b.1', 'b.1.1', 'c', 'c.1', 'c.2'], state);
40+
expect(tree.getState()).toEqual({ baseline: 'none', toggledItems: [] });
41+
});
42+
43+
test.each<GroupSelectionState<Item>>([
44+
{ baseline: 'none', toggledItems: ['a', 'b.1.1', 'c', 'c.2'] },
45+
{ baseline: 'all', toggledItems: ['b', 'b.1.1', 'c.2'] },
46+
])('item selection getters produce expected result, state=%s', state => {
47+
const tree = createSelectionTree(['a', 'a.1', 'b', 'b.1', 'b.1.1', 'b.1.2', 'c', 'c.1', 'c.2'], state);
48+
const getItemState = (item: Item) => ({
49+
s: tree.isItemSelected(item),
50+
i: tree.isItemIndeterminate(item),
51+
c: tree.getSelectedItemsCount(item),
52+
});
53+
expect(getItemState('a')).toEqual({ s: true, i: false, c: 1 });
54+
expect(getItemState('a.1')).toEqual({ s: true, i: false, c: 1 });
55+
expect(getItemState('b')).toEqual({ s: false, i: true, c: 1 });
56+
expect(getItemState('b.1')).toEqual({ s: false, i: true, c: 1 });
57+
expect(getItemState('b.1.1')).toEqual({ s: true, i: false, c: 1 });
58+
expect(getItemState('b.1.2')).toEqual({ s: false, i: false, c: 0 });
59+
expect(getItemState('c')).toEqual({ s: true, i: true, c: 1 });
60+
expect(getItemState('c.1')).toEqual({ s: true, i: false, c: 1 });
61+
expect(getItemState('c.2')).toEqual({ s: false, i: false, c: 0 });
62+
});
63+
64+
test('can call item selection getters on missing items', () => {
65+
const tree = createSelectionTree(['a', 'a.1', 'b'], { baseline: 'none', toggledItems: ['a.1'] });
66+
expect(tree.isItemSelected('x')).toBe(false);
67+
expect(tree.isItemIndeterminate('x')).toBe(false);
68+
expect(tree.getSelectedItemsCount('x')).toBe(0);
69+
});
70+
71+
test.each<[Item[], GroupSelectionState<Item>, [boolean, boolean]]>([
72+
[[], { baseline: 'none', toggledItems: [] }, [false, false]],
73+
[[], { baseline: 'all', toggledItems: [] }, [false, false]],
74+
[['a'], { baseline: 'none', toggledItems: [] }, [false, false]],
75+
[['a'], { baseline: 'all', toggledItems: [] }, [true, false]],
76+
[['a'], { baseline: 'none', toggledItems: ['a'] }, [true, false]],
77+
[['a'], { baseline: 'all', toggledItems: ['a'] }, [false, false]],
78+
[['a', 'b'], { baseline: 'none', toggledItems: ['a'] }, [false, true]],
79+
[['a', 'b'], { baseline: 'all', toggledItems: ['a'] }, [false, true]],
80+
])('computes all items selected, params: [%s, %s, %s]', (items, state, [allSelected, allIndeterminate]) => {
81+
const tree = createSelectionTree(items, state);
82+
expect(tree.isAllItemsSelected()).toBe(allSelected);
83+
expect(tree.isAllItemsIndeterminate()).toBe(allIndeterminate);
84+
});
85+
86+
test.each<GroupSelectionState<Item>>([
87+
{ baseline: 'none', toggledItems: ['a', 'b.1.1', 'c', 'c.2'] },
88+
{ baseline: 'all', toggledItems: ['b', 'b.1.1', 'c.2'] },
89+
])('computes selected leaf items, state=%s', state => {
90+
const tree = createSelectionTree(['a', 'a.1', 'b', 'b.1', 'b.1.1', 'b.1.2', 'c', 'c.1', 'c.2'], state);
91+
expect(tree.getSelectedLeafItems()).toEqual(['a.1', 'b.1.1', 'c.1']);
92+
});
93+
94+
test.each<[GroupSelectionState<Item>, GroupSelectionState<Item>]>([
95+
[
96+
{ baseline: 'none', toggledItems: [] },
97+
{ baseline: 'all', toggledItems: [] },
98+
],
99+
[
100+
{ baseline: 'none', toggledItems: ['b.1.1'] },
101+
{ baseline: 'all', toggledItems: [] },
102+
],
103+
[
104+
{ baseline: 'all', toggledItems: ['b.1.1'] },
105+
{ baseline: 'all', toggledItems: [] },
106+
],
107+
[
108+
{ baseline: 'all', toggledItems: [] },
109+
{ baseline: 'none', toggledItems: [] },
110+
],
111+
])('toggles all, from: %s, to: %s', (from, to) => {
112+
const tree = createSelectionTree(['a', 'a.1', 'b', 'b.1', 'b.1.1', 'b.1.2', 'c', 'c.1', 'c.2'], from);
113+
expect(tree.toggleAll().getState()).toEqual(to);
114+
});
115+
116+
test.each<[GroupSelectionState<Item>, GroupSelectionState<Item>]>([
117+
[
118+
{ baseline: 'none', toggledItems: [] },
119+
{ baseline: 'none', toggledItems: [] },
120+
],
121+
[
122+
{ baseline: 'all', toggledItems: [] },
123+
{ baseline: 'all', toggledItems: [] },
124+
],
125+
[
126+
{ baseline: 'none', toggledItems: ['a.1'] },
127+
{ baseline: 'all', toggledItems: ['b', 'c'] },
128+
],
129+
[
130+
{ baseline: 'all', toggledItems: ['b', 'c'] },
131+
{ baseline: 'none', toggledItems: ['a'] },
132+
],
133+
])('inverts all, from: %s, to: %s', (from, to) => {
134+
const tree = createSelectionTree(['a', 'a.1', 'b', 'b.1', 'b.1.1', 'b.1.2', 'c', 'c.1', 'c.2'], from);
135+
expect(tree.invertAll().getState()).toEqual(to);
136+
});
137+
138+
test.each<[GroupSelectionState<Item>, Item, GroupSelectionState<Item>]>([
139+
[{ baseline: 'none', toggledItems: [] }, 'a.1', { baseline: 'none', toggledItems: ['a'] }],
140+
[{ baseline: 'all', toggledItems: [] }, 'a.1', { baseline: 'all', toggledItems: ['a'] }],
141+
[{ baseline: 'none', toggledItems: ['b', 'c'] }, 'a.1', { baseline: 'all', toggledItems: [] }],
142+
[{ baseline: 'all', toggledItems: ['b', 'c'] }, 'a.1', { baseline: 'none', toggledItems: [] }],
143+
])('toggles item, from: %s, item: %s, to: %s', (from, item, to) => {
144+
const tree = createSelectionTree(['a', 'a.1', 'b', 'b.1', 'b.1.1', 'b.1.2', 'c', 'c.1', 'c.2'], from);
145+
expect(tree.toggleSome([item]).getState()).toEqual(to);
146+
});
147+
148+
test.each<[GroupSelectionState<Item>, Item, GroupSelectionState<Item>]>([
149+
[{ baseline: 'none', toggledItems: [] }, 'a.1', { baseline: 'none', toggledItems: [] }],
150+
[{ baseline: 'all', toggledItems: [] }, 'a.1', { baseline: 'all', toggledItems: [] }],
151+
[{ baseline: 'none', toggledItems: [] }, 'a.1.1', { baseline: 'none', toggledItems: ['a.1.1'] }],
152+
[{ baseline: 'all', toggledItems: [] }, 'a.1.1', { baseline: 'all', toggledItems: ['a.1.1'] }],
153+
[{ baseline: 'none', toggledItems: ['a.1.1'] }, 'a.1', { baseline: 'none', toggledItems: ['a.1', 'a.1.2'] }],
154+
[{ baseline: 'all', toggledItems: ['a.1.1'] }, 'a.1', { baseline: 'all', toggledItems: ['a.1', 'a.1.2'] }],
155+
])('inverts item, from: %s, item: %s, to: %s', (from, item, to) => {
156+
const tree = createSelectionTree(['a', 'a.1', 'a.1.1', 'a.1.2', 'a.2'], from);
157+
expect(tree.invertOne(item).getState()).toEqual(to);
158+
});
159+
160+
test('can trigger selection actions on missing items', () => {
161+
const tree = createSelectionTree(['a', 'a.1', 'b'], { baseline: 'none', toggledItems: ['a.1'] });
162+
expect(tree.toggleSome(['x']).invertOne('x').getState()).toEqual({ baseline: 'none', toggledItems: ['a'] });
163+
});
164+
165+
test('tracks items by reference', () => {
166+
const allItems = [{ id: 'a' }, { id: 'b' }, { id: 'c' }];
167+
const getId = (item: { id: string }) => item.id;
168+
const getParentId = () => null;
169+
const { items, getChildren } = computeTreeItems(allItems, { getId, getParentId, dataGrouping: true }, null, null);
170+
const tree = new SelectionTree(items, { getChildren }, { baseline: 'all', toggledItems: [items[0], { id: 'b' }] });
171+
expect(tree.getState()).toEqual({ baseline: 'all', toggledItems: [{ id: 'a' }] });
172+
});
173+
174+
test('tracks items with trackBy', () => {
175+
const allItems = [{ id: 'a' }, { id: 'b' }, { id: 'c' }];
176+
const getId = (item: { id: string }) => item.id;
177+
const getParentId = () => null;
178+
const { items, getChildren } = computeTreeItems(allItems, { getId, getParentId, dataGrouping: true }, null, null);
179+
const tree = new SelectionTree(
180+
items,
181+
{ getChildren, trackBy: getId },
182+
{ baseline: 'all', toggledItems: [items[0], { id: 'b' }] }
183+
);
184+
expect(tree.getState()).toEqual({ baseline: 'all', toggledItems: [{ id: 'a' }, { id: 'b' }] });
185+
});

0 commit comments

Comments
 (0)