diff --git a/src/components/FilterModal/FilterModal.js b/src/components/FilterModal/FilterModal.js
index 77b20fa..9b6995c 100644
--- a/src/components/FilterModal/FilterModal.js
+++ b/src/components/FilterModal/FilterModal.js
@@ -8,12 +8,16 @@ import {
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,
@@ -21,7 +25,13 @@ const FilterModal = ({
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];
@@ -29,11 +39,14 @@ const FilterModal = ({
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(
@@ -51,13 +64,24 @@ const FilterModal = ({
>
- ({
+ ...item,
+ id: item.label,
+ }))
+ }
columns={columns}
+ filters={{
+ filterConfig: filters,
+ }}
options={{
selected,
onSelect,
+ ...tableOptions,
}}
/>
@@ -92,6 +116,8 @@ FilterModal.propTypes = {
onChange: propTypes.func,
isFilterModalOpen: propTypes.bool,
onClose: propTypes.func,
+ setAsyncItems: propTypes.func,
+ tableOptions: propTypes.object,
};
/**
@@ -106,7 +132,7 @@ const FilterModalWithProvider = (props) => {
// TODO Pass down "primary table" state
return (
-
+
);
diff --git a/src/components/FilterModal/columns.js b/src/components/FilterModal/columns.js
index 125e103..34d7fb4 100644
--- a/src/components/FilterModal/columns.js
+++ b/src/components/FilterModal/columns.js
@@ -4,6 +4,7 @@ const FilterGroupText = ({ group }) => group;
export const filterOption = {
title: '',
Component: FilterOptionText,
+ exportKey: 'label',
};
export const filterGroup = {
diff --git a/src/components/FilterModal/filters.js b/src/components/FilterModal/filters.js
new file mode 100644
index 0000000..de52ef4
--- /dev/null
+++ b/src/components/FilterModal/filters.js
@@ -0,0 +1,7 @@
+export const label = {
+ type: 'text',
+ label: 'Name',
+ filterAttribute: 'label',
+};
+
+export default [label];
diff --git a/src/components/FilterModal/helpers.js b/src/components/FilterModal/helpers.js
index b540973..9c1f329 100644
--- a/src/components/FilterModal/helpers.js
+++ b/src/components/FilterModal/helpers.js
@@ -64,3 +64,8 @@ export const convertToSelectValues = (filterValues, filter) => {
return filterValues;
}
};
+
+export const labelToid = (item) => ({
+ ...item,
+ id: item.label,
+});
diff --git a/src/components/FilterModal/hooks/useFetchItems.js b/src/components/FilterModal/hooks/useFetchItems.js
index 21c5f2b..3e0917a 100644
--- a/src/components/FilterModal/hooks/useFetchItems.js
+++ b/src/components/FilterModal/hooks/useFetchItems.js
@@ -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;
diff --git a/src/components/TableStateProvider/TableStateProvider.js b/src/components/TableStateProvider/TableStateProvider.js
index fbe3c08..91c0c7b 100644
--- a/src/components/TableStateProvider/TableStateProvider.js
+++ b/src/components/TableStateProvider/TableStateProvider.js
@@ -16,7 +16,7 @@ import { TableContext } from '~/hooks/useTableState/constants';
* @group Components
*
*/
-const TableStateProvider = ({ children }) => {
+const TableStateProvider = ({ parentContext, children }) => {
const state = useState();
const observers = useRef({});
const serialisers = useRef({});
@@ -26,6 +26,7 @@ const TableStateProvider = ({ children }) => {
return (
{
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 {children};
+ return (
+
+ {children}
+
+ );
};
TableStateProviderWrapper.propTypes = {
children: propTypes.node,
+ isNewContext: propTypes.bool,
};
export default TableStateProviderWrapper;
diff --git a/src/components/TableToolsTable/TableToolsTableExperiments.stories.js b/src/components/TableToolsTable/TableToolsTableExperiments.stories.js
index 7b723f8..9f00093 100644
--- a/src/components/TableToolsTable/TableToolsTableExperiments.stories.js
+++ b/src/components/TableToolsTable/TableToolsTableExperiments.stories.js
@@ -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';
@@ -170,6 +172,166 @@ export const BulkSelectStory = {
render: (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]);
+
+ return (
+
+ );
+};
+
+export const FilterModalStory = {
+ decorators: [
+ (Story) => (
+
+
+
+
+
+ ),
+ ],
+ render: (args) => ,
+};
+
const AllEmptyExample = () => {
const { loading } = useExampleDataQuery({
endpoint: '/api',
diff --git a/src/hooks/useFilterConfig/helpers/filterChipHelpers.js b/src/hooks/useFilterConfig/helpers/filterChipHelpers.js
index 4e79672..f5bfa3b 100644
--- a/src/hooks/useFilterConfig/helpers/filterChipHelpers.js
+++ b/src/hooks/useFilterConfig/helpers/filterChipHelpers.js
@@ -4,29 +4,57 @@ import { getFilterConfigItem } from './filterConfigHelpers';
const filterChipTemplates = (configItem, value, filterTypeHelpers) =>
filterTypeHelpers?.filterChips(configItem, value);
-export const toFilterChips = (filterConfig, filterTypes, activeFilters) =>
- Object.entries(activeFilters || {})
+export const toFilterChips = (
+ filterConfig,
+ filterTypes,
+ activeFilters,
+ asyncItems,
+) => {
+ return Object.entries(activeFilters || {})
.map(([filter, value]) => {
const configItem = getFilterConfigItem(filterConfig, filter);
+ const itemsProp = configItem.type === 'groups' ? 'groups' : 'items';
+ const configItemWithAsyncItems = {
+ ...(configItem || {}),
+ [itemsProp]: [
+ ...(configItem?.[itemsProp] || []),
+ ...(asyncItems?.[stringToId(configItem?.label)] || []),
+ ],
+ };
+
return configItem && isNotEmpty(value)
- ? filterChipTemplates(configItem, value, filterTypes[configItem.type])
+ ? filterChipTemplates(
+ configItemWithAsyncItems,
+ value,
+ filterTypes[configItem.type],
+ )
: undefined;
})
.filter((v) => !!v);
+};
export const toDeselectValue = (
filterConfig,
filterTypes,
chip,
activeFilters,
+ asyncItems,
) => {
const configItem = getFilterConfigItem(
filterConfig,
stringToId(chip.category),
);
+ const itemsProp = configItem.type === 'groups' ? 'groups' : 'items';
+ const configItemWithAsyncItems = {
+ ...(configItem || {}),
+ [itemsProp]: [
+ ...(configItem?.[itemsProp] || []),
+ ...(asyncItems?.[stringToId(configItem.label)] || []),
+ ],
+ };
return filterTypes[configItem.type]?.toDeselectValue(
- configItem,
+ configItemWithAsyncItems,
chip,
activeFilters,
);
diff --git a/src/hooks/useFilterConfig/helpers/helpers.js b/src/hooks/useFilterConfig/helpers/helpers.js
index d5dd222..2546b4f 100644
--- a/src/hooks/useFilterConfig/helpers/helpers.js
+++ b/src/hooks/useFilterConfig/helpers/helpers.js
@@ -16,10 +16,14 @@ export const defaultOnChange = (handler, label) => ({
});
export const flattenConfigItems = (configItem) =>
- (configItem.items || configItem.groups).flatMap((parentItem) => [
- parentItem,
- ...parentItem.items.map((item) => ({ ...item, parent: parentItem })),
- ]);
+ // TODO This is a hack. Somewhere an `items` prop with an empty array is introduced to group filters
+ // it should be either one or the other, but a filter should never have to have both `items` and `groups`
+ [...(configItem.items || []), ...(configItem.groups || [])].flatMap(
+ (parentItem) => [
+ parentItem,
+ ...parentItem.items.map((item) => ({ ...item, parent: parentItem })),
+ ],
+ );
export const configItemItemByLabel = (configItem, label) =>
configItem.items.find(({ label: itemLabel }) => itemLabel === label);
diff --git a/src/hooks/useFilterConfig/hooks/useAsyncFilterCache.js b/src/hooks/useFilterConfig/hooks/useAsyncFilterCache.js
new file mode 100644
index 0000000..66d1c31
--- /dev/null
+++ b/src/hooks/useFilterConfig/hooks/useAsyncFilterCache.js
@@ -0,0 +1,29 @@
+import { useRef, useCallback } from 'react';
+
+/**
+ * This hook is used to cache items that have been requested to show in the FilterModal
+ * This is needed in order to retain information to render the filters chip and be able to remove it.
+ *
+ * Ideally we would not need to do this and a filters value can stand on it's own.
+ *
+ */
+const useAsyncFilterCache = () => {
+ const asyncItems = useRef({});
+
+ const setAsyncFilterCache = useCallback((filter, itemsToCache) => {
+ const newItemsToCache = itemsToCache.filter(
+ ({ label }) =>
+ !(asyncItems.current?.[filter] || [])
+ .map(({ label }) => label)
+ .includes(label),
+ );
+
+ asyncItems.current[filter] = asyncItems.current[filter]
+ ? [...(asyncItems?.current[filter] || []), ...newItemsToCache]
+ : newItemsToCache;
+ }, []);
+
+ return [asyncItems?.current, setAsyncFilterCache];
+};
+
+export default useAsyncFilterCache;
diff --git a/src/hooks/useFilterConfig/hooks/useEventHandlers.js b/src/hooks/useFilterConfig/hooks/useEventHandlers.js
index ec764d6..ab66c73 100644
--- a/src/hooks/useFilterConfig/hooks/useEventHandlers.js
+++ b/src/hooks/useFilterConfig/hooks/useEventHandlers.js
@@ -10,6 +10,7 @@ const useEventHandlers = ({
onDeleteFilter,
resetOnClear,
filterTypes,
+ asyncItems,
selectionActions: { select, deselect, reset, clear },
}) => {
const onFilterUpdate = useCallback(
@@ -36,10 +37,16 @@ const useEventHandlers = ({
const onFilterDelete = useCallback(
async (_event, chips, clearAll = false) => {
if (clearAll) {
+ const filtersToClear = Object.keys(activeFilters);
+
if (resetOnClear) {
- reset();
+ for (const filter of filtersToClear) {
+ reset(filter);
+ }
} else {
- clear();
+ for (const filter of filtersToClear) {
+ clear(filter);
+ }
}
} else {
deselect(
@@ -48,6 +55,7 @@ const useEventHandlers = ({
filterTypes,
chips[0],
activeFilters,
+ asyncItems,
),
);
}
@@ -62,6 +70,7 @@ const useEventHandlers = ({
deselect,
resetOnClear,
filterTypes,
+ asyncItems,
],
);
diff --git a/src/hooks/useFilterConfig/hooks/useFilterModal.js b/src/hooks/useFilterConfig/hooks/useFilterModal.js
index 44e5892..6945f3b 100644
--- a/src/hooks/useFilterConfig/hooks/useFilterModal.js
+++ b/src/hooks/useFilterConfig/hooks/useFilterModal.js
@@ -1,10 +1,16 @@
import { useState, useCallback } from 'react';
import { stringToId } from '../helpers';
-const useFilterModal = ({ filterConfig, activeFilters, onFilterUpdate }) => {
+const useFilterModal = ({
+ serialisers,
+ filterConfig,
+ activeFilters,
+ onFilterUpdate,
+ setAsyncItems,
+}) => {
const [modalFilter, setModalFilter] = useState();
const isFilterModalOpen = !!modalFilter;
- const filter = filterConfig.find(
+ const filter = filterConfig?.find(
({ label }) => stringToId(label) === modalFilter,
);
@@ -27,6 +33,11 @@ const useFilterModal = ({ filterConfig, activeFilters, onFilterUpdate }) => {
onChange: (values) =>
onFilterUpdate(stringToId(filter.label), undefined, values),
onClose: closeFilterModal,
+ tableOptions: {
+ serialisers,
+ ...((filter?.modal || {}).tableOptions || {}),
+ },
+ setAsyncItems,
},
};
};
diff --git a/src/hooks/useFilterConfig/hooks/useResolvedProps.js b/src/hooks/useFilterConfig/hooks/useResolvedProps.js
index 18ba1e8..043bd7d 100644
--- a/src/hooks/useFilterConfig/hooks/useResolvedProps.js
+++ b/src/hooks/useFilterConfig/hooks/useResolvedProps.js
@@ -1,4 +1,4 @@
-import { useState } from 'react';
+import { useRef, useState } from 'react';
import { useDeepCompareEffect } from 'use-deep-compare';
const resolveObjectsProps = async (objects, propsToResolve) => {
@@ -8,9 +8,16 @@ const resolveObjectsProps = async (objects, propsToResolve) => {
let newObject = { ...object };
for (const prop of propsToResolve) {
- // TODO Allow to pass function arguments when resolving props
if (typeof object[prop] === 'function') {
- newObject[prop] = await object[prop]();
+ const resolvedProp = await object[prop]();
+ if (
+ Array.isArray(resolvedProp[0]) &&
+ typeof resolvedProp[1] === 'number'
+ ) {
+ newObject[prop] = resolvedProp[0];
+ } else {
+ newObject[prop] = resolvedProp;
+ }
}
}
@@ -22,20 +29,24 @@ const resolveObjectsProps = async (objects, propsToResolve) => {
// TODO this hook may be useful elsewhere as well, move it higher up and/or into som utils hook folder
const useResolvedProps = (objects, propsToResolve) => {
- const [resolvedObjects, setResolvedObjects] = useState([]);
+ const resolving = useRef(false);
+ const [resolvedObjects, setResolvedObjects] = useState();
useDeepCompareEffect(() => {
const resolveObjects = async (objects, propsToResolve) => {
+ resolving.current = true;
const newResolvedObjects = await resolveObjectsProps(
objects,
propsToResolve,
);
-
+ resolving.current = false;
setResolvedObjects(newResolvedObjects);
};
- resolveObjects(objects, propsToResolve);
- }, [objects, propsToResolve]);
+ if (!resolvedObjects && !resolving.current) {
+ resolveObjects(objects, propsToResolve);
+ }
+ }, [objects, propsToResolve, resolvedObjects]);
return resolvedObjects;
};
diff --git a/src/hooks/useFilterConfig/useFilterConfig.js b/src/hooks/useFilterConfig/useFilterConfig.js
index 9e14710..4eca7f2 100644
--- a/src/hooks/useFilterConfig/useFilterConfig.js
+++ b/src/hooks/useFilterConfig/useFilterConfig.js
@@ -10,6 +10,8 @@ import { toFilterChips } from './helpers/filterChipHelpers';
import useEventHandlers from './hooks/useEventHandlers';
import useFilterOptions from './hooks/useFilterOptions';
import useFilterModal from './hooks/useFilterModal';
+import useAsyncFilterCache from './hooks/useAsyncFilterCache';
+
import { TABLE_STATE_NAMESPACE } from './constants';
/**
@@ -42,6 +44,7 @@ const useFilterConfig = (options) => {
enableFilters,
filterTypes,
} = useFilterOptions(options);
+ const [asyncItems, setAsyncItems] = useAsyncFilterCache();
const { selection: activeFilters, ...selectionActions } = useSelectionManager(
initialActiveFilters,
@@ -53,20 +56,29 @@ const useFilterConfig = (options) => {
activeFilters,
selectionActions,
filterTypes,
+ asyncItems,
});
const { isFilterModalOpen, openFilterModal, filterModalProps } =
- useFilterModal({ filterConfig, activeFilters, onFilterUpdate });
+ useFilterModal({
+ filterConfig: options.filters?.filterConfig,
+ activeFilters,
+ onFilterUpdate,
+ serialisers: options.serialisers,
+ setAsyncItems,
+ });
const builtFilterConfig = useMemo(
() =>
- toFilterConfig(
- filterConfig,
- filterTypes,
- activeFilters,
- onFilterUpdate,
- openFilterModal,
- ),
+ filterConfig
+ ? toFilterConfig(
+ filterConfig,
+ filterTypes,
+ activeFilters,
+ onFilterUpdate,
+ openFilterModal,
+ )
+ : [],
[filterConfig, activeFilters, onFilterUpdate, filterTypes, openFilterModal],
);
@@ -76,7 +88,7 @@ const useFilterConfig = (options) => {
serialisers?.filters
? {
serialiser: (state) =>
- serialisers.filters(state, filterConfig.map(toIdedFilters)),
+ serialisers.filters(state, filterConfig?.map(toIdedFilters)),
}
: {},
);
@@ -94,7 +106,12 @@ const useFilterConfig = (options) => {
toolbarProps: {
filterConfig: builtFilterConfig,
activeFiltersConfig: {
- filters: toFilterChips(filterConfig, filterTypes, activeFilters),
+ filters: toFilterChips(
+ filterConfig,
+ filterTypes,
+ activeFilters,
+ asyncItems,
+ ),
onDelete: onFilterDelete,
},
},
diff --git a/src/hooks/useItems/useItems.js b/src/hooks/useItems/useItems.js
index 7c57b66..b75a368 100644
--- a/src/hooks/useItems/useItems.js
+++ b/src/hooks/useItems/useItems.js
@@ -29,10 +29,11 @@ const useItems = (
externalTotal,
) => {
const tableState = useRawTableState();
- const { filter, sort, pagination } = tableState || {};
const serialisedTableState = useSerialisedTableState();
- const useInternalState = typeof externalItems === 'function';
+ const { filters, sort, pagination } =
+ serialisedTableState || tableState || {};
+ const useInternalState = typeof externalItems === 'function';
const queryFn = useCallback(async () => {
const [items, total] = await externalItems(
serialisedTableState,
@@ -50,9 +51,9 @@ const useItems = (
isFetching: internalLoading,
error: internalError,
} = useQuery({
- queryKey: ['items', serialisedTableState, filter, sort, pagination],
+ queryKey: ['items', filters, sort, pagination],
queryFn,
- enabled: useInternalState,
+ enabled: useInternalState && (!!tableState || !!serialisedTableState),
refetchOnWindowFocus: false,
});
diff --git a/src/support/api.js b/src/support/api.js
index 14abc48..b8bdb53 100644
--- a/src/support/api.js
+++ b/src/support/api.js
@@ -2,6 +2,8 @@ import { faker } from '@faker-js/faker/locale/de';
import { jsonquery } from '@jsonquerylang/jsonquery';
import itemsFactory, { genres } from '~/support/factories/items';
+import { yearsByDecade } from '~/support/factories/filters';
+
import { buildTree } from '~/support/factories/tableTree';
const DEFAULT_LIMIT = 10;
@@ -44,5 +46,6 @@ const queriedItems = (itemsToQuery) => {
export const apiHandler = queriedItems(items);
export const apiGenresHandler = queriedItems(genres);
+export const apiYearsByDecadeHandler = () => yearsByDecade;
export const apiTreehandler = async () => buildTree({ items });
export const apiSelectionHandler = () => selectedItemIds;
diff --git a/src/support/factories/filters.js b/src/support/factories/filters.js
index a906564..04ef9c6 100644
--- a/src/support/factories/filters.js
+++ b/src/support/factories/filters.js
@@ -2,6 +2,50 @@ import NumberFilter from '~/support/components/NumberFilter';
import { genres, items } from './items';
+export const yearsByDecade = [
+ {
+ label: '80s',
+ value: '80s',
+ groupSelectable: true,
+ items: [...new Array(10)].map((_, idx) => ({
+ label: `198${idx}`,
+ value: `198${idx}`,
+ })),
+ },
+ {
+ label: '90s',
+ value: '90s',
+ items: [...new Array(10)].map((_, idx) => ({
+ label: `199${idx}`,
+ value: `199${idx}`,
+ })),
+ },
+ {
+ label: '00s',
+ value: '00s',
+ items: [...new Array(10)].map((_, idx) => ({
+ label: `200${idx}`,
+ value: `200${idx}`,
+ })),
+ },
+ {
+ label: '10s',
+ value: '10s',
+ items: [...new Array(10)].map((_, idx) => ({
+ label: `201${idx}`,
+ value: `201${idx}`,
+ })),
+ },
+ {
+ label: '20s',
+ value: '20s',
+ items: [...new Array(5)].map((_, idx) => ({
+ label: `201${idx}`,
+ value: `201${idx}`,
+ })),
+ },
+];
+
export const title = {
type: 'text',
label: 'Title',
@@ -94,7 +138,7 @@ export const invalidFilter = {
filter: (items) => items,
};
-export const yearsByDecade = {
+export const yearsByDecadeFilter = {
type: 'group',
label: 'Years by decade',
filterSerialiser: (_filterConfigItem, value) => {
@@ -105,48 +149,7 @@ export const yearsByDecade = {
return `.releaseYear in [${allYears.join(', ')}]`;
},
- groups: [
- {
- label: '80s',
- value: '80s',
- items: [...new Array(10)].map((_, idx) => ({
- label: `198${idx}`,
- value: `198${idx}`,
- })),
- },
- {
- label: '90s',
- value: '90s',
- items: [...new Array(10)].map((_, idx) => ({
- label: `199${idx}`,
- value: `199${idx}`,
- })),
- },
- {
- label: '00s',
- value: '00s',
- items: [...new Array(10)].map((_, idx) => ({
- label: `200${idx}`,
- value: `200${idx}`,
- })),
- },
- {
- label: '10s',
- value: '10s',
- items: [...new Array(10)].map((_, idx) => ({
- label: `201${idx}`,
- value: `201${idx}`,
- })),
- },
- {
- label: '20s',
- value: '20s',
- items: [...new Array(5)].map((_, idx) => ({
- label: `201${idx}`,
- value: `201${idx}`,
- })),
- },
- ],
+ groups: yearsByDecade,
modal: {
title: 'Select years to filter by',
},
@@ -235,7 +238,7 @@ export default [
rating,
decade,
invalidFilter,
- yearsByDecade,
+ yearsByDecadeFilter,
genreWithModal,
genreWithFetchedItems,
genreWithFetchedItemsAndModalItems,
diff --git a/src/support/mswHandler.js b/src/support/mswHandler.js
index 54de8eb..c476c39 100644
--- a/src/support/mswHandler.js
+++ b/src/support/mswHandler.js
@@ -4,6 +4,7 @@ import {
apiHandler,
apiTreehandler,
apiGenresHandler,
+ apiYearsByDecadeHandler,
apiSelectionHandler,
} from './api';
@@ -25,6 +26,7 @@ export default [
http.get('/api', withAllParams(apiHandler)),
http.get('/api/tree', withAllParams(apiTreehandler)),
http.get('/api/genres', withAllParams(apiGenresHandler)),
+ http.get('/api/years-by-decade', withAllParams(apiYearsByDecadeHandler)),
http.get('/api/error', async () => {
await delay(DEFAULT_DELAY);
return HttpResponse.json(