Skip to content

Replace lib-ui useSelectionState with a batteries implementation, fix related quirks, handle shift+click multiselect and bulk selection #3

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 29 commits into from
Nov 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
dd60741
WIP - add internal useSelectionState to replace lib-ui version
mturley Nov 6, 2023
ace60fa
Persistent state fns for selection -- maybe don't need them?
mturley Nov 7, 2023
f2c1b1a
WIP getSelectionDerivedState
mturley Nov 8, 2023
553fb95
TODOs
mturley Nov 8, 2023
38b6fce
WIP moving selection prop helpers into their own hook and fixing high…
mturley Nov 14, 2023
4a4b3b3
WIP selection effects and more refactoring for selection
mturley Nov 14, 2023
085b87a
WIP finishing useSelectionPropHelpers and useSelectionEffects
mturley Nov 14, 2023
faac7e0
WIP getSelectionDerivedState
mturley Nov 14, 2023
baad5f6
fix type errors
mturley Nov 14, 2023
9855254
WIP rename getSelectionDerivedState -> useSelectionDerivedState
mturley Nov 15, 2023
b5f31de
Workarounds for TS inference issue
mturley Nov 15, 2023
2f084e2
WIP workarounds and logging
mturley Nov 15, 2023
09cb704
Fall back on empty array of selected item ids
mturley Nov 15, 2023
ac3255a
Selection example
mturley Nov 15, 2023
fdb0cab
Fix import issue with ToolbarBulkSelector in example
mturley Nov 15, 2023
d10287c
Clean up type assertions
mturley Nov 15, 2023
b8b47a2
Fix wonky bulk select behavior
mturley Nov 16, 2023
ec76120
Memoize selectedItems
mturley Nov 17, 2023
b63b9b6
Rename all get*DerivedState to use*DerivedState
mturley Nov 17, 2023
c2f84e3
WIP moving use*Effects to use*DerivedState instead of use*PropHelpers…
mturley Nov 17, 2023
67ec424
Revert "WIP moving use*Effects to use*DerivedState instead of use*Pro…
mturley Nov 17, 2023
4e6b044
Update selection example to use selectedItems
mturley Nov 17, 2023
eed7258
Use compact tables in examples, remove old selection-related code fro…
mturley Nov 17, 2023
a44ab99
Don't offer select all action when we don't have all items (server ta…
mturley Nov 17, 2023
0a953e3
Disable checkboxes for non-selectable items
mturley Nov 17, 2023
b9a9666
Implement shift+click multiselect
mturley Nov 17, 2023
93dea85
Fix bulk selector issues with non-selectable items
mturley Nov 17, 2023
ec5bde4
description
mturley Nov 17, 2023
b877688
Fix a11y error
mturley Nov 17, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -161,19 +161,7 @@ export const ExampleAdvancedCaching: React.FunctionComponent = () => {
isLoading: isLoadingMockData,
currentPageItems: mockFetchResponse?.data || [],
totalItemCount: mockFetchResponse?.totalItemCount || 0,
// TODO this shouldn't be necessary once we refactor useSelectionState to fit the rest of the table-batteries pattern.
// Due to an unresolved issue, the `selectionState` is required here even though we're not using selection.
// As a temporary workaround we pass stub values for these properties.
selectionState: {
selectedItems: [],
isItemSelected: () => false,
isItemSelectable: () => false,
toggleItemSelected: () => {},
selectMultiple: () => {},
areAllSelected: false,
selectAll: () => {},
setSelectedItems: () => {}
}
variant: 'compact'
});

// Everything below is the same as in the basic client-side example!
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,8 @@ export const ExampleAdvancedPersistTargets: React.FunctionComponent = () => {
description: thing.description || ''
}),
initialSort: { columnKey: 'name', direction: 'asc' },
isLoading: isLoadingMockData
isLoading: isLoadingMockData,
variant: 'compact'
});

// Here we destructure some of the properties from `tableBatteries` for rendering.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,8 @@ export const ExampleBasicClientPaginated: React.FunctionComponent = () => {
description: thing.description || ''
}),
initialSort: { columnKey: 'name', direction: 'asc' },
isLoading: isLoadingMockData
isLoading: isLoadingMockData,
variant: 'compact'
});

// Here we destructure some of the properties from `tableBatteries` for rendering.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,19 +136,7 @@ export const ExampleBasicServerPaginated: React.FunctionComponent = () => {
isLoading: isLoadingMockData,
currentPageItems: mockApiResponse.data,
totalItemCount: mockApiResponse.totalItemCount,
// TODO this shouldn't be necessary once we refactor useSelectionState to fit the rest of the table-batteries pattern.
// Due to an unresolved issue, the `selectionState` is required here even though we're not using selection.
// As a temporary workaround we pass stub values for these properties.
selectionState: {
selectedItems: [],
isItemSelected: () => false,
isItemSelectable: () => false,
toggleItemSelected: () => {},
selectMultiple: () => {},
areAllSelected: false,
selectAll: () => {},
setSelectedItems: () => {}
}
variant: 'compact'
});

// Everything below is the same as in the basic client-side example!
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,190 @@
import React from 'react';
import { ExtendedButton } from '@patternfly-labs/react-table-batteries';

export const ExampleFeatureSelection: React.FunctionComponent = () => (
<>
Table stuff goes here!
<ExtendedButton>My custom extension button</ExtendedButton>
</>
);
import {
Toolbar,
ToolbarContent,
ToolbarItem,
EmptyState,
EmptyStateIcon,
Title,
Pagination
} from '@patternfly/react-core';
import CubesIcon from '@patternfly/react-icons/dist/esm/icons/cubes-icon';
import { Table, Thead, Tr, Th, Tbody, Td } from '@patternfly/react-table';
import {
useClientTableBatteries,
TableHeaderContentWithBatteries,
ConditionalTableBody,
TableRowContentWithBatteries,
FilterToolbar,
FilterType,
ToolbarBulkSelector
} from '@patternfly-labs/react-table-batteries';

// This example table's rows represent Thing objects in our fake API.
interface Thing {
id: number;
name: string;
description: string;
}

// This is a barebones mock API server to demonstrate fetching data.
// We use a timeout of 1000ms here to simulate the loading state when data is fetched.
interface MockAPIResponse {
data: Thing[];
}
const fetchMockData = () =>
new Promise<MockAPIResponse>((resolve) => {
setTimeout(() => {
const mockData: Thing[] = [
{ id: 1, name: 'Thing 01', description: 'Something from the API' },
{ id: 2, name: 'Thing 02', description: 'Something else from the API' },
{ id: 3, name: 'Thing 03', description: 'Another API object. This one is not selectable!' },
{ id: 4, name: 'Thing 04', description: 'We have more than 10 things here' },
{ id: 5, name: 'Thing 05', description: 'So you can try the "select page" behavior' },
{ id: 6, name: 'Thing 06', description: 'These all need descriptions' },
{ id: 7, name: 'Thing 07', description: 'But there is nothing else to say' },
{ id: 8, name: 'Thing 08', description: "I hope you're enjoying these examples" },
{ id: 9, name: 'Thing 09', description: 'Some pretty cool stuff if I do say so myself' },
{ id: 10, name: 'Thing 10', description: 'See you later' },
{ id: 11, name: 'Thing 11', description: 'Oh hey you made it to page 2' },
{ id: 12, name: 'Thing 12', description: 'Nice work' }
];
resolve({ data: mockData });
}, 1000);
});

export const ExampleFeatureSelection: React.FunctionComponent = () => {
// In a real table we'd use a real API fetch here, perhaps using a library like react-query.
const [mockApiResponse, setMockApiResponse] = React.useState<MockAPIResponse>({ data: [] });
const [isLoadingMockData, setIsLoadingMockData] = React.useState(false);
React.useEffect(() => {
setIsLoadingMockData(true);
fetchMockData().then((response) => {
setMockApiResponse(response);
setIsLoadingMockData(false);
});
}, []);

const tableBatteries = useClientTableBatteries({
persistTo: 'urlParams',
persistenceKeyPrefix: 't1', // The first Things table on this page.
idProperty: 'id', // The name of a unique string or number property on the data items.
items: mockApiResponse.data, // The generic type `TItem` is inferred from the items passed here.
columnNames: {
// The keys of this object define the inferred generic type `TColumnKey`. See "Unique Identifiers".
name: 'Name',
description: 'Description'
},
isFilterEnabled: true,
isSortEnabled: true,
isPaginationEnabled: true,
isSelectionEnabled: true,
// Because isFilterEnabled is true, TypeScript will require these filterCategories:
filterCategories: [
{
key: 'name',
title: 'Name',
type: FilterType.search,
placeholderText: 'Filter by name...',
getItemValue: (thing) => thing.name || ''
},
{
key: 'description',
title: 'Description',
type: FilterType.search,
placeholderText: 'Filter by description...'
}
],
// Because isSortEnabled is true, TypeScript will require these sort-related properties:
sortableColumns: ['name', 'description'],
getSortValues: (thing) => ({
name: thing.name || '',
description: thing.description || ''
}),
initialSort: { columnKey: 'name', direction: 'asc' },
isItemSelectable: (item) => item.id !== 3, // Testing the non-selectable item behavior
isLoading: isLoadingMockData,
variant: 'compact'
});

// Here we destructure some of the properties from `tableBatteries` for rendering.
// Later we also spread the entire `tableBatteries` object onto components whose props include subsets of it.
const {
currentPageItems, // These items have already been paginated.
// `numRenderedColumns` is based on the number of columnNames and additional columns needed for
// rendering controls related to features like selection, expansion, etc.
// It is used as the colSpan when rendering a full-table-wide cell.
numRenderedColumns,
// The objects and functions in `propHelpers` correspond to the props needed for specific PatternFly or Tackle
// components and are provided to reduce prop-drilling and make the rendering code as short as possible.
propHelpers: {
toolbarProps,
toolbarBulkSelectorProps,
filterToolbarProps,
paginationToolbarItemProps,
paginationProps,
tableProps,
getThProps,
getTrProps,
getTdProps
},
selectionDerivedState: { selectedItems }
} = tableBatteries;

// eslint-disable-next-line no-console
console.log('Do something with selected items:', selectedItems);

return (
<>
<Toolbar {...toolbarProps}>
<ToolbarContent>
<ToolbarBulkSelector {...toolbarBulkSelectorProps} />
<FilterToolbar {...filterToolbarProps} id="client-paginated-example-filters" />
{/* You can render whatever other custom toolbar items you may need here! */}
<ToolbarItem {...paginationToolbarItemProps}>
<Pagination variant="top" isCompact {...paginationProps} widgetId="client-paginated-example-pagination" />
</ToolbarItem>
</ToolbarContent>
</Toolbar>
<Table {...tableProps} aria-label="Example things table">
<Thead>
<Tr>
<TableHeaderContentWithBatteries {...tableBatteries}>
<Th {...getThProps({ columnKey: 'name' })} />
<Th {...getThProps({ columnKey: 'description' })} />
</TableHeaderContentWithBatteries>
</Tr>
</Thead>
<ConditionalTableBody
isLoading={isLoadingMockData}
isNoData={currentPageItems.length === 0}
noDataEmptyState={
<EmptyState variant="sm">
<EmptyStateIcon icon={CubesIcon} />
<Title headingLevel="h2" size="lg">
No things available
</Title>
</EmptyState>
}
numRenderedColumns={numRenderedColumns}
>
<Tbody>
{currentPageItems?.map((thing, rowIndex) => (
<Tr key={thing.id} {...getTrProps({ item: thing })}>
<TableRowContentWithBatteries {...tableBatteries} item={thing} rowIndex={rowIndex}>
<Td width={30} {...getTdProps({ columnKey: 'name' })}>
{thing.name}
</Td>
<Td width={70} {...getTdProps({ columnKey: 'description' })}>
{thing.description}
</Td>
</TableRowContentWithBatteries>
</Tr>
))}
</Tbody>
</ConditionalTableBody>
</Table>
<Pagination variant="bottom" isCompact {...paginationProps} widgetId="client-paginated-example-pagination" />
</>
);
};
13 changes: 5 additions & 8 deletions packages/module/patternfly-docs/content/examples/basic.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
useClientTableBatteries,
useTablePropHelpers,
useTableState,
ToolbarBulkSelector,
TableHeaderContentWithBatteries,
ConditionalTableBody,
TableRowContentWithBatteries,
Expand Down Expand Up @@ -130,7 +131,7 @@ The easiest way to achieve this caching behavior is to use a data fetching libra

TODO this example will come in a separate PR

TODO don't use useTableState, but use getClientTableDerivedState
TODO don't use useTableState, but use useClientTableDerivedState

TODO remark on how this may be helpful for incremental adoption

Expand Down Expand Up @@ -158,7 +159,7 @@ TODO this example will come in a separate PR

TODO remark on how all the state management and built-in logic provided by `useTableState` and `useClientTableBatteries` is optional, and if you want your table to handle all its own business logic you can still benefit from useTablePropHelpers to make rendering easier.

TODO remark on how this may be helpful as the first step in incremental adoption, followed by adopting `useTableState` and then maybe `getClientTableDerivedState` or the full `useClientTableBatteries` (as in the [basic client-side example](#client-side-filteringsortingpagination)).
TODO remark on how this may be helpful as the first step in incremental adoption, followed by adopting `useTableState` and then maybe `useClientTableDerivedState` or the full `useClientTableBatteries` (as in the [basic client-side example](#client-side-filteringsortingpagination)).

```js file="./ExampleAdvancedBYOStateAndLogic.tsx"

Expand Down Expand Up @@ -208,11 +209,7 @@ TODO copy over and rework things from OLD_DOCS.md here

### Selection

TODO this example will come in a separate PR

TODO add a version of the basic client table example with only selection enabled

TODO copy over and rework things from OLD_DOCS.md here
TODO copy over and rework notes from OLD_DOCS.md here

```js file="./ExampleFeatureSelection.tsx"

Expand Down Expand Up @@ -285,7 +282,7 @@ If your API does not support these parameters or you need to have the entire col
In most cases, you'll only need to use these higher-level hooks and helpers to build a table:

- For client-paginated tables: `useClientTableBatteries` is all you need.
- Internally it uses `useTableState`, `useTablePropHelpers` and the `getClientTableDerivedState` helper. The config arguments object is a combination of the arguments required by `useTableState` and `useTablePropHelpers`.
- Internally it uses `useTableState`, `useTablePropHelpers` and the `useClientTableDerivedState` helper. The config arguments object is a combination of the arguments required by `useTableState` and `useTablePropHelpers`.
- The return value (an object we generally name `tableBatteries`) has everything you need to render your table. Give it a `console.log` to see what is available.
- For server-paginated tables: `useTableState`, `getHubRequestParams`, and `useTablePropHelpers`.
- Choose whether you want to use React state (default), URL params (recommended), localStorage or sessionStorage as the source of truth, and call `useTableState` with the appropriate `persistTo` option and optional `persistenceKeyPrefix` (to namespace persisted state for multiple tables on the same page).
Expand Down
10 changes: 5 additions & 5 deletions packages/module/src/OLD_DOCS.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ Items are filtered according to user-selected filter key/value pairs.

- Keys and filter types (search, select, etc) are defined by the `filterCategories` array config argument. The `key` properties of each of these `FilterCategory` objects are the source of truth for the inferred generic type `TFilterCategoryKeys` (For more, see the JSDoc comments in the `types.ts` file).
- Filter state is provided by `useFilterState`.
- For client-side filtering, the filter logic is provided by `getClientFilterDerivedState` (based on the `getItemValue` callback defined on each `FilterCategory` object, which is not required when using server-side filtering).
- For client-side filtering, the filter logic is provided by `useClientFilterDerivedState` (based on the `getItemValue` callback defined on each `FilterCategory` object, which is not required when using server-side filtering).
- For server-side filtering, filter state is serialized for the API by `getFilterHubRequestParams`.
- Filter-related component props are provided by `useFilterPropHelpers`.
- Filter inputs and chips are rendered by the `FilterToolbar` component.
Expand All @@ -194,7 +194,7 @@ Items are sorted according to the user-selected sort column and direction.

- Sortable columns are defined by a `sortableColumns` array of `TColumnKey` values (see [Unique Identifiers](#unique-identifiers)).
- Sort state is provided by `useSortState`.
- For client-side sorting, the sort logic is provided by `getClientSortDerivedState` (based on the `getSortValues` config argument, which is not required when using server-side sorting).
- For client-side sorting, the sort logic is provided by `useClientSortDerivedState` (based on the `getSortValues` config argument, which is not required when using server-side sorting).
- For server-side sorting, sort state is serialized for the API by `getSortHubRequestParams`.
- Sort-related component props are provided by `useSortPropHelpers`.
- Sort inputs are rendered by the table's `Th` PatternFly component.
Expand All @@ -205,7 +205,7 @@ Items are paginated according to the user-selected page number and items-per-pag

- The only config argument for pagination is the optional `initialItemsPerPage` which defaults to 10.
- Pagination state is provided by `usePaginationState`.
- For client-side pagination, the pagination logic is provided by `getClientPaginationDerivedState`.
- For client-side pagination, the pagination logic is provided by `useClientPaginationDerivedState`.
- For server-side pagination, pagination state is serialized for the API by `getPaginationHubRequestParams`.
- Pagination-related component props are provided by `usePaginationPropHelpers`.
- A `useEffect` call which prevents invalid state after an item is deleted is provided by `usePaginationEffects`. This is called internally by `usePaginationPropHelpers`.
Expand All @@ -221,7 +221,7 @@ Item details can be expanded, either with a "single expansion" variant where an

- Single or compound expansion is defined by the optional `expandableVariant` config argument which defaults to `"single"`.
- Expansion state is provided by `useExpansionState`.
- Expansion shorthand functions are provided by `getExpansionDerivedState`.
- Expansion shorthand functions are provided by `useExpansionDerivedState`.
- Expansion is never managed server-side.
- Expansion-related component props are provided by `useExpansionPropHelpers`.
- Expansion inputs are rendered by the table's `Td` PatternFly component and expanded content is managed at the consumer level by conditionally rendering a second row with full colSpan inside a PatternFly `Tbody` component. The `numRenderedColumns` value returned by `useTablePropHelpers` can be used for the correct colSpan here.
Expand All @@ -232,7 +232,7 @@ A row can be clicked to mark its item as "active", which usually opens a drawer

- The active item feature requires no config arguments.
- Active item state is provided by `useActiveItemState`.
- Active item shorthand functions are provided by `getActiveItemDerivedState`.
- Active item shorthand functions are provided by `useActiveItemDerivedState`.
- Active-item-related component props are provided by `useActiveItemPropHelpers`.
- A `useEffect` call which prevents invalid state after an item is deleted is provided by `useActiveItemEffects`. This is called internally in `useActiveItemPropHelpers`.

Expand Down
2 changes: 1 addition & 1 deletion packages/module/src/hooks/active-item/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export * from './useActiveItemState';
export * from './getActiveItemDerivedState';
export * from './useActiveItemDerivedState';
export * from './useActiveItemPropHelpers';
export * from './useActiveItemEffects';
Loading