mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 20:34:37 +03:00
743 lines
21 KiB
TypeScript
743 lines
21 KiB
TypeScript
import React, {
|
|
useCallback,
|
|
useContext,
|
|
useEffect,
|
|
useLayoutEffect,
|
|
useMemo,
|
|
useRef,
|
|
useState,
|
|
} from "react";
|
|
import clone from "lodash-es/clone";
|
|
import cloneDeep from "lodash-es/cloneDeep";
|
|
import isEqual from "lodash-es/isEqual";
|
|
import Mousetrap from "mousetrap";
|
|
import * as GQL from "src/core/generated-graphql";
|
|
import { QueryResult } from "@apollo/client";
|
|
import {
|
|
Criterion,
|
|
CriterionValue,
|
|
} from "src/models/list-filter/criteria/criterion";
|
|
import { ListFilterModel } from "src/models/list-filter/filter";
|
|
import { IconDefinition } from "@fortawesome/fontawesome-svg-core";
|
|
import { useInterfaceLocalForage } from "src/hooks/LocalForage";
|
|
import { useHistory, useLocation } from "react-router-dom";
|
|
import { ConfigurationContext } from "src/hooks/Config";
|
|
import { getFilterOptions } from "src/models/list-filter/factory";
|
|
import { useFindDefaultFilter } from "src/core/StashService";
|
|
import { Pagination, PaginationIndex } from "./Pagination";
|
|
import { EditFilterDialog } from "src/components/List/EditFilterDialog";
|
|
import { ListFilter } from "./ListFilter";
|
|
import { FilterTags } from "./FilterTags";
|
|
import { ListViewOptions } from "./ListViewOptions";
|
|
import { ListOperationButtons } from "./ListOperationButtons";
|
|
import { LoadingIndicator } from "../Shared/LoadingIndicator";
|
|
import { DisplayMode } from "src/models/list-filter/types";
|
|
import { ButtonToolbar } from "react-bootstrap";
|
|
|
|
export enum PersistanceLevel {
|
|
// do not load default query or persist display mode
|
|
NONE,
|
|
// load default query, don't load or persist display mode
|
|
ALL,
|
|
// load and persist display mode only
|
|
VIEW,
|
|
}
|
|
|
|
interface IDataItem {
|
|
id: string;
|
|
}
|
|
|
|
export interface IItemListOperation<T extends QueryResult> {
|
|
text: string;
|
|
onClick: (
|
|
result: T,
|
|
filter: ListFilterModel,
|
|
selectedIds: Set<string>
|
|
) => Promise<void>;
|
|
isDisplayed?: (
|
|
result: T,
|
|
filter: ListFilterModel,
|
|
selectedIds: Set<string>
|
|
) => boolean;
|
|
postRefetch?: boolean;
|
|
icon?: IconDefinition;
|
|
buttonVariant?: string;
|
|
}
|
|
|
|
interface IItemListOptions<T extends QueryResult, E extends IDataItem> {
|
|
filterMode: GQL.FilterMode;
|
|
useResult: (filter: ListFilterModel) => T;
|
|
getCount: (data: T) => number;
|
|
renderMetadataByline?: (data: T) => React.ReactNode;
|
|
getItems: (data: T) => E[];
|
|
}
|
|
|
|
interface IRenderListProps {
|
|
filter: ListFilterModel;
|
|
onChangePage: (page: number) => void;
|
|
updateFilter: (filter: ListFilterModel) => void;
|
|
}
|
|
|
|
interface IItemListProps<T extends QueryResult, E extends IDataItem> {
|
|
persistState?: PersistanceLevel;
|
|
persistanceKey?: string;
|
|
defaultSort?: string;
|
|
filterHook?: (filter: ListFilterModel) => ListFilterModel;
|
|
filterDialog?: (
|
|
criteria: Criterion<CriterionValue>[],
|
|
setCriteria: (v: Criterion<CriterionValue>[]) => void
|
|
) => React.ReactNode;
|
|
zoomable?: boolean;
|
|
selectable?: boolean;
|
|
alterQuery?: boolean;
|
|
defaultZoomIndex?: number;
|
|
otherOperations?: IItemListOperation<T>[];
|
|
renderContent: (
|
|
result: T,
|
|
filter: ListFilterModel,
|
|
selectedIds: Set<string>,
|
|
onSelectChange: (id: string, selected: boolean, shiftKey: boolean) => void,
|
|
onChangePage: (page: number) => void,
|
|
pageCount: number
|
|
) => React.ReactNode;
|
|
renderEditDialog?: (
|
|
selected: E[],
|
|
onClose: (applied: boolean) => void
|
|
) => React.ReactNode;
|
|
renderDeleteDialog?: (
|
|
selected: E[],
|
|
onClose: (confirmed: boolean) => void
|
|
) => React.ReactNode;
|
|
addKeybinds?: (
|
|
result: T,
|
|
filter: ListFilterModel,
|
|
selectedIds: Set<string>
|
|
) => () => void;
|
|
}
|
|
|
|
const getSelectedData = <I extends IDataItem>(
|
|
data: I[],
|
|
selectedIds: Set<string>
|
|
) => data.filter((value) => selectedIds.has(value.id));
|
|
|
|
/**
|
|
* A factory function for ItemList components.
|
|
* IMPORTANT: as the component manipulates the URL query string, if there are
|
|
* ever multiple ItemLists rendered at once, all but one of them need to have
|
|
* `alterQuery` set to false to prevent conflicts.
|
|
*/
|
|
export function makeItemList<T extends QueryResult, E extends IDataItem>({
|
|
filterMode,
|
|
useResult,
|
|
getCount,
|
|
renderMetadataByline,
|
|
getItems,
|
|
}: IItemListOptions<T, E>) {
|
|
const filterOptions = getFilterOptions(filterMode);
|
|
|
|
const RenderList: React.FC<IItemListProps<T, E> & IRenderListProps> = ({
|
|
filter,
|
|
onChangePage: _onChangePage,
|
|
updateFilter,
|
|
persistState,
|
|
zoomable,
|
|
selectable,
|
|
otherOperations,
|
|
renderContent,
|
|
renderEditDialog,
|
|
renderDeleteDialog,
|
|
addKeybinds,
|
|
}) => {
|
|
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
|
|
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
|
|
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
|
|
const [lastClickedId, setLastClickedId] = useState<string>();
|
|
|
|
const [editingCriterion, setEditingCriterion] = useState<string>();
|
|
const [showEditFilter, setShowEditFilter] = useState(false);
|
|
|
|
const result = useResult(filter);
|
|
const [totalCount, setTotalCount] = useState(0);
|
|
const [metadataByline, setMetadataByline] = useState<React.ReactNode>();
|
|
const items = useMemo(() => getItems(result), [result]);
|
|
|
|
const [arePaging, setArePaging] = useState(false);
|
|
const hidePagination = !arePaging && result.loading;
|
|
|
|
// useLayoutEffect to set total count before paint, avoiding a 0 being displayed
|
|
useLayoutEffect(() => {
|
|
if (result.loading) return;
|
|
setArePaging(false);
|
|
|
|
setTotalCount(getCount(result));
|
|
setMetadataByline(renderMetadataByline?.(result));
|
|
}, [result]);
|
|
|
|
const onChangePage = useCallback(
|
|
(page: number) => {
|
|
setArePaging(true);
|
|
_onChangePage(page);
|
|
},
|
|
[_onChangePage]
|
|
);
|
|
|
|
// handle case where page is more than there are pages
|
|
useEffect(() => {
|
|
const pages = Math.ceil(totalCount / filter.itemsPerPage);
|
|
if (pages > 0 && filter.currentPage > pages) {
|
|
onChangePage(pages);
|
|
}
|
|
}, [filter, onChangePage, totalCount]);
|
|
|
|
// set up hotkeys
|
|
useEffect(() => {
|
|
Mousetrap.bind("f", (e) => {
|
|
setShowEditFilter(true);
|
|
// prevent default behavior of typing f in a text field
|
|
// otherwise the filter dialog closes, the query field is focused and
|
|
// f is typed.
|
|
e.preventDefault();
|
|
});
|
|
|
|
return () => {
|
|
Mousetrap.unbind("f");
|
|
};
|
|
}, []);
|
|
useEffect(() => {
|
|
const pages = Math.ceil(totalCount / filter.itemsPerPage);
|
|
Mousetrap.bind("right", () => {
|
|
if (filter.currentPage < pages) {
|
|
onChangePage(filter.currentPage + 1);
|
|
}
|
|
});
|
|
Mousetrap.bind("left", () => {
|
|
if (filter.currentPage > 1) {
|
|
onChangePage(filter.currentPage - 1);
|
|
}
|
|
});
|
|
Mousetrap.bind("shift+right", () => {
|
|
onChangePage(Math.min(pages, filter.currentPage + 10));
|
|
});
|
|
Mousetrap.bind("shift+left", () => {
|
|
onChangePage(Math.max(1, filter.currentPage - 10));
|
|
});
|
|
Mousetrap.bind("ctrl+end", () => {
|
|
onChangePage(pages);
|
|
});
|
|
Mousetrap.bind("ctrl+home", () => {
|
|
onChangePage(1);
|
|
});
|
|
|
|
return () => {
|
|
Mousetrap.unbind("right");
|
|
Mousetrap.unbind("left");
|
|
Mousetrap.unbind("shift+right");
|
|
Mousetrap.unbind("shift+left");
|
|
Mousetrap.unbind("ctrl+end");
|
|
Mousetrap.unbind("ctrl+home");
|
|
};
|
|
}, [filter, onChangePage, totalCount]);
|
|
useEffect(() => {
|
|
if (addKeybinds) {
|
|
const unbindExtras = addKeybinds(result, filter, selectedIds);
|
|
return () => {
|
|
unbindExtras();
|
|
};
|
|
}
|
|
}, [addKeybinds, result, filter, selectedIds]);
|
|
|
|
function singleSelect(id: string, selected: boolean) {
|
|
setLastClickedId(id);
|
|
|
|
const newSelectedIds = clone(selectedIds);
|
|
if (selected) {
|
|
newSelectedIds.add(id);
|
|
} else {
|
|
newSelectedIds.delete(id);
|
|
}
|
|
|
|
setSelectedIds(newSelectedIds);
|
|
}
|
|
|
|
function selectRange(startIndex: number, endIndex: number) {
|
|
let start = startIndex;
|
|
let end = endIndex;
|
|
if (start > end) {
|
|
const tmp = start;
|
|
start = end;
|
|
end = tmp;
|
|
}
|
|
|
|
const subset = items.slice(start, end + 1);
|
|
const newSelectedIds = new Set<string>();
|
|
|
|
subset.forEach((item) => {
|
|
newSelectedIds.add(item.id);
|
|
});
|
|
|
|
setSelectedIds(newSelectedIds);
|
|
}
|
|
|
|
function multiSelect(id: string) {
|
|
let startIndex = 0;
|
|
let thisIndex = -1;
|
|
|
|
if (lastClickedId) {
|
|
startIndex = items.findIndex((item) => {
|
|
return item.id === lastClickedId;
|
|
});
|
|
}
|
|
|
|
thisIndex = items.findIndex((item) => {
|
|
return item.id === id;
|
|
});
|
|
|
|
selectRange(startIndex, thisIndex);
|
|
}
|
|
|
|
function onSelectChange(id: string, selected: boolean, shiftKey: boolean) {
|
|
if (shiftKey) {
|
|
multiSelect(id);
|
|
} else {
|
|
singleSelect(id, selected);
|
|
}
|
|
}
|
|
|
|
function onSelectAll() {
|
|
const newSelectedIds = new Set<string>();
|
|
items.forEach((item) => {
|
|
newSelectedIds.add(item.id);
|
|
});
|
|
|
|
setSelectedIds(newSelectedIds);
|
|
setLastClickedId(undefined);
|
|
}
|
|
|
|
function onSelectNone() {
|
|
const newSelectedIds = new Set<string>();
|
|
setSelectedIds(newSelectedIds);
|
|
setLastClickedId(undefined);
|
|
}
|
|
|
|
function onChangeZoom(newZoomIndex: number) {
|
|
const newFilter = cloneDeep(filter);
|
|
newFilter.zoomIndex = newZoomIndex;
|
|
updateFilter(newFilter);
|
|
}
|
|
|
|
async function onOperationClicked(o: IItemListOperation<T>) {
|
|
await o.onClick(result, filter, selectedIds);
|
|
if (o.postRefetch) {
|
|
result.refetch();
|
|
}
|
|
}
|
|
|
|
const operations = otherOperations?.map((o) => ({
|
|
text: o.text,
|
|
onClick: () => {
|
|
onOperationClicked(o);
|
|
},
|
|
isDisplayed: () => {
|
|
if (o.isDisplayed) {
|
|
return o.isDisplayed(result, filter, selectedIds);
|
|
}
|
|
|
|
return true;
|
|
},
|
|
icon: o.icon,
|
|
buttonVariant: o.buttonVariant,
|
|
}));
|
|
|
|
function onEdit() {
|
|
setIsEditDialogOpen(true);
|
|
}
|
|
|
|
function onEditDialogClosed(applied: boolean) {
|
|
if (applied) {
|
|
onSelectNone();
|
|
}
|
|
setIsEditDialogOpen(false);
|
|
|
|
// refetch
|
|
result.refetch();
|
|
}
|
|
|
|
function onDelete() {
|
|
setIsDeleteDialogOpen(true);
|
|
}
|
|
|
|
function onDeleteDialogClosed(deleted: boolean) {
|
|
if (deleted) {
|
|
onSelectNone();
|
|
}
|
|
setIsDeleteDialogOpen(false);
|
|
|
|
// refetch
|
|
result.refetch();
|
|
}
|
|
|
|
function renderPagination() {
|
|
if (hidePagination) return;
|
|
return (
|
|
<Pagination
|
|
itemsPerPage={filter.itemsPerPage}
|
|
currentPage={filter.currentPage}
|
|
totalItems={totalCount}
|
|
metadataByline={metadataByline}
|
|
onChangePage={onChangePage}
|
|
/>
|
|
);
|
|
}
|
|
|
|
function renderPaginationIndex() {
|
|
if (hidePagination) return;
|
|
return (
|
|
<PaginationIndex
|
|
itemsPerPage={filter.itemsPerPage}
|
|
currentPage={filter.currentPage}
|
|
totalItems={totalCount}
|
|
metadataByline={metadataByline}
|
|
/>
|
|
);
|
|
}
|
|
|
|
function maybeRenderContent() {
|
|
if (result.loading) {
|
|
return <LoadingIndicator />;
|
|
}
|
|
if (result.error) {
|
|
return <h1>{result.error.message}</h1>;
|
|
}
|
|
|
|
const pages = Math.ceil(totalCount / filter.itemsPerPage);
|
|
return (
|
|
<>
|
|
{renderContent(
|
|
result,
|
|
filter,
|
|
selectedIds,
|
|
onSelectChange,
|
|
onChangePage,
|
|
pages
|
|
)}
|
|
{!!pages && (
|
|
<>
|
|
{renderPaginationIndex()}
|
|
{renderPagination()}
|
|
</>
|
|
)}
|
|
</>
|
|
);
|
|
}
|
|
|
|
function onChangeDisplayMode(displayMode: DisplayMode) {
|
|
const newFilter = cloneDeep(filter);
|
|
newFilter.displayMode = displayMode;
|
|
updateFilter(newFilter);
|
|
}
|
|
|
|
function onRemoveCriterion(removedCriterion: Criterion<CriterionValue>) {
|
|
const newFilter = cloneDeep(filter);
|
|
newFilter.criteria = newFilter.criteria.filter(
|
|
(criterion) => criterion.getId() !== removedCriterion.getId()
|
|
);
|
|
newFilter.currentPage = 1;
|
|
updateFilter(newFilter);
|
|
}
|
|
|
|
function onClearAllCriteria() {
|
|
const newFilter = cloneDeep(filter);
|
|
newFilter.criteria = [];
|
|
newFilter.currentPage = 1;
|
|
updateFilter(newFilter);
|
|
}
|
|
|
|
function onApplyEditFilter(f: ListFilterModel) {
|
|
setShowEditFilter(false);
|
|
setEditingCriterion(undefined);
|
|
updateFilter(f);
|
|
}
|
|
|
|
function onCancelEditFilter() {
|
|
setShowEditFilter(false);
|
|
setEditingCriterion(undefined);
|
|
}
|
|
|
|
return (
|
|
<div className="item-list-container">
|
|
<ButtonToolbar className="justify-content-center">
|
|
<ListFilter
|
|
onFilterUpdate={updateFilter}
|
|
filter={filter}
|
|
filterOptions={filterOptions}
|
|
openFilterDialog={() => setShowEditFilter(true)}
|
|
persistState={persistState}
|
|
/>
|
|
<ListOperationButtons
|
|
onSelectAll={selectable ? onSelectAll : undefined}
|
|
onSelectNone={selectable ? onSelectNone : undefined}
|
|
otherOperations={operations}
|
|
itemsSelected={selectedIds.size > 0}
|
|
onEdit={renderEditDialog ? onEdit : undefined}
|
|
onDelete={renderDeleteDialog ? onDelete : undefined}
|
|
/>
|
|
<ListViewOptions
|
|
displayMode={filter.displayMode}
|
|
displayModeOptions={filterOptions.displayModeOptions}
|
|
onSetDisplayMode={onChangeDisplayMode}
|
|
zoomIndex={zoomable ? filter.zoomIndex : undefined}
|
|
onSetZoom={zoomable ? onChangeZoom : undefined}
|
|
/>
|
|
</ButtonToolbar>
|
|
<FilterTags
|
|
criteria={filter.criteria}
|
|
onEditCriterion={(c) => setEditingCriterion(c.criterionOption.type)}
|
|
onRemoveCriterion={onRemoveCriterion}
|
|
onRemoveAll={() => onClearAllCriteria()}
|
|
/>
|
|
{(showEditFilter || editingCriterion) && (
|
|
<EditFilterDialog
|
|
filter={filter}
|
|
onApply={onApplyEditFilter}
|
|
onCancel={onCancelEditFilter}
|
|
editingCriterion={editingCriterion}
|
|
/>
|
|
)}
|
|
{isEditDialogOpen &&
|
|
renderEditDialog &&
|
|
renderEditDialog(getSelectedData(items, selectedIds), (applied) =>
|
|
onEditDialogClosed(applied)
|
|
)}
|
|
{isDeleteDialogOpen &&
|
|
renderDeleteDialog &&
|
|
renderDeleteDialog(getSelectedData(items, selectedIds), (deleted) =>
|
|
onDeleteDialogClosed(deleted)
|
|
)}
|
|
{renderPagination()}
|
|
{renderPaginationIndex()}
|
|
{maybeRenderContent()}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const ItemList: React.FC<IItemListProps<T, E>> = (props) => {
|
|
const {
|
|
persistState,
|
|
persistanceKey = filterMode,
|
|
defaultSort = filterOptions.defaultSortBy,
|
|
filterHook,
|
|
defaultZoomIndex,
|
|
alterQuery = true,
|
|
} = props;
|
|
|
|
const history = useHistory();
|
|
const location = useLocation();
|
|
const [interfaceState, setInterfaceState] = useInterfaceLocalForage();
|
|
const [filterInitialised, setFilterInitialised] = useState(false);
|
|
const { configuration: config } = useContext(ConfigurationContext);
|
|
|
|
const lastPathname = useRef(location.pathname);
|
|
const defaultDisplayMode = filterOptions.displayModeOptions[0];
|
|
const [filter, setFilter] = useState<ListFilterModel>(
|
|
() => new ListFilterModel(filterMode)
|
|
);
|
|
|
|
const updateSavedFilter = useCallback(
|
|
(updatedFilter: ListFilterModel) => {
|
|
setInterfaceState((prevState) => {
|
|
if (!prevState.queryConfig) {
|
|
prevState.queryConfig = {};
|
|
}
|
|
|
|
const oldFilter = prevState.queryConfig[persistanceKey]?.filter ?? "";
|
|
const newFilter = new URLSearchParams(oldFilter);
|
|
newFilter.set("disp", String(updatedFilter.displayMode));
|
|
|
|
return {
|
|
...prevState,
|
|
queryConfig: {
|
|
...prevState.queryConfig,
|
|
[persistanceKey]: {
|
|
...prevState.queryConfig[persistanceKey],
|
|
filter: newFilter.toString(),
|
|
},
|
|
},
|
|
};
|
|
});
|
|
},
|
|
[persistanceKey, setInterfaceState]
|
|
);
|
|
|
|
const { data: defaultFilter, loading: defaultFilterLoading } =
|
|
useFindDefaultFilter(filterMode);
|
|
|
|
const updateQueryParams = useCallback(
|
|
(newFilter: ListFilterModel) => {
|
|
if (!alterQuery) return;
|
|
|
|
const newParams = newFilter.makeQueryParameters();
|
|
history.replace({ ...history.location, search: newParams });
|
|
},
|
|
[alterQuery, history]
|
|
);
|
|
|
|
const updateFilter = useCallback(
|
|
(newFilter: ListFilterModel) => {
|
|
setFilter(newFilter);
|
|
updateQueryParams(newFilter);
|
|
if (persistState === PersistanceLevel.VIEW) {
|
|
updateSavedFilter(newFilter);
|
|
}
|
|
},
|
|
[persistState, updateSavedFilter, updateQueryParams]
|
|
);
|
|
|
|
// 'Startup' hook, initialises the filters
|
|
useEffect(() => {
|
|
// Only run once
|
|
if (filterInitialised) return;
|
|
|
|
let newFilter = new ListFilterModel(
|
|
filterMode,
|
|
config,
|
|
defaultSort,
|
|
defaultDisplayMode,
|
|
defaultZoomIndex
|
|
);
|
|
let loadDefault = true;
|
|
if (alterQuery && location.search) {
|
|
loadDefault = false;
|
|
newFilter.configureFromQueryString(location.search);
|
|
}
|
|
|
|
if (persistState === PersistanceLevel.ALL) {
|
|
// only set default filter if uninitialised
|
|
if (loadDefault) {
|
|
// wait until default filter is loaded
|
|
if (defaultFilterLoading) return;
|
|
|
|
if (defaultFilter?.findDefaultFilter) {
|
|
newFilter.currentPage = 1;
|
|
try {
|
|
newFilter.configureFromSavedFilter(
|
|
defaultFilter.findDefaultFilter
|
|
);
|
|
} catch (err) {
|
|
console.log(err);
|
|
// ignore
|
|
}
|
|
// #1507 - reset random seed when loaded
|
|
newFilter.randomSeed = -1;
|
|
}
|
|
}
|
|
} else if (persistState === PersistanceLevel.VIEW) {
|
|
// wait until forage is initialised
|
|
if (interfaceState.loading) return;
|
|
|
|
const storedQuery = interfaceState.data?.queryConfig?.[persistanceKey];
|
|
if (persistState === PersistanceLevel.VIEW && storedQuery) {
|
|
const displayMode = new URLSearchParams(storedQuery.filter).get(
|
|
"disp"
|
|
);
|
|
if (displayMode) {
|
|
newFilter.displayMode = Number.parseInt(displayMode, 10);
|
|
}
|
|
}
|
|
}
|
|
setFilter(newFilter);
|
|
updateQueryParams(newFilter);
|
|
|
|
setFilterInitialised(true);
|
|
}, [
|
|
filterInitialised,
|
|
location,
|
|
config,
|
|
defaultSort,
|
|
defaultDisplayMode,
|
|
defaultZoomIndex,
|
|
alterQuery,
|
|
persistState,
|
|
updateQueryParams,
|
|
defaultFilter,
|
|
defaultFilterLoading,
|
|
interfaceState,
|
|
persistanceKey,
|
|
]);
|
|
|
|
// This hook runs on every page location change (ie navigation),
|
|
// and updates the filter accordingly.
|
|
useEffect(() => {
|
|
if (!filterInitialised || !alterQuery) return;
|
|
|
|
// re-init if the pathname has changed
|
|
if (location.pathname !== lastPathname.current) {
|
|
lastPathname.current = location.pathname;
|
|
setFilterInitialised(false);
|
|
return;
|
|
}
|
|
|
|
// re-init to load default filter on empty new query params
|
|
if (!location.search) {
|
|
setFilterInitialised(false);
|
|
return;
|
|
}
|
|
|
|
// the query has changed, update filter if necessary
|
|
setFilter((prevFilter) => {
|
|
let newFilter = prevFilter.clone();
|
|
newFilter.configureFromQueryString(location.search);
|
|
if (!isEqual(newFilter, prevFilter)) {
|
|
return newFilter;
|
|
} else {
|
|
return prevFilter;
|
|
}
|
|
});
|
|
}, [filterInitialised, alterQuery, location]);
|
|
|
|
const onChangePage = useCallback(
|
|
(page: number) => {
|
|
const newFilter = cloneDeep(filter);
|
|
newFilter.currentPage = page;
|
|
updateFilter(newFilter);
|
|
|
|
// if the current page has a detail-header, then
|
|
// scroll up relative to that rather than 0, 0
|
|
const detailHeader = document.querySelector(".detail-header");
|
|
if (detailHeader) {
|
|
window.scrollTo(0, detailHeader.scrollHeight - 50);
|
|
} else {
|
|
window.scrollTo(0, 0);
|
|
}
|
|
},
|
|
[filter, updateFilter]
|
|
);
|
|
|
|
const renderFilter = useMemo(() => {
|
|
if (filterInitialised) {
|
|
return filterHook ? filterHook(cloneDeep(filter)) : filter;
|
|
}
|
|
}, [filterInitialised, filter, filterHook]);
|
|
|
|
if (!renderFilter) return null;
|
|
|
|
return (
|
|
<RenderList
|
|
filter={renderFilter}
|
|
onChangePage={onChangePage}
|
|
updateFilter={updateFilter}
|
|
{...props}
|
|
/>
|
|
);
|
|
};
|
|
|
|
return ItemList;
|
|
}
|
|
|
|
export const showWhenSelected = <T extends QueryResult>(
|
|
result: T,
|
|
filter: ListFilterModel,
|
|
selectedIds: Set<string>
|
|
) => {
|
|
return selectedIds.size > 0;
|
|
};
|