Skip to content

Commit 825e839

Browse files
authored
chore: Implements grouped data support in table (WIP) (#3673)
1 parent 62efe83 commit 825e839

35 files changed

+2158
-294
lines changed

pages/table/editable.page.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -307,10 +307,13 @@ const Demo = forwardRef(
307307
expandableRows={
308308
expandableRows
309309
? {
310-
getItemChildren: item => [
311-
{ ...item, Id: item.Id + '-1' },
312-
{ ...item, Id: item.Id + '-2' },
313-
],
310+
getItemChildren: item =>
311+
item.Id.split('-').length < 3
312+
? [
313+
{ ...item, Id: item.Id + '-1' },
314+
{ ...item, Id: item.Id + '-2' },
315+
]
316+
: [],
314317
isItemExpandable: item => !item.Id.endsWith('-1') && !item.Id.endsWith('-2'),
315318
expandedItems,
316319
onExpandableItemToggle: ({ detail }) => {
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import React, { useState } from 'react';
5+
6+
import { Button, PropertyFilter } from '~components';
7+
import Header from '~components/header';
8+
import Table from '~components/table';
9+
10+
import { SimplePage } from '../app/templates';
11+
import { BulkActionModal, DataGrouping, FilterLayout } from './grouped-table/grouped-table-components';
12+
import {
13+
createColumnDefinitions,
14+
getHeaderCounterText,
15+
getLoaderSelectionAriaLabel,
16+
getSelectionAriaLabel,
17+
useProgressiveLoading,
18+
useTransactions,
19+
} from './grouped-table/grouped-table-config';
20+
import { allTransactions, isGroupRow, TransactionRow } from './grouped-table/grouped-table-data';
21+
import { getMatchesCountText, renderAriaLive } from './shared-configs';
22+
23+
export default () => {
24+
const [confirmationDialogOpen, setConfirmationDialogOpen] = useState(false);
25+
const collection = useProgressiveLoading(useTransactions(), {
26+
getLabel: (item: TransactionRow) => item.group,
27+
getCount: (item: TransactionRow) => (isGroupRow(item) ? item.transactions.length : 1),
28+
});
29+
return (
30+
<SimplePage title="Grouped table demo with collection hooks" i18n={{}} screenshotArea={{}}>
31+
<Table
32+
items={collection.items}
33+
{...collection.collectionProps}
34+
stickyColumns={{ first: 1 }}
35+
resizableColumns={true}
36+
columnDefinitions={createColumnDefinitions(collection)}
37+
ariaLabels={{
38+
tableLabel: 'Transactions table',
39+
selectionGroupLabel: 'Transactions selection',
40+
allItemsSelectionLabel: getSelectionAriaLabel,
41+
itemSelectionLabel: getSelectionAriaLabel,
42+
itemLoaderSelectionLabel: getLoaderSelectionAriaLabel,
43+
}}
44+
renderAriaLive={renderAriaLive}
45+
variant="borderless"
46+
header={
47+
<Header
48+
variant="h2"
49+
counter={getHeaderCounterText(allTransactions.length, collection.collectionProps.selectedItems)}
50+
actions={
51+
<Button
52+
variant="primary"
53+
disabled={collection.collectionProps.selectedItems?.length === 0}
54+
onClick={() => setConfirmationDialogOpen(true)}
55+
>
56+
Bulk update
57+
</Button>
58+
}
59+
>
60+
Transactions
61+
</Header>
62+
}
63+
filter={
64+
<FilterLayout
65+
filter={
66+
<PropertyFilter
67+
{...collection.propertyFilterProps}
68+
countText={getMatchesCountText(collection.filteredItemsCount ?? 0)}
69+
filteringPlaceholder="Search transactions"
70+
/>
71+
}
72+
dataGrouping={<DataGrouping groups={collection.groups} onChange={collection.setGroups} />}
73+
/>
74+
}
75+
/>
76+
{confirmationDialogOpen && (
77+
<BulkActionModal
78+
collection={collection}
79+
onDismiss={() => setConfirmationDialogOpen(false)}
80+
onSubmit={() => {
81+
setConfirmationDialogOpen(false);
82+
collection.actions.setGroupSelection({ inverted: false, toggledItems: [] });
83+
}}
84+
/>
85+
)}
86+
</SimplePage>
87+
);
88+
};
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import React, { useRef, useState } from 'react';
5+
6+
import { UseCollectionResult } from '@cloudscape-design/collection-hooks';
7+
8+
import {
9+
Box,
10+
Button,
11+
Checkbox,
12+
FormField,
13+
Modal,
14+
Popover,
15+
PopoverProps,
16+
Select,
17+
SelectProps,
18+
SpaceBetween,
19+
Token,
20+
} from '~components';
21+
22+
import { groupOptions, sortOptions } from './grouped-table-config';
23+
import { GroupDefinition, isGroupRow, TransactionRow } from './grouped-table-data';
24+
import { createWysiwygQuery } from './grouped-table-query';
25+
26+
import styles from './styles.scss';
27+
28+
export function FilterLayout({ filter, dataGrouping }: { filter: React.ReactNode; dataGrouping: React.ReactNode }) {
29+
return (
30+
<div className={styles['filter-layout']}>
31+
<div className={styles['filter-layout-filter']}>{filter}</div>
32+
<div>{dataGrouping}</div>
33+
</div>
34+
);
35+
}
36+
37+
export function DataGrouping({
38+
groups,
39+
onChange,
40+
}: {
41+
groups: GroupDefinition[];
42+
onChange: (groups: GroupDefinition[]) => void;
43+
}) {
44+
const popoverRef = useRef<PopoverProps.Ref>(null);
45+
const propertyToGroup = new Map(groups.map(g => [g.property, g]));
46+
return (
47+
<div className={styles['grouping-wrapper']}>
48+
<Box fontWeight="bold">group by</Box>
49+
<ul aria-label="selected data groups">
50+
{groups.map(({ property, sorting }, groupIndex) => {
51+
const groupLabel = `${groupOptions.find(o => o.value === property)!.label} (${sorting})`;
52+
return (
53+
<li key={property}>
54+
<Token
55+
ariaLabel={groupLabel}
56+
label={
57+
<Popover
58+
ref={popoverRef}
59+
header="Edit group"
60+
content={
61+
<GroupEditor
62+
group={{ property, sorting }}
63+
groupOptions={groupOptions.filter(o => o.value === property || !propertyToGroup.get(o.value))}
64+
onDismiss={() => popoverRef.current?.dismiss()}
65+
onSubmit={updated => {
66+
onChange(groups.map((g, index) => (index === groupIndex ? updated : g)));
67+
popoverRef.current?.dismiss();
68+
}}
69+
/>
70+
}
71+
>
72+
{groupLabel}
73+
</Popover>
74+
}
75+
onDismiss={() => onChange(groups.filter((_, index) => index !== groupIndex))}
76+
dismissLabel={`Remove group ${groupLabel}`}
77+
/>
78+
</li>
79+
);
80+
})}
81+
</ul>
82+
{groups.length < 5 && (
83+
<Popover
84+
ref={popoverRef}
85+
triggerType="custom"
86+
header="Add group"
87+
content={
88+
<GroupEditor
89+
group={{ property: undefined, sorting: undefined }}
90+
groupOptions={groupOptions.filter(o => !propertyToGroup.get(o.value))}
91+
onDismiss={() => popoverRef.current?.dismiss()}
92+
onSubmit={group => {
93+
onChange([...groups, group]);
94+
popoverRef.current?.dismiss();
95+
}}
96+
/>
97+
}
98+
>
99+
<Button variant="inline-link">add group</Button>
100+
</Popover>
101+
)}
102+
</div>
103+
);
104+
}
105+
106+
function GroupEditor({
107+
group,
108+
groupOptions,
109+
onDismiss,
110+
onSubmit,
111+
}: {
112+
group: Partial<GroupDefinition>;
113+
groupOptions: SelectProps.Option[];
114+
onDismiss: () => void;
115+
onSubmit: (group: GroupDefinition) => void;
116+
}) {
117+
const [selectedProperty, setSelectedProperty] = useState<null | string>(group.property ?? null);
118+
const [selectedSorting, setSelectedSorting] = useState<null | 'asc' | 'desc'>(group.sorting ?? null);
119+
return (
120+
<SpaceBetween size="m">
121+
<FormField label="Property">
122+
<Select
123+
options={groupOptions}
124+
selectedOption={groupOptions.find(o => o.value === selectedProperty) ?? null}
125+
onChange={({ detail }) => setSelectedProperty(detail.selectedOption.value!)}
126+
/>
127+
</FormField>
128+
<FormField label="Sort by">
129+
<Select
130+
options={sortOptions}
131+
selectedOption={sortOptions.find(o => o.value === selectedSorting) ?? null}
132+
onChange={({ detail }) => setSelectedSorting(detail.selectedOption.value as 'asc' | 'desc')}
133+
/>
134+
</FormField>
135+
<SpaceBetween size="m" direction="horizontal">
136+
<Button variant="link" onClick={onDismiss}>
137+
Cancel
138+
</Button>
139+
<Button
140+
disabled={!selectedProperty || !selectedSorting}
141+
onClick={() => onSubmit({ property: selectedProperty!, sorting: selectedSorting! })}
142+
>
143+
Apply
144+
</Button>
145+
</SpaceBetween>
146+
</SpaceBetween>
147+
);
148+
}
149+
150+
export function BulkActionModal({
151+
collection,
152+
onDismiss,
153+
onSubmit,
154+
}: {
155+
collection: UseCollectionResult<TransactionRow>;
156+
onDismiss: () => void;
157+
onSubmit: () => void;
158+
}) {
159+
const [includeNew, setIncludeNew] = useState(true);
160+
const selectedItemsCount = collection.collectionProps.selectedItems?.length ?? 0;
161+
const query = createWysiwygQuery(collection.items, {
162+
getId: item => item.key,
163+
getGroup: item => [item.groupKey, item.group],
164+
getChildren: item => (isGroupRow(item) ? item.children : []),
165+
selection: collection.collectionProps.expandableRows!.groupSelection!,
166+
filter: collection.propertyFilterProps!.query,
167+
});
168+
return (
169+
<Modal
170+
header="Update confirmation"
171+
visible={true}
172+
onDismiss={onDismiss}
173+
footer={
174+
<Box float="right">
175+
<SpaceBetween direction="horizontal" size="xs">
176+
<Button variant="link" onClick={onDismiss}>
177+
Cancel
178+
</Button>
179+
<Button variant="primary" onClick={onSubmit}>
180+
Submit
181+
</Button>
182+
</SpaceBetween>
183+
</Box>
184+
}
185+
>
186+
<SpaceBetween size="s">
187+
<Box fontWeight="bold">You selected {selectedItemsCount} transactions for update.</Box>
188+
189+
<Checkbox checked={includeNew} onChange={({ detail }) => setIncludeNew(detail.checked)}>
190+
Include new transactions that match the current filters.
191+
</Checkbox>
192+
193+
<FormField label="Update query">
194+
<Box variant="awsui-inline-code">{query}</Box>
195+
</FormField>
196+
</SpaceBetween>
197+
</Modal>
198+
);
199+
}

0 commit comments

Comments
 (0)