Simplify list store and table implementation

This commit is contained in:
jeffvli 2023-07-19 14:22:24 -07:00
parent 9bcefb3105
commit 8b4a2d1ac0
2 changed files with 135 additions and 49 deletions

View file

@ -1,3 +1,4 @@
import { MutableRefObject, useCallback, useMemo } from 'react';
import {
BodyScrollEvent,
ColDef,
@ -11,51 +12,45 @@ import {
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
import { QueryKey, useQueryClient } from '@tanstack/react-query';
import debounce from 'lodash/debounce';
import { MutableRefObject, useCallback, useMemo } from 'react';
import { generatePath, useNavigate } from 'react-router';
import { api } from '/@/renderer/api';
import { QueryPagination, queryKeys } from '/@/renderer/api/query-keys';
import { BasePaginatedResponse, LibraryItem } from '/@/renderer/api/types';
import { getColumnDefs, VirtualTableProps } from '/@/renderer/components/virtual-table';
import { SetContextMenuItems, useHandleTableContextMenu } from '/@/renderer/features/context-menu';
import { AppRoute } from '/@/renderer/router/routes';
import { ListDeterministicArgs, ListItemProps, ListTableProps } from '/@/renderer/store';
import { ListDisplayType, ServerListItem, TablePagination } from '/@/renderer/types';
import { useListStoreActions } from '/@/renderer/store';
import { ListDisplayType, ServerListItem } from '/@/renderer/types';
import { useListStoreByKey } from '../../../store/list.store';
export type AgGridFetchFn<TResponse, TFilter> = (
args: { filter: TFilter; limit: number; startIndex: number },
signal?: AbortSignal,
) => Promise<TResponse>;
interface UseAgGridProps<TResponse, TFilter> {
interface UseAgGridProps<TFilter> {
contextMenu: SetContextMenuItems;
fetch: {
filter: TFilter;
fn: AgGridFetchFn<TResponse, TFilter>;
itemCount?: number;
queryKey: (id: string, query?: Record<any, any>) => QueryKey;
server: ServerListItem | null;
};
customFilters?: Partial<TFilter>;
itemCount?: number;
itemType: LibraryItem;
pageKey: string;
properties: ListItemProps<any>;
setTable: (args: { data: Partial<ListTableProps> } & ListDeterministicArgs) => void;
setTablePagination: (args: { data: Partial<TablePagination> } & ListDeterministicArgs) => void;
server: ServerListItem | null;
tableRef: MutableRefObject<AgGridReactType | null>;
}
export const useVirtualTable = <TResponse, TFilter>({
fetch,
export const useVirtualTable = <TFilter>({
server,
tableRef,
properties,
setTable,
setTablePagination,
pageKey,
itemType,
contextMenu,
itemCount,
}: UseAgGridProps<TResponse, TFilter>) => {
customFilters,
}: UseAgGridProps<TFilter>) => {
const queryClient = useQueryClient();
const navigate = useNavigate();
const { setTable, setTablePagination } = useListStoreActions();
const properties = useListStoreByKey({ filter: customFilters, key: pageKey });
const isPaginationEnabled = properties.display === ListDisplayType.TABLE_PAGINATED;
@ -77,6 +72,43 @@ export const useVirtualTable = <TResponse, TFilter>({
}
};
const queryKeyFn:
| ((serverId: string, query: Record<any, any>, pagination: QueryPagination) => QueryKey)
| null = useMemo(() => {
if (itemType === LibraryItem.ALBUM) {
return queryKeys.albums.list;
}
if (itemType === LibraryItem.ALBUM_ARTIST) {
return queryKeys.albumArtists.list;
}
if (itemType === LibraryItem.PLAYLIST) {
return queryKeys.playlists.list;
}
if (itemType === LibraryItem.SONG) {
return queryKeys.songs.list;
}
return null;
}, [itemType]);
const queryFn: ((args: any) => Promise<BasePaginatedResponse<any> | null | undefined>) | null =
useMemo(() => {
if (itemType === LibraryItem.ALBUM) {
return api.controller.getAlbumList;
}
if (itemType === LibraryItem.ALBUM_ARTIST) {
return api.controller.getAlbumArtistList;
}
if (itemType === LibraryItem.PLAYLIST) {
return api.controller.getPlaylistList;
}
if (itemType === LibraryItem.SONG) {
return api.controller.getSongList;
}
return null;
}, [itemType]);
const onGridReady = useCallback(
(params: GridReadyEvent) => {
const dataSource: IDatasource = {
@ -84,29 +116,32 @@ export const useVirtualTable = <TResponse, TFilter>({
const limit = params.endRow - params.startRow;
const startIndex = params.startRow;
const queryKey = fetch.queryKey(fetch.server?.id || '', {
limit,
startIndex,
...fetch.filter,
});
const results = (await queryClient.fetchQuery(
queryKey,
async ({ signal }) => {
const res = await fetch.fn(
{
filter: fetch.filter,
limit,
startIndex,
},
signal,
);
return res;
const queryKey = queryKeyFn!(
server?.id || '',
{
...(properties.filter as any),
},
{
limit,
startIndex,
},
);
{ cacheTime: 1000 * 60 * 1 },
)) as BasePaginatedResponse<any>;
const results = (await queryClient.fetchQuery(queryKey, async ({ signal }) => {
const res = await queryFn!({
apiClientProps: {
server,
signal,
},
query: {
...properties.filter,
limit,
startIndex,
},
});
return res;
})) as BasePaginatedResponse<any>;
params.successCallback(results?.items || [], results?.totalRecordCount || 0);
},
@ -116,7 +151,14 @@ export const useVirtualTable = <TResponse, TFilter>({
params.api.setDatasource(dataSource);
params.api.ensureIndexVisible(properties.table.scrollOffset || 0, 'top');
},
[fetch, properties.table.scrollOffset, queryClient],
[
properties.table.scrollOffset,
properties.filter,
queryKeyFn,
server,
queryClient,
queryFn,
],
);
const onPaginationChanged = useCallback(
@ -228,6 +270,13 @@ export const useVirtualTable = <TResponse, TFilter>({
case LibraryItem.ALBUM:
navigate(generatePath(AppRoute.LIBRARY_ALBUMS_DETAIL, { albumId: e.data.id }));
break;
case LibraryItem.ALBUM_ARTIST:
navigate(
generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL, {
albumArtistId: e.data.id,
}),
);
break;
case LibraryItem.ARTIST:
navigate(
generatePath(AppRoute.LIBRARY_ARTISTS_DETAIL, { artistId: e.data.id }),

View file

@ -65,11 +65,20 @@ export type ListDeterministicArgs = { key: ListKey };
export interface ListSlice extends ListState {
_actions: {
getFilter: (args: { id?: string; itemType: LibraryItem; key?: string }) => FilterType;
getFilter: (args: {
customFilters?: Record<any, any>;
id?: string;
itemType: LibraryItem;
key?: string;
}) => FilterType;
resetFilter: () => void;
setDisplayType: (args: { data: ListDisplayType } & ListDeterministicArgs) => void;
setFilter: (
args: { data: Partial<FilterType>; itemType: LibraryItem } & ListDeterministicArgs,
args: {
customFilters?: Record<any, any>;
data: Partial<FilterType>;
itemType: LibraryItem;
} & ListDeterministicArgs,
) => FilterType;
setGrid: (args: { data: Partial<ListGridProps> } & ListDeterministicArgs) => void;
setStore: (data: Partial<ListSlice>) => void;
@ -168,11 +177,14 @@ export const useListStore = create<ListSlice>()(
}
});
return get()._actions.getFilter({
id,
itemType: args.itemType,
key: args.key,
});
return {
...get()._actions.getFilter({
id,
itemType: args.itemType,
key: args.key,
}),
...args.customFilters,
};
},
setGrid: (args) => {
const [page, id] = args.key.split('_');
@ -494,6 +506,31 @@ export const useListStore = create<ListSlice>()(
export const useListStoreActions = () => useListStore((state) => state._actions);
export const useListStoreByKey = <TFilter>(args: { filter?: Partial<TFilter>; key: string }) => {
const key = args.key as keyof ListState['item'];
return useListStore(
(state) => ({
...state.item[key],
filter: {
...state.item[key].filter,
...args.filter,
},
}),
shallow,
);
};
export const useListFilterByKey = <TFilter>(args: { filter?: Partial<TFilter>; key: string }) => {
const key = args.key as keyof ListState['item'];
return useListStore(
(state) => ({
...state.item[key].filter,
...args.filter,
}),
shallow,
);
};
export const useAlbumListStore = (args?: { id?: string; key?: string }) =>
useListStore((state) => {
const detail = args?.key ? state.detail[args.key] : undefined;