diff --git a/packages/core/src/actions/config.ts b/packages/core/src/actions/config.ts index 66ebc3935..39e48a9d9 100644 --- a/packages/core/src/actions/config.ts +++ b/packages/core/src/actions/config.ts @@ -145,6 +145,8 @@ function applyFolderCollectionDefaults( ): FolderCollectionWithDefaults { const collection: FolderCollectionWithDefaults = { ...originalCollection, + view_filters: undefined, + view_groups: undefined, i18n: collectionI18n, }; @@ -228,6 +230,8 @@ function applyFilesCollectionDefaults( const collection: FilesCollectionWithDefaults = { ...originalCollection, i18n: collectionI18n, + view_filters: undefined, + view_groups: undefined, files: originalCollection.files.map(f => applyCollectionFileDefaults(f, originalCollection, collectionI18n, config), ), @@ -271,7 +275,7 @@ function applyCollectionDefaults( collection.fields = setI18nDefaultsForFields(collection.fields, Boolean(collectionI18n)); } - const { view_filters, view_groups } = collection; + const { view_filters, view_groups } = originalCollection; if (!collection.sortable_fields) { collection.sortable_fields = { @@ -280,7 +284,7 @@ function applyCollectionDefaults( } collection.view_filters = { - default: collection.view_filters?.default, + default: originalCollection.view_filters?.default, filters: (view_filters?.filters ?? []).map(filter => { return { ...filter, @@ -290,7 +294,7 @@ function applyCollectionDefaults( }; collection.view_groups = { - default: collection.view_groups?.default, + default: originalCollection.view_groups?.default, groups: (view_groups?.groups ?? []).map(group => { return { ...group, diff --git a/packages/core/src/actions/entries.ts b/packages/core/src/actions/entries.ts index 698ff2a40..86da780a3 100644 --- a/packages/core/src/actions/entries.ts +++ b/packages/core/src/actions/entries.ts @@ -44,11 +44,7 @@ import { Cursor } from '../lib/util'; import { getFields, updateFieldByKey } from '../lib/util/collection.util'; import { createEmptyDraftData, createEmptyDraftI18nData } from '../lib/util/entry.util'; import { selectCollectionEntriesCursor } from '../reducers/selectors/cursors'; -import { - selectEntriesSelectedSort, - selectIsFetching, - selectPublishedSlugs, -} from '../reducers/selectors/entries'; +import { selectIsFetching, selectPublishedSlugs } from '../reducers/selectors/entries'; import { addSnackbar } from '../store/slices/snackbars'; import { createAssetProxy } from '../valueObjects/AssetProxy'; import createEntry from '../valueObjects/createEntry'; @@ -73,7 +69,9 @@ import type { SortDirection, ValueOrNestedValue, ViewFilter, + ViewFilterWithDefaults, ViewGroup, + ViewGroupWithDefaults, } from '../interface'; import type { RootState } from '../store'; import type AssetProxy from '../valueObjects/AssetProxy'; @@ -122,7 +120,10 @@ export function entriesLoading(collection: CollectionWithDefaults) { } as const; } -export function filterEntriesRequest(collection: CollectionWithDefaults, filter: ViewFilter) { +export function filterEntriesRequest( + collection: CollectionWithDefaults, + filter: ViewFilterWithDefaults, +) { return { type: FILTER_ENTRIES_REQUEST, payload: { @@ -162,7 +163,10 @@ export function filterEntriesFailure( } as const; } -export function groupEntriesRequest(collection: CollectionWithDefaults, group: ViewGroup) { +export function groupEntriesRequest( + collection: CollectionWithDefaults, + group: ViewGroupWithDefaults, +) { return { type: GROUP_ENTRIES_REQUEST, payload: { @@ -315,7 +319,7 @@ export function sortByField( }; } -export function filterByField(collection: CollectionWithDefaults, filter: ViewFilter) { +export function filterByField(collection: CollectionWithDefaults, filter: ViewFilterWithDefaults) { return async (dispatch: ThunkDispatch, getState: () => RootState) => { const state = getState(); // if we're already fetching we update the filter key, but skip loading entries @@ -334,17 +338,12 @@ export function filterByField(collection: CollectionWithDefaults, filter: ViewFi }; } -export function groupByField(collection: CollectionWithDefaults, group: ViewGroup) { +export function groupByField(collection: CollectionWithDefaults, group: ViewGroupWithDefaults) { return async (dispatch: ThunkDispatch, getState: () => RootState) => { const state = getState(); const isFetching = selectIsFetching(state, collection.name); - dispatch({ - type: GROUP_ENTRIES_REQUEST, - payload: { - collection: collection.name, - group, - }, - }); + dispatch(groupEntriesRequest(collection, group)); + if (isFetching) { return; } @@ -699,10 +698,6 @@ export function loadEntries(collection: CollectionWithDefaults, page = 0) { return; } const state = getState(); - const sortField = selectEntriesSelectedSort(state, collection.name); - if (sortField) { - return dispatch(sortByField(collection, sortField.key, sortField.direction)); - } const configState = state.config; if (!configState.config) { @@ -751,6 +746,7 @@ export function loadEntries(collection: CollectionWithDefaults, page = 0) { ); } catch (error: unknown) { console.error(error); + if (error instanceof Error) { dispatch( addSnackbar({ diff --git a/packages/core/src/components/collections/CollectionControls.tsx b/packages/core/src/components/collections/CollectionControls.tsx index bfc47568d..9437d123e 100644 --- a/packages/core/src/components/collections/CollectionControls.tsx +++ b/packages/core/src/components/collections/CollectionControls.tsx @@ -7,16 +7,16 @@ import GroupControl from './GroupControl'; import MobileCollectionControls from './mobile/MobileCollectionControls'; import SortControl from './SortControl'; -import type { ViewStyle } from '@staticcms/core/constants/views'; import type { FilterMap, GroupMap, SortableField, SortDirection, SortMap, - ViewFilter, - ViewGroup, + ViewFilterWithDefaults, + ViewGroupWithDefaults, } from '@staticcms/core'; +import type { ViewStyle } from '@staticcms/core/constants/views'; import type { FC } from 'react'; interface CollectionControlsProps { @@ -26,11 +26,11 @@ interface CollectionControlsProps { onSortClick?: (key: string, direction?: SortDirection) => Promise; sort?: SortMap | undefined; filter?: Record; - viewFilters?: ViewFilter[]; - onFilterClick?: (filter: ViewFilter) => void; + viewFilters?: ViewFilterWithDefaults[]; + onFilterClick?: (filter: ViewFilterWithDefaults) => void; group?: Record; - viewGroups?: ViewGroup[]; - onGroupClick?: (filter: ViewGroup) => void; + viewGroups?: ViewGroupWithDefaults[]; + onGroupClick?: (filter: ViewGroupWithDefaults) => void; } const CollectionControls: FC = ({ diff --git a/packages/core/src/components/collections/CollectionView.tsx b/packages/core/src/components/collections/CollectionView.tsx index ab9e97aa4..06bc72aa5 100644 --- a/packages/core/src/components/collections/CollectionView.tsx +++ b/packages/core/src/components/collections/CollectionView.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import React, { useCallback, useMemo } from 'react'; import { changeViewStyle, @@ -6,7 +6,6 @@ import { groupByField, sortByField, } from '@staticcms/core/actions/entries'; -import { SORT_DIRECTION_ASCENDING } from '@staticcms/core/constants'; import useTranslate from '@staticcms/core/lib/hooks/useTranslate'; import { getSortableFields, @@ -28,8 +27,13 @@ import CollectionHeader from './CollectionHeader'; import EntriesCollection from './entries/EntriesCollection'; import EntriesSearch from './entries/EntriesSearch'; +import type { + CollectionWithDefaults, + SortDirection, + ViewFilterWithDefaults, + ViewGroupWithDefaults, +} from '@staticcms/core'; import type { ViewStyle } from '@staticcms/core/constants/views'; -import type { CollectionWithDefaults, SortDirection, ViewFilter, ViewGroup } from '@staticcms/core'; import type { FC } from 'react'; import './Collection.css'; @@ -70,13 +74,6 @@ const CollectionView: FC = ({ const filter = useAppSelector(state => selectEntriesFilter(state, collection?.name)); const group = useAppSelector(state => selectEntriesGroup(state, collection?.name)); - const [readyToLoad, setReadyToLoad] = useState(false); - const [prevCollection, setPrevCollection] = useState(); - - useEffect(() => { - setPrevCollection(collection); - }, [collection]); - const searchResultKey = useMemo( () => `collection.collectionTop.searchResults${isSingleSearchResult ? 'InCollection' : ''}`, [isSingleSearchResult], @@ -110,12 +107,7 @@ const CollectionView: FC = ({ } return ( - + ); }, [ collection, @@ -123,8 +115,6 @@ const CollectionView: FC = ({ filterTerm, isSearchResults, isSingleSearchResult, - prevCollection, - readyToLoad, searchTerm, viewStyle, ]); @@ -137,14 +127,14 @@ const CollectionView: FC = ({ ); const onFilterClick = useCallback( - async (filter: ViewFilter) => { + async (filter: ViewFilterWithDefaults) => { collection && (await dispatch(filterByField(collection, filter))); }, [collection, dispatch], ); const onGroupClick = useCallback( - async (group: ViewGroup) => { + async (group: ViewGroupWithDefaults) => { collection && (await dispatch(groupByField(collection, group))); }, [collection, dispatch], @@ -157,80 +147,6 @@ const CollectionView: FC = ({ [dispatch], ); - useEffect(() => { - if (prevCollection === collection) { - if (!readyToLoad) { - setReadyToLoad(true); - } - return; - } - - if (sort?.[0]?.key) { - if (!readyToLoad) { - setReadyToLoad(true); - } - return; - } - - const defaultSort = collection?.sortable_fields?.default; - const defaultViewGroupName = collection?.view_groups?.default; - const defaultViewFilterName = collection?.view_filters?.default; - if (!defaultViewGroupName && !defaultViewFilterName && (!defaultSort || !defaultSort.field)) { - if (!readyToLoad) { - setReadyToLoad(true); - } - return; - } - - setReadyToLoad(false); - - let alive = true; - - const sortGroupFilterEntries = () => { - setTimeout(async () => { - if (defaultSort && defaultSort.field) { - await onSortClick(defaultSort.field, defaultSort.direction ?? SORT_DIRECTION_ASCENDING); - } - - if (defaultViewGroupName) { - const defaultViewGroup = viewGroups?.groups.find(g => g.name === defaultViewGroupName); - if (defaultViewGroup) { - await onGroupClick(defaultViewGroup); - } - } - - if (defaultViewFilterName) { - const defaultViewFilter = viewFilters?.filters.find( - f => f.name === defaultViewFilterName, - ); - if (defaultViewFilter) { - await onFilterClick(defaultViewFilter); - } - } - - if (alive) { - setReadyToLoad(true); - } - }); - }; - - sortGroupFilterEntries(); - - return () => { - alive = false; - }; - }, [ - collection, - onFilterClick, - onGroupClick, - onSortClick, - prevCollection, - readyToLoad, - sort, - viewFilters?.filters, - viewGroups?.groups, - ]); - const collectionDescription = collection?.description; return ( diff --git a/packages/core/src/components/collections/FilterControl.tsx b/packages/core/src/components/collections/FilterControl.tsx index 2aea73fb5..06302e61d 100644 --- a/packages/core/src/components/collections/FilterControl.tsx +++ b/packages/core/src/components/collections/FilterControl.tsx @@ -7,8 +7,8 @@ import Menu from '../common/menu/Menu'; import MenuGroup from '../common/menu/MenuGroup'; import MenuItemButton from '../common/menu/MenuItemButton'; -import type { FilterMap, ViewFilter } from '@staticcms/core'; -import type { MouseEvent, FC } from 'react'; +import type { FilterMap, ViewFilterWithDefaults } from '@staticcms/core'; +import type { FC, MouseEvent } from 'react'; import './FilterControl.css'; @@ -24,9 +24,9 @@ export const classes = generateClassNames('FilterControl', [ export interface FilterControlProps { filter: Record | undefined; - viewFilters: ViewFilter[] | undefined; + viewFilters: ViewFilterWithDefaults[] | undefined; variant?: 'menu' | 'list'; - onFilterClick: ((viewFilter: ViewFilter) => void) | undefined; + onFilterClick: ((viewFilter: ViewFilterWithDefaults) => void) | undefined; } const FilterControl: FC = ({ @@ -40,7 +40,7 @@ const FilterControl: FC = ({ const anyActive = useMemo(() => Object.keys(filter).some(key => filter[key]?.active), [filter]); const handleFilterClick = useCallback( - (viewFilter: ViewFilter) => (event: MouseEvent) => { + (viewFilter: ViewFilterWithDefaults) => (event: MouseEvent) => { event.stopPropagation(); event.preventDefault(); onFilterClick?.(viewFilter); diff --git a/packages/core/src/components/collections/GroupControl.tsx b/packages/core/src/components/collections/GroupControl.tsx index fb5040cb2..deacaeeae 100644 --- a/packages/core/src/components/collections/GroupControl.tsx +++ b/packages/core/src/components/collections/GroupControl.tsx @@ -7,7 +7,7 @@ import Menu from '../common/menu/Menu'; import MenuGroup from '../common/menu/MenuGroup'; import MenuItemButton from '../common/menu/MenuItemButton'; -import type { GroupMap, ViewGroup } from '@staticcms/core'; +import type { GroupMap, ViewGroupWithDefaults } from '@staticcms/core'; import type { FC, MouseEvent } from 'react'; import './GroupControl.css'; @@ -25,9 +25,9 @@ export const classes = generateClassNames('GroupControl', [ export interface GroupControlProps { group: Record | undefined; - viewGroups: ViewGroup[] | undefined; + viewGroups: ViewGroupWithDefaults[] | undefined; variant?: 'menu' | 'list'; - onGroupClick: ((viewGroup: ViewGroup) => void) | undefined; + onGroupClick: ((viewGroup: ViewGroupWithDefaults) => void) | undefined; } const GroupControl: FC = ({ @@ -41,7 +41,7 @@ const GroupControl: FC = ({ const activeGroup = useMemo(() => Object.values(group).find(f => f.active === true), [group]); const handleGroupClick = useCallback( - (viewGroup: ViewGroup) => (event: MouseEvent) => { + (viewGroup: ViewGroupWithDefaults) => (event: MouseEvent) => { event.stopPropagation(); event.preventDefault(); onGroupClick?.(viewGroup); diff --git a/packages/core/src/components/collections/entries/EntriesCollection.tsx b/packages/core/src/components/collections/entries/EntriesCollection.tsx index 2059cafb1..f5b980fa5 100644 --- a/packages/core/src/components/collections/entries/EntriesCollection.tsx +++ b/packages/core/src/components/collections/entries/EntriesCollection.tsx @@ -15,8 +15,8 @@ import Button from '../../common/button/Button'; import Entries from './Entries'; import entriesClasses from './Entries.classes'; -import type { ViewStyle } from '@staticcms/core/constants/views'; import type { CollectionWithDefaults, Entry, GroupOfEntries } from '@staticcms/core'; +import type { ViewStyle } from '@staticcms/core/constants/views'; import type { RootState } from '@staticcms/core/store'; import type { FC } from 'react'; import type { t } from 'react-polyglot'; @@ -65,13 +65,11 @@ const EntriesCollection: FC = ({ cursor, page, entriesLoaded, - readyToLoad, }) => { const t = useTranslate(); const dispatch = useAppDispatch(); - const [prevReadyToLoad, setPrevReadyToLoad] = useState(false); const [prevCollection, setPrevCollection] = useState(collection); const groups = useGroups(collection.name); @@ -89,26 +87,12 @@ const EntriesCollection: FC = ({ }, [collection, entries, filterTerm]); useEffect(() => { - if ( - collection && - !entriesLoaded && - readyToLoad && - (!prevReadyToLoad || prevCollection !== collection) - ) { + if (collection && !entriesLoaded && prevCollection !== collection) { dispatch(loadEntries(collection)); } - setPrevReadyToLoad(readyToLoad); setPrevCollection(collection); - }, [ - collection, - dispatch, - entriesLoaded, - prevCollection, - prevReadyToLoad, - readyToLoad, - useWorkflow, - ]); + }, [collection, dispatch, entriesLoaded, prevCollection, useWorkflow]); const handleCursorActions = useCallback( (action: string) => { @@ -182,7 +166,6 @@ const EntriesCollection: FC = ({ interface EntriesCollectionOwnProps { collection: CollectionWithDefaults; viewStyle: ViewStyle; - readyToLoad: boolean; filterTerm: string; } diff --git a/packages/core/src/interface.ts b/packages/core/src/interface.ts index 7ff8744d6..5479331c0 100644 --- a/packages/core/src/interface.ts +++ b/packages/core/src/interface.ts @@ -89,9 +89,9 @@ export type SortMap = Record; export type Sort = Record; -export type FilterMap = ViewFilter & { active?: boolean }; +export type FilterMap = ViewFilterWithDefaults & { active?: boolean }; -export type GroupMap = ViewGroup & { active?: boolean }; +export type GroupMap = ViewGroupWithDefaults & { active?: boolean }; export type Filter = Record>; // collection.field.active @@ -305,6 +305,8 @@ export interface BaseCollection { export interface BaseCollectionWithDefaults extends Omit { i18n?: I18nInfo; + view_filters?: ViewFiltersWithDefaults; + view_groups?: ViewGroupsWithDefaults; } export interface FilesCollection extends BaseCollection { @@ -958,11 +960,19 @@ export interface ViewFilter { pattern: string | boolean | number; } +export interface ViewFilterWithDefaults extends ViewFilter { + id: string; +} + export interface ViewFilters { default?: string; filters: ViewFilter[]; } +export interface ViewFiltersWithDefaults extends ViewFilters { + filters: ViewFilterWithDefaults[]; +} + export interface ViewGroup { id?: string; name: string; @@ -971,11 +981,19 @@ export interface ViewGroup { pattern?: string; } +export interface ViewGroupWithDefaults extends ViewGroup { + id: string; +} + export interface ViewGroups { default?: string; groups: ViewGroup[]; } +export interface ViewGroupsWithDefaults extends ViewGroups { + groups: ViewGroupWithDefaults[]; +} + export type SortDirection = | typeof SORT_DIRECTION_ASCENDING | typeof SORT_DIRECTION_DESCENDING diff --git a/packages/core/src/reducers/entries.ts b/packages/core/src/reducers/entries.ts index 6bc05cb55..46f4f521c 100644 --- a/packages/core/src/reducers/entries.ts +++ b/packages/core/src/reducers/entries.ts @@ -3,6 +3,7 @@ import sortBy from 'lodash/sortBy'; import { CHANGE_VIEW_STYLE, + CONFIG_SUCCESS, ENTRIES_FAILURE, ENTRIES_REQUEST, ENTRIES_SUCCESS, @@ -18,6 +19,7 @@ import { GROUP_ENTRIES_REQUEST, GROUP_ENTRIES_SUCCESS, SEARCH_ENTRIES_SUCCESS, + SORT_DIRECTION_ASCENDING, SORT_ENTRIES_FAILURE, SORT_ENTRIES_REQUEST, SORT_ENTRIES_SUCCESS, @@ -25,6 +27,7 @@ import { import { VIEW_STYLES, VIEW_STYLE_TABLE } from '../constants/views'; import set from '../lib/util/set.util'; +import type { ConfigAction } from '../actions/config'; import type { EntriesAction } from '../actions/entries'; import type { SearchAction } from '../actions/search'; import type { ViewStyle } from '../constants/views'; @@ -124,9 +127,70 @@ export type EntriesState = { function entries( state: EntriesState = { entries: {}, pages: {}, sort: loadSort(), viewStyle: loadViewStyle() }, - action: EntriesAction | SearchAction, + action: EntriesAction | SearchAction | ConfigAction, ): EntriesState { switch (action.type) { + case CONFIG_SUCCESS: { + const config = action.payload.config; + + const sort: EntriesState['sort'] = {}; + const group: EntriesState['group'] = {}; + const filter: EntriesState['filter'] = {}; + + for (const collection of config.collections) { + if (collection.sortable_fields && collection.sortable_fields.default) { + const key = collection.sortable_fields.default.field; + sort[collection.name] = { + [key]: { + key, + direction: collection.sortable_fields.default.direction ?? SORT_DIRECTION_ASCENDING, + }, + } as SortMap; + } + + if (collection.view_filters && collection.view_filters.default) { + const defaultViewFilterName = collection.view_filters.default; + const defaultViewFilter = collection.view_filters.filters.find( + f => f.name === defaultViewFilterName, + ); + + const collectionFilters: Record = {}; + if (defaultViewFilter) { + collectionFilters[defaultViewFilter.id] = { + ...defaultViewFilter, + active: true, + }; + } + + filter[collection.name] = collectionFilters; + } + + if (collection.view_groups && collection.view_groups.default) { + const defaultViewGroupName = collection.view_groups.default; + const defaultViewGroup = collection.view_groups.groups.find( + g => g.name === defaultViewGroupName, + ); + + const collectionGroups: Record = {}; + if (defaultViewGroup) { + collectionGroups[defaultViewGroup.id] = { + ...defaultViewGroup, + active: true, + }; + } + + group[collection.name] = collectionGroups; + } + } + + return { + ...state, + sort, + group, + filter, + }; + } + case ENTRY_REQUEST: { const payload = action.payload; diff --git a/packages/core/src/reducers/selectors/entries.ts b/packages/core/src/reducers/selectors/entries.ts index 80756de08..8b5d8489b 100644 --- a/packages/core/src/reducers/selectors/entries.ts +++ b/packages/core/src/reducers/selectors/entries.ts @@ -4,7 +4,6 @@ import get from 'lodash/get'; import { SORT_DIRECTION_NONE } from '@staticcms/core/constants'; import { filterNullish } from '@staticcms/core/lib/util/null.util'; -import type { ViewStyle } from '@staticcms/core/constants/views'; import type { Entries, Entry, @@ -14,6 +13,7 @@ import type { SortMap, SortObject, } from '@staticcms/core'; +import type { ViewStyle } from '@staticcms/core/constants/views'; import type { RootState } from '@staticcms/core/store'; export const selectEntriesFilters = (entries: RootState) => { diff --git a/packages/core/test/data/collections.mock.ts b/packages/core/test/data/collections.mock.ts index 4e7e7cc70..ceff10f67 100644 --- a/packages/core/test/data/collections.mock.ts +++ b/packages/core/test/data/collections.mock.ts @@ -46,6 +46,24 @@ export const createMockFolderCollectionWithDefaults = ( ): FolderCollectionWithDefaults => ({ ...createMockFolderCollection(extra, ...fields), i18n: extra.i18n, + view_filters: extra.view_filters + ? { + ...extra.view_filters, + filters: extra.view_filters.filters.map(f => ({ + ...f, + id: `${f.field}__${f.pattern}`, + })), + } + : undefined, + view_groups: extra.view_groups + ? { + ...extra.view_groups, + groups: extra.view_groups.groups.map(g => ({ + ...g, + id: `${g.field}__${g.pattern}`, + })), + } + : undefined, }); export const createMockCollectionFile = ( @@ -102,4 +120,22 @@ export const createMockFilesCollectionWithDefaults = ( ...createMockFilesCollection(extra), i18n: extra.i18n, files: extra.files, + view_filters: extra.view_filters + ? { + ...extra.view_filters, + filters: extra.view_filters.filters.map(f => ({ + ...f, + id: `${f.field}__${f.pattern}`, + })), + } + : undefined, + view_groups: extra.view_groups + ? { + ...extra.view_groups, + groups: extra.view_groups.groups.map(g => ({ + ...g, + id: `${g.field}__${g.pattern}`, + })), + } + : undefined, });