Files
stash/ui/v2.5/src/hooks/ListHook.tsx
2020-03-01 21:04:31 +01:00

311 lines
11 KiB
TypeScript

import _ from "lodash";
import queryString from "query-string";
import React, { useEffect, useState } from "react";
import { Spinner } from 'react-bootstrap';
import { QueryHookResult } from "react-apollo-hooks";
import { ListFilter } from "../components/list/ListFilter";
import { Pagination } from "../components/list/Pagination";
import { StashService } from "../core/StashService";
import { IBaseProps } from "../models";
import { Criterion } from "../models/list-filter/criteria/criterion";
import { ListFilterModel } from "../models/list-filter/filter";
import { DisplayMode, FilterMode } from "../models/list-filter/types";
export interface IListHookData {
filter: ListFilterModel;
template: JSX.Element;
options: IListHookOptions;
onSelectChange: (id: string, selected : boolean, shiftKey: boolean) => void;
}
interface IListHookOperation {
text: string;
onClick: (result: QueryHookResult<any, any>, filter: ListFilterModel, selectedIds: Set<string>) => void;
}
export interface IListHookOptions {
filterMode: FilterMode;
props: IBaseProps;
zoomable?: boolean;
otherOperations?: IListHookOperation[];
renderContent: (result: QueryHookResult<any, any>, filter: ListFilterModel, selectedIds: Set<string>, zoomIndex: number) => JSX.Element | undefined;
renderSelectedOptions?: (result: QueryHookResult<any, any>, selectedIds: Set<string>) => JSX.Element | undefined;
}
export class ListHook {
public static useList(options: IListHookOptions): IListHookData {
const [filter, setFilter] = useState<ListFilterModel>(new ListFilterModel(options.filterMode));
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
const [lastClickedId, setLastClickedId] = useState<string | undefined>(undefined);
const [totalCount, setTotalCount] = useState<number>(0);
const [zoomIndex, setZoomIndex] = useState<number>(1);
// Update the filter when the query parameters change
useEffect(() => {
const queryParams = queryString.parse(options.props.location.search);
const newFilter = _.cloneDeep(filter);
newFilter.configureFromQueryParameters(queryParams);
setFilter(newFilter);
// TODO: Need this side effect to update the query params properly
filter.configureFromQueryParameters(queryParams);
}, [options.props.location.search]);
let result: QueryHookResult<any, any>;
let getData: (filter : ListFilterModel) => QueryHookResult<any, any>;
let getItems: () => any[];
let getCount: () => number;
switch (options.filterMode) {
case FilterMode.Scenes: {
getData = (filter : ListFilterModel) => { return StashService.useFindScenes(filter); }
getItems = () => { return !!result.data && !!result.data.findScenes ? result.data.findScenes.scenes : []; }
getCount = () => { return !!result.data && !!result.data.findScenes ? result.data.findScenes.count : 0; }
break;
}
case FilterMode.SceneMarkers: {
getData = (filter : ListFilterModel) => { return StashService.useFindSceneMarkers(filter); }
getItems = () => { return !!result.data && !!result.data.findSceneMarkers ? result.data.findSceneMarkers.scene_markers : []; }
getCount = () => { return !!result.data && !!result.data.findSceneMarkers ? result.data.findSceneMarkers.count : 0; }
break;
}
case FilterMode.Galleries: {
getData = (filter : ListFilterModel) => { return StashService.useFindGalleries(filter); }
getItems = () => { return !!result.data && !!result.data.findGalleries ? result.data.findGalleries.galleries : []; }
getCount = () => { return !!result.data && !!result.data.findGalleries ? result.data.findGalleries.count : 0; }
break;
}
case FilterMode.Studios: {
getData = (filter : ListFilterModel) => { return StashService.useFindStudios(filter); }
getItems = () => { return !!result.data && !!result.data.findStudios ? result.data.findStudios.studios : []; }
getCount = () => { return !!result.data && !!result.data.findStudios ? result.data.findStudios.count : 0; }
break;
}
case FilterMode.Performers: {
getData = (filter : ListFilterModel) => { return StashService.useFindPerformers(filter); }
getItems = () => { return !!result.data && !!result.data.findPerformers ? result.data.findPerformers.performers : []; }
getCount = () => { return !!result.data && !!result.data.findPerformers ? result.data.findPerformers.count : 0; }
break;
}
default: {
console.error("REMOVE DEFAULT IN LIST HOOK");
getData = (filter : ListFilterModel) => { return StashService.useFindScenes(filter); }
getItems = () => { return !!result.data && !!result.data.findScenes ? result.data.findScenes.scenes : []; }
getCount = () => { return !!result.data && !!result.data.findScenes ? result.data.findScenes.count : 0; }
break;
}
}
result = getData(filter);
useEffect(() => {
setTotalCount(getCount());
// select none when data changes
onSelectNone();
setLastClickedId(undefined);
}, [result.data])
// Update the query parameters when the data changes
useEffect(() => {
const location = Object.assign({}, options.props.history.location);
location.search = filter.makeQueryParameters();
options.props.history.replace(location);
}, [result.data, filter.displayMode]);
// Update the total count
useEffect(() => {
const newFilter = _.cloneDeep(filter);
newFilter.totalCount = totalCount;
setFilter(newFilter);
}, [totalCount]);
function onChangePageSize(pageSize: number) {
const newFilter = _.cloneDeep(filter);
newFilter.itemsPerPage = pageSize;
newFilter.currentPage = 1;
setFilter(newFilter);
}
function onChangeQuery(query: string) {
const newFilter = _.cloneDeep(filter);
newFilter.searchTerm = query;
newFilter.currentPage = 1;
setFilter(newFilter);
}
function onChangeSortDirection(sortDirection: "asc" | "desc") {
const newFilter = _.cloneDeep(filter);
newFilter.sortDirection = sortDirection;
setFilter(newFilter);
}
function onChangeSortBy(sortBy: string) {
const newFilter = _.cloneDeep(filter);
newFilter.sortBy = sortBy;
newFilter.currentPage = 1;
setFilter(newFilter);
}
function onChangeDisplayMode(displayMode: DisplayMode) {
const newFilter = _.cloneDeep(filter);
newFilter.displayMode = displayMode;
setFilter(newFilter);
}
function onAddCriterion(criterion: Criterion, oldId?: string) {
const newFilter = _.cloneDeep(filter);
// Find if we are editing an existing criteria, then modify that. Or create a new one.
const existingIndex = newFilter.criteria.findIndex((c) => {
// If we modified an existing criterion, then look for the old id.
const id = !!oldId ? oldId : criterion.getId();
return c.getId() === id;
});
if (existingIndex === -1) {
newFilter.criteria.push(criterion);
} else {
newFilter.criteria[existingIndex] = criterion;
}
// Remove duplicate modifiers
newFilter.criteria = newFilter.criteria.filter((obj, pos, arr) => {
return arr.map((mapObj: any) => mapObj.getId()).indexOf(obj.getId()) === pos;
});
newFilter.currentPage = 1;
setFilter(newFilter);
}
function onRemoveCriterion(removedCriterion: Criterion) {
const newFilter = _.cloneDeep(filter);
newFilter.criteria = newFilter.criteria.filter((criterion) => criterion.getId() !== removedCriterion.getId());
newFilter.currentPage = 1;
setFilter(newFilter);
}
function onChangePage(page: number) {
const newFilter = _.cloneDeep(filter);
newFilter.currentPage = page;
setFilter(newFilter);
}
function onSelectChange(id: string, selected : boolean, shiftKey: boolean) {
if (shiftKey) {
multiSelect(id, selected);
} else {
singleSelect(id, selected);
}
}
function singleSelect(id: string, selected: boolean) {
setLastClickedId(id);
const newSelectedIds = _.clone(selectedIds);
if (selected) {
newSelectedIds.add(id);
} else {
newSelectedIds.delete(id);
}
setSelectedIds(newSelectedIds);
}
function multiSelect(id: string, selected : boolean) {
let startIndex = 0;
let thisIndex = -1;
if (!!lastClickedId) {
startIndex = getItems().findIndex((item) => {
return item.id === lastClickedId;
});
}
thisIndex = getItems().findIndex((item) => {
return item.id === id;
});
selectRange(startIndex, thisIndex);
}
function selectRange(startIndex : number, endIndex : number) {
if (startIndex > endIndex) {
let tmp = startIndex;
startIndex = endIndex;
endIndex = tmp;
}
const subset = getItems().slice(startIndex, endIndex + 1);
const newSelectedIds : Set<string> = new Set();
subset.forEach((item) => {
newSelectedIds.add(item.id);
});
setSelectedIds(newSelectedIds);
}
function onSelectAll() {
const newSelectedIds : Set<string> = new Set();
getItems().forEach((item) => {
newSelectedIds.add(item.id);
});
setSelectedIds(newSelectedIds);
setLastClickedId(undefined);
}
function onSelectNone() {
const newSelectedIds : Set<string> = new Set();
setSelectedIds(newSelectedIds);
setLastClickedId(undefined);
}
function onChangeZoom(newZoomIndex : number) {
setZoomIndex(newZoomIndex);
}
const otherOperations = options.otherOperations ? options.otherOperations.map((o) => {
return {
text: o.text,
onClick: () => {
o.onClick(result, filter, selectedIds);
}
}
}) : undefined;
const template = (
<div>
<ListFilter
onChangePageSize={onChangePageSize}
onChangeQuery={onChangeQuery}
onChangeSortDirection={onChangeSortDirection}
onChangeSortBy={onChangeSortBy}
onChangeDisplayMode={onChangeDisplayMode}
onAddCriterion={onAddCriterion}
onRemoveCriterion={onRemoveCriterion}
onSelectAll={onSelectAll}
onSelectNone={onSelectNone}
zoomIndex={options.zoomable ? zoomIndex : undefined}
onChangeZoom={options.zoomable ? onChangeZoom : undefined}
otherOperations={otherOperations}
filter={filter}
/>
{options.renderSelectedOptions && selectedIds.size > 0 ? options.renderSelectedOptions(result, selectedIds) : undefined}
{result.loading ? <Spinner animation="border" variant="light" /> : undefined}
{result.error ? <h1>{result.error.message}</h1> : undefined}
{options.renderContent(result, filter, selectedIds, zoomIndex)}
<Pagination
itemsPerPage={filter.itemsPerPage}
currentPage={filter.currentPage}
totalItems={totalCount}
onChangePage={onChangePage}
/>
</div>
);
return { filter, template, options, onSelectChange };
}
}