Skip to content

Commit

Permalink
Merge pull request #3 from mturley/fix-selection
Browse files Browse the repository at this point in the history
Replace lib-ui useSelectionState with a batteries implementation, fix related quirks, handle shift+click multiselect and bulk selection
  • Loading branch information
mturley authored Nov 21, 2023
2 parents 58bf4dc + b877688 commit e67b6fe
Show file tree
Hide file tree
Showing 35 changed files with 737 additions and 203 deletions.
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

0 comments on commit e67b6fe

Please sign in to comment.