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 { import {
BodyScrollEvent, BodyScrollEvent,
ColDef, ColDef,
@ -11,51 +12,45 @@ import {
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact'; import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
import { QueryKey, useQueryClient } from '@tanstack/react-query'; import { QueryKey, useQueryClient } from '@tanstack/react-query';
import debounce from 'lodash/debounce'; import debounce from 'lodash/debounce';
import { MutableRefObject, useCallback, useMemo } from 'react';
import { generatePath, useNavigate } from 'react-router'; 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 { BasePaginatedResponse, LibraryItem } from '/@/renderer/api/types';
import { getColumnDefs, VirtualTableProps } from '/@/renderer/components/virtual-table'; import { getColumnDefs, VirtualTableProps } from '/@/renderer/components/virtual-table';
import { SetContextMenuItems, useHandleTableContextMenu } from '/@/renderer/features/context-menu'; import { SetContextMenuItems, useHandleTableContextMenu } from '/@/renderer/features/context-menu';
import { AppRoute } from '/@/renderer/router/routes'; import { AppRoute } from '/@/renderer/router/routes';
import { ListDeterministicArgs, ListItemProps, ListTableProps } from '/@/renderer/store'; import { useListStoreActions } from '/@/renderer/store';
import { ListDisplayType, ServerListItem, TablePagination } from '/@/renderer/types'; import { ListDisplayType, ServerListItem } from '/@/renderer/types';
import { useListStoreByKey } from '../../../store/list.store';
export type AgGridFetchFn<TResponse, TFilter> = ( export type AgGridFetchFn<TResponse, TFilter> = (
args: { filter: TFilter; limit: number; startIndex: number }, args: { filter: TFilter; limit: number; startIndex: number },
signal?: AbortSignal, signal?: AbortSignal,
) => Promise<TResponse>; ) => Promise<TResponse>;
interface UseAgGridProps<TResponse, TFilter> { interface UseAgGridProps<TFilter> {
contextMenu: SetContextMenuItems; contextMenu: SetContextMenuItems;
fetch: { customFilters?: Partial<TFilter>;
filter: TFilter;
fn: AgGridFetchFn<TResponse, TFilter>;
itemCount?: number;
queryKey: (id: string, query?: Record<any, any>) => QueryKey;
server: ServerListItem | null;
};
itemCount?: number; itemCount?: number;
itemType: LibraryItem; itemType: LibraryItem;
pageKey: string; pageKey: string;
properties: ListItemProps<any>; server: ServerListItem | null;
setTable: (args: { data: Partial<ListTableProps> } & ListDeterministicArgs) => void;
setTablePagination: (args: { data: Partial<TablePagination> } & ListDeterministicArgs) => void;
tableRef: MutableRefObject<AgGridReactType | null>; tableRef: MutableRefObject<AgGridReactType | null>;
} }
export const useVirtualTable = <TResponse, TFilter>({ export const useVirtualTable = <TFilter>({
fetch, server,
tableRef, tableRef,
properties,
setTable,
setTablePagination,
pageKey, pageKey,
itemType, itemType,
contextMenu, contextMenu,
itemCount, itemCount,
}: UseAgGridProps<TResponse, TFilter>) => { customFilters,
}: UseAgGridProps<TFilter>) => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const navigate = useNavigate(); const navigate = useNavigate();
const { setTable, setTablePagination } = useListStoreActions();
const properties = useListStoreByKey({ filter: customFilters, key: pageKey });
const isPaginationEnabled = properties.display === ListDisplayType.TABLE_PAGINATED; 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( const onGridReady = useCallback(
(params: GridReadyEvent) => { (params: GridReadyEvent) => {
const dataSource: IDatasource = { const dataSource: IDatasource = {
@ -84,29 +116,32 @@ export const useVirtualTable = <TResponse, TFilter>({
const limit = params.endRow - params.startRow; const limit = params.endRow - params.startRow;
const startIndex = params.startRow; const startIndex = params.startRow;
const queryKey = fetch.queryKey(fetch.server?.id || '', { const queryKey = queryKeyFn!(
limit, server?.id || '',
startIndex, {
...fetch.filter, ...(properties.filter as any),
});
const results = (await queryClient.fetchQuery(
queryKey,
async ({ signal }) => {
const res = await fetch.fn(
{
filter: fetch.filter,
limit,
startIndex,
},
signal,
);
return res;
}, },
{
limit,
startIndex,
},
);
{ cacheTime: 1000 * 60 * 1 }, const results = (await queryClient.fetchQuery(queryKey, async ({ signal }) => {
)) as BasePaginatedResponse<any>; const res = await queryFn!({
apiClientProps: {
server,
signal,
},
query: {
...properties.filter,
limit,
startIndex,
},
});
return res;
})) as BasePaginatedResponse<any>;
params.successCallback(results?.items || [], results?.totalRecordCount || 0); params.successCallback(results?.items || [], results?.totalRecordCount || 0);
}, },
@ -116,7 +151,14 @@ export const useVirtualTable = <TResponse, TFilter>({
params.api.setDatasource(dataSource); params.api.setDatasource(dataSource);
params.api.ensureIndexVisible(properties.table.scrollOffset || 0, 'top'); 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( const onPaginationChanged = useCallback(
@ -228,6 +270,13 @@ export const useVirtualTable = <TResponse, TFilter>({
case LibraryItem.ALBUM: case LibraryItem.ALBUM:
navigate(generatePath(AppRoute.LIBRARY_ALBUMS_DETAIL, { albumId: e.data.id })); navigate(generatePath(AppRoute.LIBRARY_ALBUMS_DETAIL, { albumId: e.data.id }));
break; break;
case LibraryItem.ALBUM_ARTIST:
navigate(
generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL, {
albumArtistId: e.data.id,
}),
);
break;
case LibraryItem.ARTIST: case LibraryItem.ARTIST:
navigate( navigate(
generatePath(AppRoute.LIBRARY_ARTISTS_DETAIL, { artistId: e.data.id }), 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 { export interface ListSlice extends ListState {
_actions: { _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; resetFilter: () => void;
setDisplayType: (args: { data: ListDisplayType } & ListDeterministicArgs) => void; setDisplayType: (args: { data: ListDisplayType } & ListDeterministicArgs) => void;
setFilter: ( setFilter: (
args: { data: Partial<FilterType>; itemType: LibraryItem } & ListDeterministicArgs, args: {
customFilters?: Record<any, any>;
data: Partial<FilterType>;
itemType: LibraryItem;
} & ListDeterministicArgs,
) => FilterType; ) => FilterType;
setGrid: (args: { data: Partial<ListGridProps> } & ListDeterministicArgs) => void; setGrid: (args: { data: Partial<ListGridProps> } & ListDeterministicArgs) => void;
setStore: (data: Partial<ListSlice>) => void; setStore: (data: Partial<ListSlice>) => void;
@ -168,11 +177,14 @@ export const useListStore = create<ListSlice>()(
} }
}); });
return get()._actions.getFilter({ return {
id, ...get()._actions.getFilter({
itemType: args.itemType, id,
key: args.key, itemType: args.itemType,
}); key: args.key,
}),
...args.customFilters,
};
}, },
setGrid: (args) => { setGrid: (args) => {
const [page, id] = args.key.split('_'); const [page, id] = args.key.split('_');
@ -494,6 +506,31 @@ export const useListStore = create<ListSlice>()(
export const useListStoreActions = () => useListStore((state) => state._actions); 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 }) => export const useAlbumListStore = (args?: { id?: string; key?: string }) =>
useListStore((state) => { useListStore((state) => {
const detail = args?.key ? state.detail[args.key] : undefined; const detail = args?.key ? state.detail[args.key] : undefined;