Skip to content
44 changes: 35 additions & 9 deletions src/components/FilterModal/FilterModal.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,32 +8,45 @@
Button,
} from '@patternfly/react-core';

import { TableToolsTable, TableStateProvider } from '~/components';

import { filterOption, filterGroup } from './columns';
import { convertToSelectValues, convertToFilterValues } from './helpers';
import {
TableToolsTable,
TableStateProvider,
StaticTableToolsTable,
} from '~/components';

import useFetchItems from './hooks/useFetchItems';
import { convertToSelectValues, convertToFilterValues } from './helpers';
import { filterOption, filterGroup } from './columns';
import filters from './filters';

const FilterModal = ({
isFilterModalOpen,
filter,
onClose,
activeFilters,
onChange,
setAsyncItems,
tableOptions,
}) => {
const items = filter.modal?.items || filter.items;
const isAsync = typeof (filter.modal?.items || filter.items) === 'function';
const TableComponent = isAsync ? TableToolsTable : StaticTableToolsTable;

const title = filter?.modal?.title || `Filter by ${filter.label}`;
const defaultColumns =
filter.type === 'group' ? [filterOption, filterGroup] : [filterOption];
const {
modal: { columns = defaultColumns, applyLabel = 'Apply selected' } = {},
} = filter;

const fetchItems = useFetchItems({
items,
filter,
setAsyncItems,
});
const [selectedFilterValues, setSelectedFilterValues] =
useState(activeFilters);

// TODO Replace this with using the "StaticTableToolsTable" instead for cases where there is no function to fetch
const fetchItems = useFetchItems(filter);
const selected = convertToSelectValues(activeFilters, filter);

const onSelect = useCallback(
Expand All @@ -51,13 +64,24 @@
>
<ModalHeader title={title} />
<ModalBody>
<TableToolsTable
<TableComponent
variant="compact"
items={fetchItems}
items={
isAsync
? fetchItems
: items.map((item) => ({
...item,
id: item.label,
}))
}
columns={columns}
filters={{
filterConfig: filters,
}}
options={{
selected,
onSelect,
...tableOptions,
}}
/>
</ModalBody>
Expand Down Expand Up @@ -92,12 +116,14 @@
onChange: propTypes.func,
isFilterModalOpen: propTypes.bool,
onClose: propTypes.func,
setAsyncItems: propTypes.func,
tableOptions: propTypes.object,
};

/**

Check warning on line 123 in src/components/FilterModal/FilterModal.js

View workflow job for this annotation

GitHub Actions / build (21.x)

Missing JSDoc @param "props" declaration

Check warning on line 123 in src/components/FilterModal/FilterModal.js

View workflow job for this annotation

GitHub Actions / build (22.x)

Missing JSDoc @param "props" declaration
* Component used to provide a modal for filters that have the modal enabled
*
* @returns {React.ReactElement}

Check warning on line 126 in src/components/FilterModal/FilterModal.js

View workflow job for this annotation

GitHub Actions / build (21.x)

Missing JSDoc @returns description

Check warning on line 126 in src/components/FilterModal/FilterModal.js

View workflow job for this annotation

GitHub Actions / build (22.x)

Missing JSDoc @returns description
*
* @group Components
*
Expand All @@ -106,7 +132,7 @@
// TODO Pass down "primary table" state

return (
<TableStateProvider>
<TableStateProvider isNewContext>
<FilterModal {...props} />
</TableStateProvider>
);
Expand Down
1 change: 1 addition & 0 deletions src/components/FilterModal/columns.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const FilterGroupText = ({ group }) => group;
export const filterOption = {
title: '',
Component: FilterOptionText,
exportKey: 'label',
};

export const filterGroup = {
Expand Down
7 changes: 7 additions & 0 deletions src/components/FilterModal/filters.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const label = {
type: 'text',
label: 'Name',
filterAttribute: 'label',
};

export default [label];
5 changes: 5 additions & 0 deletions src/components/FilterModal/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,8 @@ export const convertToSelectValues = (filterValues, filter) => {
return filterValues;
}
};

export const labelToid = (item) => ({
...item,
id: item.label,
});
33 changes: 16 additions & 17 deletions src/components/FilterModal/hooks/useFetchItems.js
Original file line number Diff line number Diff line change
@@ -1,26 +1,25 @@
import { useCallback } from 'react';
import { fetchStatic } from '../helpers';
import { stringToId } from '~/hooks/useFilterConfig/helpers';
import { labelToid } from '../helpers';

const useFetchItems = ({ items, filter, setAsyncItems }) => {
const id = stringToId(filter.label);

const useFetchItems = ({
items: filterItems,
groups: groupItems,
modal: { items: modalItems } = {},
type,
}) => {
const fetchItems = useCallback(
(...args) => {
if (modalItems) {
return modalItems(...args);
} else if (
typeof filterItems === 'function' ||
typeof groupItems === 'function'
) {
return (filterItems || groupItems)(...args);
async (...args) => {
const filterItems = await items(...args);
// TODO extract identifying "table tools request returns"
if (Array.isArray(filterItems[0]) && typeof filterItems[1] === 'number') {
setAsyncItems(id, filterItems[0]);

return [filterItems[0].map(labelToid), filterItems[1]];
} else {
return fetchStatic(filterItems || groupItems, type, ...args);
setAsyncItems(id, filterItems);

return [filterItems.map(labelToid), filterItems.length];
}
},
[modalItems, filterItems, groupItems, type],
[id, items, setAsyncItems],
);

return fetchItems;
Expand Down
16 changes: 12 additions & 4 deletions src/components/TableStateProvider/TableStateProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@

import { TableContext } from '~/hooks/useTableState/constants';

/**

Check warning on line 6 in src/components/TableStateProvider/TableStateProvider.js

View workflow job for this annotation

GitHub Actions / build (21.x)

Missing JSDoc @param "props.parentContext" declaration

Check warning on line 6 in src/components/TableStateProvider/TableStateProvider.js

View workflow job for this annotation

GitHub Actions / build (22.x)

Missing JSDoc @param "props.parentContext" declaration
* This component provides a context for components/hooks that want to use async tables and access it's state to perform API requests
*
* @param {object} [props] Component Props

Check warning on line 9 in src/components/TableStateProvider/TableStateProvider.js

View workflow job for this annotation

GitHub Actions / build (21.x)

Missing @param "props.parentContext"

Check warning on line 9 in src/components/TableStateProvider/TableStateProvider.js

View workflow job for this annotation

GitHub Actions / build (22.x)

Missing @param "props.parentContext"
* @param {React.ReactElement} [props.children] Child components to render within
*
* @returns {React.ReactElement} The passed in component wrapped in a TableContext provider
Expand All @@ -16,7 +16,7 @@
* @group Components
*
*/
const TableStateProvider = ({ children }) => {
const TableStateProvider = ({ parentContext, children }) => {
const state = useState();
const observers = useRef({});
const serialisers = useRef({});
Expand All @@ -26,6 +26,7 @@
return (
<TableContext.Provider
value={{
parentContext,
state,
observers,
serialisers,
Expand All @@ -40,17 +41,24 @@

TableStateProvider.propTypes = {
children: propTypes.node,
parentContext: propTypes.object,
};

const TableStateProviderWrapper = ({ children }) => {
const TableStateProviderWrapper = ({ isNewContext, children }) => {
const tableContext = useContext(TableContext);
const Wrapper = tableContext ? React.Fragment : TableStateProvider;
const Wrapper =
tableContext && !isNewContext ? React.Fragment : TableStateProvider;

return <Wrapper>{children}</Wrapper>;
return (
<Wrapper {...(isNewContext ? { parentContext: tableContext } : {})}>
{children}
</Wrapper>
);
};

TableStateProviderWrapper.propTypes = {
children: propTypes.node,
isNewContext: propTypes.bool,
};

export default TableStateProviderWrapper;
164 changes: 163 additions & 1 deletion src/components/TableToolsTable/TableToolsTableExperiments.stories.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import React, { useCallback, useEffect } from 'react';
import React, { useCallback, useEffect, useMemo } from 'react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { Card, CardBody, Spinner, Button, Label } from '@patternfly/react-core';

import defaultStoryMeta from '~/support/defaultStoryMeta';
import columns from '~/support/factories/columns';
import { genres } from '~/support/factories/items';

import paginationSerialiser from '~/components/StaticTableToolsTable/helpers/serialisers/pagination';
import sortSerialiser from '~/components/StaticTableToolsTable/helpers/serialisers/sort';
import filtersSerialiser from '~/components/StaticTableToolsTable/helpers/serialisers/filters';
Expand Down Expand Up @@ -170,6 +172,166 @@
render: (args) => <BulkSelectExample {...args} />,
};

const genreFilterOptions = genres.map((genre) => ({
label: genre,
value: genre,
}));

const FilterModalExample = () => {
const { fetch: fetchGenre } = useExampleDataQuery({
endpoint: '/api/genres',
skip: true,
});
const { fetch: fetchYearsByDecade } = useExampleDataQuery({
endpoint: '/api/years-by-decade',
skip: true,
});
const {
loading,
result: { data, meta: { total } = {} } = {},
error,
itemIdsInTable,
} = useExampleDataQuery({
endpoint: '/api',
useTableState: true,
});

// This function is used in two ways
// It is called without a serialisedTableState and tableState when it is called for the filter dropdown
// If now separate fetch function is provided it is also called from the table in the modal WITH a serialisedTableState
// In case the request for filter items does require any state of the table it should be explicitly added and NOT use the passed serialisedTableState or tableState params
const genreItemFetch = useCallback(
async (
{ filters, pagination, sort } = {
pagination: { offset: 0, limit: 15 },
},
) => {
const params = {
...(filters ? { filters } : {}),
...(pagination ? pagination : {}),
...(sort ? { sort } : {}),
};
const genresJson = await fetchGenre(params);

return [
genresJson.data.map((genre) => ({
label: genre,
value: genre,
})),
genresJson.meta.total,
];
},
[fetchGenre],
);

const filters = useMemo(() => {
return [
{
type: 'group',
label: 'Years by decade',
filterSerialiser: (_filterConfigItem, value) => {
const allYears = Object.entries(value).reduce(
(years, [, groupYears]) => [...years, ...Object.keys(groupYears)],
[],
);

return `.releaseYear in [${allYears.join(', ')}]`;
},
groups: async () => {
const yearsByDecade = await fetchYearsByDecade();
return yearsByDecade;
},
modal: {
title: 'Select years to filter by',
},
},
{
type: 'checkbox',
label: 'Genre with fetched items',
filterAttribute: 'genre',
items: genreItemFetch,
modal: {
tableOptions: {
exporter: async () =>
(await genreItemFetch({ pagination: { limit: 'max' } }))[0],
// TODO Find way to put items fetch here to the async cache
// Or find different way to allow enabling "select all" in the modal
itemIdsInTable: async () =>
(await genreItemFetch({ pagination: { limit: 'max' } }))[0].map(
({ value: id }) => id,
),
},
},
},
{
type: 'checkbox',
label: 'Genre with static items and modal',
filterAttribute: 'genre',
items: genreFilterOptions.slice(0, 30),
modal: true,
},
{
type: 'checkbox',
label: 'Genre with fetched items and modal items',
filterAttribute: 'genre',
items: genreFilterOptions.slice(0, 20),
modal: {
items: async (
_serialisedState,
{ pagination: { state } = { page: 1, perPage: 10 } } = {},
) => {
const offset = (state?.page - 1) * state?.perPage;
const limit = state?.perPage;
const genresResponse = await fetch(
`/api/genres?offset=${offset}&limit=${limit}`,
);
const genresJson = await genresResponse.json();

return [
genresJson.data.map((genre) => ({
label: genre,
value: genre,
})),
genresJson.meta.total,
];
},
},
},
];
}, [genreItemFetch]);

Check warning on line 301 in src/components/TableToolsTable/TableToolsTableExperiments.stories.js

View workflow job for this annotation

GitHub Actions / build (21.x)

React Hook useMemo has a missing dependency: 'fetchYearsByDecade'. Either include it or remove the dependency array

Check warning on line 301 in src/components/TableToolsTable/TableToolsTableExperiments.stories.js

View workflow job for this annotation

GitHub Actions / build (22.x)

React Hook useMemo has a missing dependency: 'fetchYearsByDecade'. Either include it or remove the dependency array

return (
<TableToolsTable
loading={loading}
items={data}
filters={{
filterConfig: filters,
}}
total={total}
error={error}
columns={columns}
options={{
...defaultOptions,
debug: true,
itemIdsInTable,
}}
/>
);
};

export const FilterModalStory = {
decorators: [
(Story) => (
<QueryClientProvider client={queryClient}>
<TableStateProvider>
<Story />
</TableStateProvider>
</QueryClientProvider>
),
],
render: (args) => <FilterModalExample {...args} />,
};

const AllEmptyExample = () => {
const { loading } = useExampleDataQuery({
endpoint: '/api',
Expand Down
Loading
Loading