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(