mirror of
https://github.com/stashapp/stash.git
synced 2026-04-23 08:59:08 -05:00
Maintain filter parameters in session (#326)
* Maintain query parameters in local forage * Keep items per page * Refactor * Fix warnings * Add back required go module * Fix loading spinner on sub-component listhook * Add queries to localforage if not present
This commit is contained in:
@@ -12,6 +12,8 @@ require (
|
||||
github.com/golang-migrate/migrate/v4 v4.3.1
|
||||
github.com/gorilla/websocket v1.4.0
|
||||
github.com/h2non/filetype v1.0.8
|
||||
// this is required for generate
|
||||
github.com/inconshreveable/mousetrap v1.0.0 // indirect
|
||||
github.com/jmoiron/sqlx v1.2.0
|
||||
github.com/mattn/go-sqlite3 v1.10.0
|
||||
github.com/rs/cors v1.6.0
|
||||
|
||||
@@ -78,8 +78,9 @@ export class Pagination extends React.Component<IPaginationProps, IPaginationSta
|
||||
|
||||
const pagerState = this.getPagerState(this.props.totalItems, page, this.props.itemsPerPage);
|
||||
|
||||
if (page < 1) { page = 1; }
|
||||
// rearranged this so that the minimum page number is 1, not 0
|
||||
if (page > pagerState.totalPages) { page = pagerState.totalPages; }
|
||||
if (page < 1) { page = 1; }
|
||||
|
||||
this.setState(pagerState);
|
||||
if (propagate) { this.props.onChangePage(page); }
|
||||
|
||||
+136
-81
@@ -1,7 +1,7 @@
|
||||
import { Spinner } from "@blueprintjs/core";
|
||||
import _ from "lodash";
|
||||
import queryString from "query-string";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import React, { useEffect, useState, useRef } from "react";
|
||||
import { QueryHookResult } from "react-apollo-hooks";
|
||||
import { ListFilter } from "../components/list/ListFilter";
|
||||
import { Pagination } from "../components/list/Pagination";
|
||||
@@ -10,6 +10,7 @@ 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";
|
||||
import { useInterfaceLocalForage } from "./LocalForage";
|
||||
|
||||
export interface IListHookData {
|
||||
filter: ListFilterModel;
|
||||
@@ -23,6 +24,66 @@ interface IListHookOperation {
|
||||
onClick: (result: QueryHookResult<any, any>, filter: ListFilterModel, selectedIds: Set<string>) => void;
|
||||
}
|
||||
|
||||
export interface IFilterListImpl {
|
||||
getData: (filter : ListFilterModel) => QueryHookResult<any, any>;
|
||||
getItems: (data: any) => any[];
|
||||
getCount: (data: any) => number;
|
||||
}
|
||||
|
||||
const SceneFilterListImpl: IFilterListImpl = {
|
||||
getData: (filter : ListFilterModel) => { return StashService.useFindScenes(filter); },
|
||||
getItems: (data: any) => { return !!data && !!data.findScenes ? data.findScenes.scenes : []; },
|
||||
getCount: (data: any) => { return !!data && !!data.findScenes ? data.findScenes.count : 0; }
|
||||
}
|
||||
|
||||
const SceneMarkerFilterListImpl: IFilterListImpl = {
|
||||
getData: (filter : ListFilterModel) => { return StashService.useFindSceneMarkers(filter); },
|
||||
getItems: (data: any) => { return !!data && !!data.findSceneMarkers ? data.findSceneMarkers.scene_markers : []; },
|
||||
getCount: (data: any) => { return !!data && !!data.findSceneMarkers ? data.findSceneMarkers.count : 0; }
|
||||
}
|
||||
|
||||
const GalleryFilterListImpl: IFilterListImpl = {
|
||||
getData: (filter : ListFilterModel) => { return StashService.useFindGalleries(filter); },
|
||||
getItems: (data: any) => { return !!data && !!data.findGalleries ? data.findGalleries.galleries : []; },
|
||||
getCount: (data: any) => { return !!data && !!data.findGalleries ? data.findGalleries.count : 0; }
|
||||
}
|
||||
|
||||
const StudioFilterListImpl: IFilterListImpl = {
|
||||
getData: (filter : ListFilterModel) => { return StashService.useFindStudios(filter); },
|
||||
getItems: (data: any) => { return !!data && !!data.findStudios ? data.findStudios.studios : []; },
|
||||
getCount: (data: any) => { return !!data && !!data.findStudios ? data.findStudios.count : 0; }
|
||||
}
|
||||
|
||||
const PerformerFilterListImpl: IFilterListImpl = {
|
||||
getData: (filter : ListFilterModel) => { return StashService.useFindPerformers(filter); },
|
||||
getItems: (data: any) => { return !!data && !!data.findPerformers ? data.findPerformers.performers : []; },
|
||||
getCount: (data: any) => { return !!data && !!data.findPerformers ? data.findPerformers.count : 0; }
|
||||
}
|
||||
|
||||
function getFilterListImpl(filterMode: FilterMode) {
|
||||
switch (filterMode) {
|
||||
case FilterMode.Scenes: {
|
||||
return SceneFilterListImpl;
|
||||
}
|
||||
case FilterMode.SceneMarkers: {
|
||||
return SceneMarkerFilterListImpl;
|
||||
}
|
||||
case FilterMode.Galleries: {
|
||||
return GalleryFilterListImpl;
|
||||
}
|
||||
case FilterMode.Studios: {
|
||||
return StudioFilterListImpl;
|
||||
}
|
||||
case FilterMode.Performers: {
|
||||
return PerformerFilterListImpl;
|
||||
}
|
||||
default: {
|
||||
console.error("REMOVE DEFAULT IN LIST HOOK");
|
||||
return SceneFilterListImpl;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface IListHookOptions {
|
||||
filterMode: FilterMode;
|
||||
subComponent?: boolean;
|
||||
@@ -42,19 +103,52 @@ export class ListHook {
|
||||
const [totalCount, setTotalCount] = useState<number>(0);
|
||||
const [zoomIndex, setZoomIndex] = useState<number>(1);
|
||||
|
||||
// Update the filter when the query parameters change
|
||||
// don't use query parameters for sub-components
|
||||
if (!options.subComponent) {
|
||||
useEffect(() => {
|
||||
const queryParams = queryString.parse(options.props!.location.search);
|
||||
const newFilter = _.cloneDeep(filter);
|
||||
newFilter.configureFromQueryParameters(queryParams);
|
||||
setFilter(newFilter);
|
||||
const [interfaceForage, setInterfaceForage] = useInterfaceLocalForage();
|
||||
const forageInitialised = useRef<boolean>(false);
|
||||
|
||||
// TODO: Need this side effect to update the query params properly
|
||||
filter.configureFromQueryParameters(queryParams);
|
||||
}, [options.props.location.search]);
|
||||
}
|
||||
const filterListImpl = getFilterListImpl(options.filterMode);
|
||||
|
||||
// Update the filter when the query parameters change
|
||||
// we want to use the local forage only when the location search has not been set
|
||||
useEffect(() => {
|
||||
function updateFromLocalForage(queryData: any) {
|
||||
const queryParams = queryString.parse(queryData.filter);
|
||||
|
||||
setFilter((f) => {
|
||||
const newFilter = _.cloneDeep(f);
|
||||
newFilter.configureFromQueryParameters(queryParams);
|
||||
newFilter.currentPage = queryData.currentPage;
|
||||
newFilter.itemsPerPage = queryData.itemsPerPage;
|
||||
return newFilter;
|
||||
});
|
||||
}
|
||||
|
||||
function updateFromQueryString(queryStr: string) {
|
||||
const queryParams = queryString.parse(queryStr);
|
||||
setFilter((f) => {
|
||||
const newFilter = _.cloneDeep(f);
|
||||
newFilter.configureFromQueryParameters(queryParams);
|
||||
return newFilter;
|
||||
});
|
||||
}
|
||||
|
||||
// don't use query parameters for sub-components
|
||||
if (!options.subComponent) {
|
||||
// do this only once after local forage has been initialised
|
||||
if (!forageInitialised.current && !interfaceForage.loading) {
|
||||
forageInitialised.current = true;
|
||||
|
||||
if (!options.props!.location.search && interfaceForage.data && interfaceForage.data.queries[options.filterMode]) {
|
||||
let queryData = interfaceForage.data.queries[options.filterMode];
|
||||
// we have some data, try to load it
|
||||
updateFromLocalForage(queryData);
|
||||
} else if (interfaceForage.data) {
|
||||
// else fallback to query string
|
||||
updateFromQueryString(options.props!.location.search);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [interfaceForage.data, interfaceForage.loading, options.props, options.filterMode, options.subComponent]);
|
||||
|
||||
function getFilter() {
|
||||
if (!options.filterHook) {
|
||||
@@ -66,78 +160,39 @@ export class ListHook {
|
||||
return options.filterHook(newFilter);
|
||||
}
|
||||
|
||||
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(getFilter());
|
||||
const result = filterListImpl.getData(getFilter());
|
||||
|
||||
useEffect(() => {
|
||||
setTotalCount(getCount());
|
||||
setTotalCount(filterListImpl.getCount(result.data));
|
||||
|
||||
// select none when data changes
|
||||
onSelectNone();
|
||||
setLastClickedId(undefined);
|
||||
}, [result.data])
|
||||
}, [result.data, filterListImpl])
|
||||
|
||||
// Update the query parameters when the data changes
|
||||
// don't use query parameters for sub-components
|
||||
if (!options.subComponent) {
|
||||
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]);
|
||||
// don't use query parameters for sub-components
|
||||
if (!options.subComponent) {
|
||||
// don't update this until local forage is loaded
|
||||
if (forageInitialised.current) {
|
||||
const location = Object.assign({}, options.props.history.location);
|
||||
location.search = filter.makeQueryParameters();
|
||||
options.props.history.replace(location);
|
||||
|
||||
setInterfaceForage((d) => {
|
||||
const dataClone = _.cloneDeep(d);
|
||||
dataClone!.queries[options.filterMode] = {
|
||||
filter: location.search,
|
||||
itemsPerPage: filter.itemsPerPage,
|
||||
currentPage: filter.currentPage
|
||||
};
|
||||
return dataClone;
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [result.data, filter, options.subComponent, options.filterMode, options.props.history, setInterfaceForage]);
|
||||
|
||||
function onChangePageSize(pageSize: number) {
|
||||
const newFilter = _.cloneDeep(filter);
|
||||
@@ -235,12 +290,12 @@ export class ListHook {
|
||||
let thisIndex = -1;
|
||||
|
||||
if (!!lastClickedId) {
|
||||
startIndex = getItems().findIndex((item) => {
|
||||
startIndex = filterListImpl.getItems(result).findIndex((item) => {
|
||||
return item.id === lastClickedId;
|
||||
});
|
||||
}
|
||||
|
||||
thisIndex = getItems().findIndex((item) => {
|
||||
thisIndex = filterListImpl.getItems(result).findIndex((item) => {
|
||||
return item.id === id;
|
||||
});
|
||||
|
||||
@@ -254,7 +309,7 @@ export class ListHook {
|
||||
endIndex = tmp;
|
||||
}
|
||||
|
||||
const subset = getItems().slice(startIndex, endIndex + 1);
|
||||
const subset = filterListImpl.getItems(result).slice(startIndex, endIndex + 1);
|
||||
const newSelectedIds : Set<string> = new Set();
|
||||
|
||||
subset.forEach((item) => {
|
||||
@@ -266,7 +321,7 @@ export class ListHook {
|
||||
|
||||
function onSelectAll() {
|
||||
const newSelectedIds : Set<string> = new Set();
|
||||
getItems().forEach((item) => {
|
||||
filterListImpl.getItems(result).forEach((item) => {
|
||||
newSelectedIds.add(item.id);
|
||||
});
|
||||
|
||||
@@ -311,7 +366,7 @@ export class ListHook {
|
||||
filter={filter}
|
||||
/>
|
||||
{options.renderSelectedOptions && selectedIds.size > 0 ? options.renderSelectedOptions(result, selectedIds) : undefined}
|
||||
{result.loading ? <Spinner size={Spinner.SIZE_LARGE} /> : undefined}
|
||||
{result.loading || (!options.subComponent && !forageInitialised.current) ? <Spinner size={Spinner.SIZE_LARGE} /> : undefined}
|
||||
{result.error ? <h1>{result.error.message}</h1> : undefined}
|
||||
{options.renderContent(result, filter, selectedIds, zoomIndex)}
|
||||
<Pagination
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import localForage from "localforage";
|
||||
import _ from "lodash";
|
||||
import React from "react";
|
||||
import React, { Dispatch, SetStateAction } from "react";
|
||||
|
||||
interface IInterfaceWallConfig {
|
||||
}
|
||||
export interface IInterfaceConfig {
|
||||
wall: IInterfaceWallConfig;
|
||||
queries: any;
|
||||
}
|
||||
|
||||
type ValidTypes = IInterfaceConfig | undefined;
|
||||
@@ -14,25 +15,33 @@ interface ILocalForage<T> {
|
||||
data: T;
|
||||
setData: React.Dispatch<React.SetStateAction<T>>;
|
||||
error: Error | null;
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
export function useInterfaceLocalForage(): ILocalForage<IInterfaceConfig | undefined> {
|
||||
export function useInterfaceLocalForage(): [ILocalForage<IInterfaceConfig | undefined>, React.Dispatch<React.SetStateAction<IInterfaceConfig | undefined>>] {
|
||||
const result = useLocalForage("interface");
|
||||
// Set defaults
|
||||
React.useEffect(() => {
|
||||
if (result.data === undefined) {
|
||||
if (!result.data) {
|
||||
result.setData({
|
||||
wall: {
|
||||
// nothing here currently
|
||||
},
|
||||
queries: {}
|
||||
});
|
||||
} else if (!result.data.queries) {
|
||||
let newData = Object.assign({}, result.data);
|
||||
newData.queries = {};
|
||||
result.setData(newData);
|
||||
}
|
||||
});
|
||||
return result;
|
||||
return [result, result.setData];
|
||||
}
|
||||
|
||||
function useLocalForage(item: string): ILocalForage<ValidTypes> {
|
||||
const [json, setJson] = React.useState<ValidTypes>(undefined);
|
||||
const [err, setErr] = React.useState(null);
|
||||
const [loaded, setLoaded] = React.useState<boolean>(false);
|
||||
|
||||
const prevJson = React.useRef<ValidTypes>(undefined);
|
||||
React.useEffect(() => {
|
||||
@@ -45,7 +54,6 @@ function useLocalForage(item: string): ILocalForage<ValidTypes> {
|
||||
runAsync();
|
||||
});
|
||||
|
||||
const [err, setErr] = React.useState(null);
|
||||
React.useEffect(() => {
|
||||
async function runAsync() {
|
||||
try {
|
||||
@@ -58,9 +66,10 @@ function useLocalForage(item: string): ILocalForage<ValidTypes> {
|
||||
} catch (error) {
|
||||
setErr(error);
|
||||
}
|
||||
setLoaded(true);
|
||||
}
|
||||
runAsync();
|
||||
});
|
||||
|
||||
return {data: json, setData: setJson, error: err};
|
||||
return {data: json, setData: setJson, error: err, loading: !loaded};
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ interface IQueryParameters {
|
||||
disp?: string;
|
||||
q?: string;
|
||||
p?: string;
|
||||
perPage?: string;
|
||||
c?: string[];
|
||||
}
|
||||
|
||||
@@ -45,7 +46,6 @@ export class ListFilterModel {
|
||||
public displayModeOptions: DisplayMode[] = [];
|
||||
public criterionOptions: ICriterionOption[] = [];
|
||||
public criteria: Array<Criterion<any, any>> = [];
|
||||
public totalCount: number = 0;
|
||||
public randomSeed: number = -1;
|
||||
|
||||
private static createCriterionOption(criterion: CriterionType) {
|
||||
@@ -180,6 +180,9 @@ export class ListFilterModel {
|
||||
if (params.p !== undefined) {
|
||||
this.currentPage = Number(params.p);
|
||||
}
|
||||
if (params.perPage !== undefined) {
|
||||
this.itemsPerPage = Number(params.perPage);
|
||||
}
|
||||
|
||||
if (params.c !== undefined) {
|
||||
this.criteria = [];
|
||||
@@ -241,6 +244,7 @@ export class ListFilterModel {
|
||||
disp: this.displayMode,
|
||||
q: this.searchTerm,
|
||||
p: this.currentPage,
|
||||
perPage: this.itemsPerPage,
|
||||
c: encodedCriteria,
|
||||
};
|
||||
return queryString.stringify(result, {encode: false});
|
||||
|
||||
Reference in New Issue
Block a user