Add album table view

This commit is contained in:
jeffvli 2022-12-28 01:44:49 -08:00
parent e5ad41b9da
commit b967c8cb19
5 changed files with 461 additions and 85 deletions

View file

@ -1,9 +1,10 @@
import { Flex, Slider } from '@mantine/core';
import { useQueryClient } from '@tanstack/react-query';
import debounce from 'lodash/debounce';
import throttle from 'lodash/throttle';
import type { ChangeEvent, MouseEvent, MutableRefObject } from 'react';
import { useCallback } from 'react';
import { IDatasource } from '@ag-grid-community/core';
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
import { Flex, Group, Stack } from '@mantine/core';
import { useQueryClient } from '@tanstack/react-query';
import debounce from 'lodash/debounce';
import {
RiArrowDownSLine,
RiFilter3Line,
@ -18,11 +19,16 @@ import { controller } from '/@/renderer/api/controller';
import { queryKeys } from '/@/renderer/api/query-keys';
import { AlbumListSort, ServerType, SortOrder } from '/@/renderer/api/types';
import {
ALBUM_TABLE_COLUMNS,
Button,
DropdownMenu,
MultiSelect,
PageHeader,
Popover,
SearchInput,
Slider,
Switch,
Text,
TextTitle,
VirtualInfiniteGridRef,
} from '/@/renderer/components';
@ -36,8 +42,10 @@ import {
useCurrentServer,
useSetAlbumFilters,
useSetAlbumStore,
useSetAlbumTable,
useSetAlbumTablePagination,
} from '/@/renderer/store';
import { ListDisplayType } from '/@/renderer/types';
import { ListDisplayType, TableColumn } from '/@/renderer/types';
const FILTERS = {
jellyfin: [
@ -82,9 +90,10 @@ const HeaderItems = styled.div`
interface AlbumListHeaderProps {
gridRef: MutableRefObject<VirtualInfiniteGridRef | null>;
tableRef: MutableRefObject<AgGridReactType | null>;
}
export const AlbumListHeader = ({ gridRef }: AlbumListHeaderProps) => {
export const AlbumListHeader = ({ gridRef, tableRef }: AlbumListHeaderProps) => {
const queryClient = useQueryClient();
const server = useCurrentServer();
const setPage = useSetAlbumStore();
@ -95,6 +104,9 @@ export const AlbumListHeader = ({ gridRef }: AlbumListHeaderProps) => {
const musicFoldersQuery = useMusicFolders();
const setPagination = useSetAlbumTablePagination();
const setTable = useSetAlbumTable();
const sortByLabel =
(server?.type &&
FILTERS[server.type as keyof typeof FILTERS].find((f) => f.value === filters.sortBy)?.name) ||
@ -102,13 +114,16 @@ export const AlbumListHeader = ({ gridRef }: AlbumListHeaderProps) => {
const sortOrderLabel = ORDER.find((o) => o.value === filters.sortOrder)?.name || 'Unknown';
const setSize = throttle(
(e: number) =>
setPage({
list: { ...page, grid: { ...page.grid, size: e } },
}),
200,
);
const handleItemSize = (e: number) => {
if (
page.display === ListDisplayType.TABLE ||
page.display === ListDisplayType.TABLE_PAGINATED
) {
setTable({ rowHeight: e });
} else {
setPage({ list: { ...page, grid: { ...page.grid, size: e } } });
}
};
const fetch = useCallback(
async (skip: number, take: number, filters: AlbumListFilter) => {
@ -137,18 +152,59 @@ export const AlbumListHeader = ({ gridRef }: AlbumListHeaderProps) => {
const handleFilterChange = useCallback(
async (filters: AlbumListFilter) => {
gridRef.current?.scrollTo(0);
gridRef.current?.resetLoadMoreItemsCache();
if (
page.display === ListDisplayType.TABLE ||
page.display === ListDisplayType.TABLE_PAGINATED
) {
const dataSource: IDatasource = {
getRows: async (params) => {
const limit = params.endRow - params.startRow;
const startIndex = params.startRow;
// Refetching within the virtualized grid may be inconsistent due to it refetching
// using an outdated set of filters. To avoid this, we fetch using the updated filters
// and then set the grid's data here.
const data = await fetch(0, 200, filters);
const queryKey = queryKeys.albums.list(server?.id || '', {
limit,
startIndex,
...filters,
});
if (!data?.items) return;
gridRef.current?.setItemData(data.items);
const albumsRes = await queryClient.fetchQuery(queryKey, async ({ signal }) =>
api.controller.getAlbumList({
query: {
limit,
startIndex,
...filters,
},
server,
signal,
}),
);
const albums = api.normalize.albumList(albumsRes, server);
params.successCallback(albums?.items || [], albumsRes?.totalRecordCount || undefined);
},
rowCount: undefined,
};
tableRef.current?.api.setDatasource(dataSource);
tableRef.current?.api.purgeInfiniteCache();
tableRef.current?.api.ensureIndexVisible(0, 'top');
if (page.display === ListDisplayType.TABLE_PAGINATED) {
setPagination({ currentPage: 0 });
}
} else {
gridRef.current?.scrollTo(0);
gridRef.current?.resetLoadMoreItemsCache();
// Refetching within the virtualized grid may be inconsistent due to it refetching
// using an outdated set of filters. To avoid this, we fetch using the updated filters
// and then set the grid's data here.
const data = await fetch(0, 200, filters);
if (!data?.items) return;
gridRef.current?.setItemData(data.items);
}
},
[gridRef, fetch],
[page.display, tableRef, setPagination, server, queryClient, gridRef, fetch],
);
const handleSetSortBy = useCallback(
@ -194,14 +250,7 @@ export const AlbumListHeader = ({ gridRef }: AlbumListHeaderProps) => {
const handleSetViewType = useCallback(
(e: MouseEvent<HTMLButtonElement>) => {
if (!e.currentTarget?.value) return;
const type = e.currentTarget.value;
if (type === ListDisplayType.CARD) {
setPage({ list: { ...page, display: ListDisplayType.CARD } });
} else if (type === ListDisplayType.POSTER) {
setPage({ list: { ...page, display: ListDisplayType.POSTER } });
} else {
setPage({ list: { ...page, display: ListDisplayType.TABLE } });
}
setPage({ list: { ...page, display: e.currentTarget.value as ListDisplayType } });
},
[page, setPage],
);
@ -213,6 +262,39 @@ export const AlbumListHeader = ({ gridRef }: AlbumListHeaderProps) => {
if (previousSearchTerm !== searchTerm) handleFilterChange(updatedFilters);
}, 500);
const handleTableColumns = (values: TableColumn[]) => {
const existingColumns = page.table.columns;
if (values.length === 0) {
return setTable({
columns: [],
});
}
// If adding a column
if (values.length > existingColumns.length) {
const newColumn = { column: values[values.length - 1], width: 100 };
setTable({ columns: [...existingColumns, newColumn] });
} else {
// If removing a column
const removed = existingColumns.filter((column) => !values.includes(column.column));
const newColumns = existingColumns.filter((column) => !removed.includes(column));
setTable({ columns: newColumns });
}
return tableRef.current?.api.sizeColumnsToFit();
};
const handleAutoFitColumns = (e: ChangeEvent<HTMLInputElement>) => {
setTable({ autoFit: e.currentTarget.checked });
if (e.currentTarget.checked) {
tableRef.current?.api.sizeColumnsToFit();
}
};
return (
<PageHeader>
<HeaderItems ref={cq.ref}>
@ -239,15 +321,6 @@ export const AlbumListHeader = ({ gridRef }: AlbumListHeaderProps) => {
</Button>
</DropdownMenu.Target>
<DropdownMenu.Dropdown>
<DropdownMenu.Label>Item size</DropdownMenu.Label>
<DropdownMenu.Item>
<Slider
defaultValue={page.grid.size || 0}
label={null}
onChange={setSize}
/>
</DropdownMenu.Item>
<DropdownMenu.Divider />
<DropdownMenu.Label>Display type</DropdownMenu.Label>
<DropdownMenu.Item
$isActive={page.display === ListDisplayType.CARD}
@ -264,20 +337,69 @@ export const AlbumListHeader = ({ gridRef }: AlbumListHeaderProps) => {
Poster
</DropdownMenu.Item>
<DropdownMenu.Item
disabled
$isActive={page.display === ListDisplayType.TABLE}
value="list"
value={ListDisplayType.TABLE}
onClick={handleSetViewType}
>
List
Table
</DropdownMenu.Item>
<DropdownMenu.Item
$isActive={page.display === ListDisplayType.TABLE_PAGINATED}
value={ListDisplayType.TABLE_PAGINATED}
onClick={handleSetViewType}
>
Table (paginated)
</DropdownMenu.Item>
<DropdownMenu.Divider />
<DropdownMenu.Label>Item size</DropdownMenu.Label>
<DropdownMenu.Item closeMenuOnClick={false}>
<Slider
defaultValue={
page.display === ListDisplayType.CARD || page.display === ListDisplayType.POSTER
? page.grid.size
: page.table.rowHeight
}
label={null}
max={100}
min={25}
onChangeEnd={handleItemSize}
/>
</DropdownMenu.Item>
{(page.display === ListDisplayType.TABLE ||
page.display === ListDisplayType.TABLE_PAGINATED) && (
<>
<DropdownMenu.Label>Table Columns</DropdownMenu.Label>
<DropdownMenu.Item
closeMenuOnClick={false}
component="div"
sx={{ cursor: 'default' }}
>
<Stack>
<MultiSelect
clearable
data={ALBUM_TABLE_COLUMNS}
defaultValue={page.table?.columns.map((column) => column.column)}
width={300}
onChange={handleTableColumns}
/>
<Group position="apart">
<Text>Auto Fit Columns</Text>
<Switch
defaultChecked={page.table.autoFit}
onChange={handleAutoFitColumns}
/>
</Group>
</Stack>
</DropdownMenu.Item>
</>
)}
</DropdownMenu.Dropdown>
</DropdownMenu>
<DropdownMenu position="bottom-start">
<DropdownMenu.Target>
<Button
compact
fw="normal"
fw="600"
variant="subtle"
>
{sortByLabel}
@ -298,8 +420,7 @@ export const AlbumListHeader = ({ gridRef }: AlbumListHeaderProps) => {
</DropdownMenu>
<Button
compact
fw="normal"
tooltip={!cq.isMd ? { label: sortOrderLabel } : undefined}
fw="600"
variant="subtle"
onClick={handleToggleSortOrder}
>
@ -320,8 +441,7 @@ export const AlbumListHeader = ({ gridRef }: AlbumListHeaderProps) => {
<DropdownMenu.Target>
<Button
compact
fw="normal"
tooltip={!cq.isMd ? { label: 'Folder' } : undefined}
fw="600"
variant="subtle"
>
{cq.isMd ? 'Folder' : <RiFolder2Line size={15} />}
@ -341,15 +461,11 @@ export const AlbumListHeader = ({ gridRef }: AlbumListHeaderProps) => {
</DropdownMenu.Dropdown>
</DropdownMenu>
)}
<Popover
closeOnClickOutside={false}
position="bottom-start"
>
<Popover position="bottom-start">
<Popover.Target>
<Button
compact
fw="normal"
tooltip={!cq.isMd ? { label: 'Filters' } : undefined}
fw="600"
variant="subtle"
>
{cq.isMd ? 'Filters' : <RiFilter3Line size={15} />}
@ -367,7 +483,6 @@ export const AlbumListHeader = ({ gridRef }: AlbumListHeaderProps) => {
<DropdownMenu.Target>
<Button
compact
tooltip={{ label: 'More' }}
variant="subtle"
>
<RiMoreFill size={15} />