Default view filters (#4962)

* Merge/adapt from yoshnopa:defaultDetails
* Deprecate and remove default filter calls
* Fix weird behaviour when clicking set as default
* Update deprecated get/set default filter resolvers
* Add config migration
---------
Co-authored-by: yoshnopa <usingusenet@protonmail.com>
This commit is contained in:
WithoutPants 2024-06-18 10:51:52 +10:00 committed by GitHub
parent 4be60310c3
commit f9a624b803
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
59 changed files with 611 additions and 403 deletions

2
go.mod
View file

@ -34,6 +34,7 @@ require (
github.com/knadh/koanf v1.5.0
github.com/lucasb-eyer/go-colorful v1.2.0
github.com/mattn/go-sqlite3 v1.14.22
github.com/mitchellh/mapstructure v1.5.0
github.com/natefinch/pie v0.0.0-20170715172608-9a0d72014007
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8
github.com/remeh/sizedwaitgroup v1.0.0
@ -88,7 +89,6 @@ require (
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect

View file

@ -4,6 +4,7 @@ type Query {
findSavedFilter(id: ID!): SavedFilter
findSavedFilters(mode: FilterMode): [SavedFilter!]!
findDefaultFilter(mode: FilterMode!): SavedFilter
@deprecated(reason: "default filter now stored in UI config")
"Find a scene by ID or Checksum"
findScene(id: ID, checksum: String): Scene
@ -345,6 +346,7 @@ type Mutation {
saveFilter(input: SaveFilterInput!): SavedFilter!
destroySavedFilter(input: DestroyFilterInput!): Boolean!
setDefaultFilter(input: SetDefaultFilterInput!): Boolean!
@deprecated(reason: "now uses UI config")
"Change general configuration options"
configureGeneral(input: ConfigGeneralInput!): ConfigGeneralResult!

View file

@ -7,7 +7,10 @@ import (
"strconv"
"strings"
"github.com/mitchellh/mapstructure"
"github.com/stashapp/stash/internal/manager/config"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/utils"
)
func (r *mutationResolver) SaveFilter(ctx context.Context, input SaveFilterInput) (ret *models.SavedFilter, err error) {
@ -67,30 +70,48 @@ func (r *mutationResolver) DestroySavedFilter(ctx context.Context, input Destroy
}
func (r *mutationResolver) SetDefaultFilter(ctx context.Context, input SetDefaultFilterInput) (bool, error) {
if err := r.withTxn(ctx, func(ctx context.Context) error {
qb := r.repository.SavedFilter
// deprecated - write to the config in the meantime
config := config.GetInstance()
uiConfig := config.GetUIConfiguration()
if uiConfig == nil {
uiConfig = make(map[string]interface{})
}
m := utils.NestedMap(uiConfig)
if input.FindFilter == nil && input.ObjectFilter == nil && input.UIOptions == nil {
// clearing
def, err := qb.FindDefault(ctx, input.Mode)
if err != nil {
return err
m.Delete("defaultFilters." + strings.ToLower(input.Mode.String()))
config.SetUIConfiguration(m)
if err := config.Write(); err != nil {
return false, err
}
if def != nil {
return qb.Destroy(ctx, def.ID)
return true, nil
}
return nil
}
return qb.SetDefault(ctx, &models.SavedFilter{
Mode: input.Mode,
FindFilter: input.FindFilter,
ObjectFilter: input.ObjectFilter,
UIOptions: input.UIOptions,
subMap := make(map[string]interface{})
d, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
TagName: "json",
WeaklyTypedInput: true,
Result: &subMap,
})
}); err != nil {
if err != nil {
return false, err
}
if err := d.Decode(input); err != nil {
return false, err
}
m.Set("defaultFilters."+strings.ToLower(input.Mode.String()), subMap)
config.SetUIConfiguration(m)
if err := config.Write(); err != nil {
return false, err
}

View file

@ -3,8 +3,12 @@ package api
import (
"context"
"strconv"
"strings"
"github.com/mitchellh/mapstructure"
"github.com/stashapp/stash/internal/manager/config"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/utils"
)
func (r *queryResolver) FindSavedFilter(ctx context.Context, id string) (ret *models.SavedFilter, err error) {
@ -37,11 +41,35 @@ func (r *queryResolver) FindSavedFilters(ctx context.Context, mode *models.Filte
}
func (r *queryResolver) FindDefaultFilter(ctx context.Context, mode models.FilterMode) (ret *models.SavedFilter, err error) {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.SavedFilter.FindDefault(ctx, mode)
return err
}); err != nil {
// deprecated - read from the config in the meantime
config := config.GetInstance()
uiConfig := config.GetUIConfiguration()
if uiConfig == nil {
return nil, nil
}
m := utils.NestedMap(uiConfig)
filterRaw, _ := m.Get("defaultFilters." + strings.ToLower(mode.String()))
if filterRaw == nil {
return nil, nil
}
ret = &models.SavedFilter{}
d, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
TagName: "json",
WeaklyTypedInput: true,
Result: ret,
})
if err != nil {
return nil, err
}
return ret, err
if err := d.Decode(filterRaw); err != nil {
return nil, err
}
return ret, nil
}

View file

@ -7,13 +7,11 @@ type SavedFilterReader interface {
Find(ctx context.Context, id int) (*SavedFilter, error)
FindMany(ctx context.Context, ids []int, ignoreNotFound bool) ([]*SavedFilter, error)
FindByMode(ctx context.Context, mode FilterMode) ([]*SavedFilter, error)
FindDefault(ctx context.Context, mode FilterMode) (*SavedFilter, error)
}
type SavedFilterWriter interface {
Create(ctx context.Context, obj *SavedFilter) error
Update(ctx context.Context, obj *SavedFilter) error
SetDefault(ctx context.Context, obj *SavedFilter) error
Destroy(ctx context.Context, id int) error
}

View file

@ -30,7 +30,7 @@ const (
dbConnTimeout = 30
)
var appSchemaVersion uint = 59
var appSchemaVersion uint = 60
//go:embed migrations/*.sql
var migrationsBox embed.FS

View file

@ -0,0 +1,2 @@
-- no schema changes
-- default filters will be removed in post-migration

View file

@ -0,0 +1,176 @@
package migrations
import (
"context"
"encoding/json"
"fmt"
"os"
"strings"
"time"
"github.com/jmoiron/sqlx"
"github.com/stashapp/stash/internal/manager/config"
"github.com/stashapp/stash/pkg/logger"
"github.com/stashapp/stash/pkg/sqlite"
)
type schema60Migrator struct {
migrator
}
func post60(ctx context.Context, db *sqlx.DB) error {
logger.Info("Running post-migration for schema version 60")
m := schema60Migrator{
migrator: migrator{
db: db,
},
}
return m.migrate(ctx)
}
func (m *schema60Migrator) decodeJSON(s string, v interface{}) {
if s == "" {
return
}
if err := json.Unmarshal([]byte(s), v); err != nil {
logger.Errorf("error decoding json %q: %v", s, err)
}
}
type schema60DefaultFilters map[string]interface{}
func (m *schema60Migrator) migrate(ctx context.Context) error {
// save default filters into the UI config
if err := m.withTxn(ctx, func(tx *sqlx.Tx) error {
query := "SELECT id, mode, find_filter, object_filter, ui_options FROM `saved_filters` WHERE `name` = ''"
rows, err := m.db.Query(query)
if err != nil {
return err
}
defer rows.Close()
defaultFilters := make(schema60DefaultFilters)
for rows.Next() {
var (
id int
mode string
findFilterStr string
objectFilterStr string
uiOptionsStr string
)
if err := rows.Scan(&id, &mode, &findFilterStr, &objectFilterStr, &uiOptionsStr); err != nil {
return err
}
// convert the filters to the correct format
findFilter := make(map[string]interface{})
objectFilter := make(map[string]interface{})
uiOptions := make(map[string]interface{})
m.decodeJSON(findFilterStr, &findFilter)
m.decodeJSON(objectFilterStr, &objectFilter)
m.decodeJSON(uiOptionsStr, &uiOptions)
o := map[string]interface{}{
"mode": mode,
"find_filter": findFilter,
"object_filter": objectFilter,
"ui_options": uiOptions,
}
defaultFilters[strings.ToLower(mode)] = o
}
if err := rows.Err(); err != nil {
return err
}
if err := m.saveDefaultFilters(defaultFilters); err != nil {
return fmt.Errorf("saving default filters: %w", err)
}
// remove the default filters from the database
query = "DELETE FROM `saved_filters` WHERE `name` = ''"
if _, err := m.db.Exec(query); err != nil {
return fmt.Errorf("deleting default filters: %w", err)
}
return nil
}); err != nil {
return err
}
return nil
}
func (m *schema60Migrator) saveDefaultFilters(defaultFilters schema60DefaultFilters) error {
if len(defaultFilters) == 0 {
logger.Debugf("no default filters to save")
return nil
}
// save the default filters into the UI config
config := config.GetInstance()
orgPath := config.GetConfigFile()
if orgPath == "" {
// no config file to migrate (usually in a test or new system)
logger.Debugf("no config file to migrate")
return nil
}
uiConfig := config.GetUIConfiguration()
if uiConfig == nil {
uiConfig = make(map[string]interface{})
}
// if the defaultFilters key already exists, don't overwrite them
if _, found := uiConfig["defaultFilters"]; found {
logger.Warn("defaultFilters already exists in the UI config, skipping migration")
return nil
}
if err := m.backupConfig(orgPath); err != nil {
return fmt.Errorf("backing up config: %w", err)
}
uiConfig["defaultFilters"] = map[string]interface{}(defaultFilters)
config.SetUIConfiguration(uiConfig)
if err := config.Write(); err != nil {
return fmt.Errorf("failed to write config: %w", err)
}
return nil
}
func (m *schema60Migrator) backupConfig(orgPath string) error {
c := config.GetInstance()
// save a backup of the original config file
backupPath := fmt.Sprintf("%s.59.%s", orgPath, time.Now().Format("20060102_150405"))
data, err := c.Marshal()
if err != nil {
return fmt.Errorf("failed to marshal backup config: %w", err)
}
logger.Infof("Backing up config to %s", backupPath)
if err := os.WriteFile(backupPath, data, 0644); err != nil {
return fmt.Errorf("failed to write backup config: %w", err)
}
return nil
}
func init() {
sqlite.RegisterPostMigration(60, post60)
}

View file

@ -141,23 +141,6 @@ func (qb *SavedFilterStore) Update(ctx context.Context, updatedObject *models.Sa
return nil
}
func (qb *SavedFilterStore) SetDefault(ctx context.Context, obj *models.SavedFilter) error {
// find the existing default
existing, err := qb.FindDefault(ctx, obj.Mode)
if err != nil {
return err
}
obj.Name = savedFilterDefaultName
if existing != nil {
obj.ID = existing.ID
return qb.Update(ctx, obj)
}
return qb.Create(ctx, obj)
}
func (qb *SavedFilterStore) Destroy(ctx context.Context, id int) error {
return qb.destroyExisting(ctx, []int{id})
}
@ -258,22 +241,6 @@ func (qb *SavedFilterStore) FindByMode(ctx context.Context, mode models.FilterMo
return ret, nil
}
func (qb *SavedFilterStore) FindDefault(ctx context.Context, mode models.FilterMode) (*models.SavedFilter, error) {
// SELECT * FROM saved_filters WHERE mode = ? AND name = ?
table := qb.table()
sq := qb.selectDataset().Prepared(true).Where(
table.Col("mode").Eq(mode),
table.Col("name").Eq(savedFilterDefaultName),
)
ret, err := qb.get(ctx, sq)
if err != nil && !errors.Is(err, sql.ErrNoRows) {
return nil, err
}
return ret, nil
}
func (qb *SavedFilterStore) All(ctx context.Context) ([]*models.SavedFilter, error) {
return qb.getMany(ctx, qb.selectDataset())
}

View file

@ -96,66 +96,6 @@ func TestSavedFilterDestroy(t *testing.T) {
})
}
func TestSavedFilterFindDefault(t *testing.T) {
withTxn(func(ctx context.Context) error {
def, err := db.SavedFilter.FindDefault(ctx, models.FilterModeScenes)
if err == nil {
assert.Equal(t, savedFilterIDs[savedFilterIdxDefaultScene], def.ID)
}
return err
})
}
func TestSavedFilterSetDefault(t *testing.T) {
filterQ := ""
filterPage := 1
filterPerPage := 40
filterSort := "date"
filterDirection := models.SortDirectionEnumAsc
findFilter := models.FindFilterType{
Q: &filterQ,
Page: &filterPage,
PerPage: &filterPerPage,
Sort: &filterSort,
Direction: &filterDirection,
}
objectFilter := map[string]interface{}{
"test": "foo",
}
uiOptions := map[string]interface{}{
"display_mode": 1,
"zoom_index": 1,
}
withTxn(func(ctx context.Context) error {
err := db.SavedFilter.SetDefault(ctx, &models.SavedFilter{
Mode: models.FilterModeMovies,
FindFilter: &findFilter,
ObjectFilter: objectFilter,
UIOptions: uiOptions,
})
return err
})
var defID int
withTxn(func(ctx context.Context) error {
def, err := db.SavedFilter.FindDefault(ctx, models.FilterModeMovies)
if err == nil {
defID = def.ID
assert.Equal(t, &findFilter, def.FindFilter)
}
return err
})
// destroy it again
withTxn(func(ctx context.Context) error {
return db.SavedFilter.Destroy(ctx, defID)
})
}
// TODO Update
// TODO Destroy
// TODO Find

View file

@ -16,11 +16,6 @@ extend input SaveFilterInput {
ui_options: SavedUIOptions
}
extend input SetDefaultFilterInput {
object_filter: SavedObjectFilter
ui_options: SavedUIOptions
}
extend type Mutation {
configureUI(input: Map, partial: Map): UIConfig!
}

View file

@ -7,7 +7,3 @@ mutation SaveFilter($input: SaveFilterInput!) {
mutation DestroySavedFilter($input: DestroyFilterInput!) {
destroySavedFilter(input: $input)
}
mutation SetDefaultFilter($input: SetDefaultFilterInput!) {
setDefaultFilter(input: $input)
}

View file

@ -9,9 +9,3 @@ query FindSavedFilters($mode: FilterMode) {
...SavedFilterData
}
}
query FindDefaultFilter($mode: FilterMode!) {
findDefaultFilter(mode: $mode) {
...SavedFilterData
}
}

View file

@ -2,16 +2,16 @@ import React from "react";
import { Route, Switch } from "react-router-dom";
import { Helmet } from "react-helmet";
import { useTitleProps } from "src/hooks/title";
import { PersistanceLevel } from "../List/ItemList";
import Gallery from "./GalleryDetails/Gallery";
import GalleryCreate from "./GalleryDetails/GalleryCreate";
import { GalleryList } from "./GalleryList";
import { useScrollToTopOnMount } from "src/hooks/scrollToTop";
import { View } from "../List/views";
const Galleries: React.FC = () => {
useScrollToTopOnMount();
return <GalleryList persistState={PersistanceLevel.ALL} />;
return <GalleryList view={View.Galleries} />;
};
const GalleryRoutes: React.FC = () => {

View file

@ -4,14 +4,12 @@ import { GalleriesCriterion } from "src/models/list-filter/criteria/galleries";
import { ListFilterModel } from "src/models/list-filter/filter";
import { ImageList } from "src/components/Images/ImageList";
import { mutateRemoveGalleryImages } from "src/core/StashService";
import {
showWhenSelected,
PersistanceLevel,
} from "src/components/List/ItemList";
import { showWhenSelected } from "src/components/List/ItemList";
import { useToast } from "src/hooks/Toast";
import { useIntl } from "react-intl";
import { faMinus } from "@fortawesome/free-solid-svg-icons";
import { galleryTitle } from "src/core/galleries";
import { View } from "src/components/List/views";
interface IGalleryDetailsProps {
active: boolean;
@ -102,8 +100,7 @@ export const GalleryImagesPanel: React.FC<IGalleryDetailsProps> = ({
filterHook={filterHook}
alterQuery={active}
extraOperations={otherOperations}
persistState={PersistanceLevel.VIEW}
persistanceKey="galleryimages"
view={View.GalleryImages}
chapters={gallery.chapters}
/>
);

View file

@ -4,11 +4,7 @@ import cloneDeep from "lodash-es/cloneDeep";
import { useHistory } from "react-router-dom";
import Mousetrap from "mousetrap";
import * as GQL from "src/core/generated-graphql";
import {
makeItemList,
PersistanceLevel,
showWhenSelected,
} from "../List/ItemList";
import { makeItemList, showWhenSelected } from "../List/ItemList";
import { ListFilterModel } from "src/models/list-filter/filter";
import { DisplayMode } from "src/models/list-filter/types";
import { queryFindGalleries, useFindGalleries } from "src/core/StashService";
@ -18,6 +14,7 @@ import { DeleteGalleriesDialog } from "./DeleteGalleriesDialog";
import { ExportDialog } from "../Shared/ExportDialog";
import { GalleryListTable } from "./GalleryListTable";
import { GalleryCardGrid } from "./GalleryGridCard";
import { View } from "../List/views";
const GalleryItemList = makeItemList({
filterMode: GQL.FilterMode.Galleries,
@ -32,13 +29,13 @@ const GalleryItemList = makeItemList({
interface IGalleryList {
filterHook?: (filter: ListFilterModel) => ListFilterModel;
persistState?: PersistanceLevel;
view?: View;
alterQuery?: boolean;
}
export const GalleryList: React.FC<IGalleryList> = ({
filterHook,
persistState,
view,
alterQuery,
}) => {
const intl = useIntl();
@ -192,7 +189,7 @@ export const GalleryList: React.FC<IGalleryList> = ({
zoomable
selectable
filterHook={filterHook}
persistState={persistState}
view={view}
alterQuery={alterQuery}
otherOperations={otherOperations}
addKeybinds={addKeybinds}

View file

@ -14,7 +14,6 @@ import { queryFindImages, useFindImages } from "src/core/StashService";
import {
makeItemList,
IItemListOperation,
PersistanceLevel,
showWhenSelected,
} from "../List/ItemList";
import { useLightbox } from "src/hooks/Lightbox/hooks";
@ -31,6 +30,7 @@ import { objectTitle } from "src/core/files";
import TextUtils from "src/utils/text";
import { ConfigurationContext } from "src/hooks/Config";
import { ImageGridCard } from "./ImageGridCard";
import { View } from "../List/views";
interface IImageWallProps {
images: GQL.SlimImageDataFragment[];
@ -270,8 +270,7 @@ const ImageItemList = makeItemList({
interface IImageList {
filterHook?: (filter: ListFilterModel) => ListFilterModel;
persistState?: PersistanceLevel;
persistanceKey?: string;
view?: View;
alterQuery?: boolean;
extraOperations?: IItemListOperation<GQL.FindImagesQueryResult>[];
chapters?: GQL.GalleryChapterDataFragment[];
@ -279,8 +278,7 @@ interface IImageList {
export const ImageList: React.FC<IImageList> = ({
filterHook,
persistState,
persistanceKey,
view,
alterQuery,
extraOperations,
chapters = [],
@ -421,8 +419,7 @@ export const ImageList: React.FC<IImageList> = ({
zoomable
selectable
filterHook={filterHook}
persistState={persistState}
persistanceKey={persistanceKey}
view={view}
alterQuery={alterQuery}
otherOperations={otherOperations}
addKeybinds={addKeybinds}

View file

@ -2,15 +2,15 @@ import React from "react";
import { Route, Switch } from "react-router-dom";
import { Helmet } from "react-helmet";
import { useTitleProps } from "src/hooks/title";
import { PersistanceLevel } from "../List/ItemList";
import Image from "./ImageDetails/Image";
import { ImageList } from "./ImageList";
import { useScrollToTopOnMount } from "src/hooks/scrollToTop";
import { View } from "../List/views";
const Images: React.FC = () => {
useScrollToTopOnMount();
return <ImageList persistState={PersistanceLevel.ALL} />;
return <ImageList view={View.Images} />;
};
const ImageRoutes: React.FC = () => {

View file

@ -19,11 +19,9 @@ import {
} 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";
@ -33,15 +31,8 @@ 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,
}
import { View } from "./views";
import { useDefaultFilter } from "./util";
interface IDataItem {
id: string;
@ -79,8 +70,7 @@ interface IRenderListProps {
}
interface IItemListProps<T extends QueryResult, E extends IDataItem> {
persistState?: PersistanceLevel;
persistanceKey?: string;
view?: View;
defaultSort?: string;
filterHook?: (filter: ListFilterModel) => ListFilterModel;
filterDialog?: (
@ -140,7 +130,7 @@ export function makeItemList<T extends QueryResult, E extends IDataItem>({
filterHook,
onChangePage: _onChangePage,
updateFilter,
persistState,
view,
zoomable,
selectable,
otherOperations,
@ -480,7 +470,7 @@ export function makeItemList<T extends QueryResult, E extends IDataItem>({
filter={filter}
filterOptions={filterOptions}
openFilterDialog={() => setShowEditFilter(true)}
persistState={persistState}
view={view}
/>
<ListOperationButtons
onSelectAll={selectable ? onSelectAll : undefined}
@ -531,8 +521,7 @@ export function makeItemList<T extends QueryResult, E extends IDataItem>({
const ItemList: React.FC<IItemListProps<T, E>> = (props) => {
const {
persistState,
persistanceKey = filterMode,
view,
defaultSort = filterOptions.defaultSortBy,
defaultZoomIndex,
alterQuery = true,
@ -540,7 +529,6 @@ export function makeItemList<T extends QueryResult, E extends IDataItem>({
const history = useHistory();
const location = useLocation();
const [interfaceState, setInterfaceState] = useInterfaceLocalForage();
const [filterInitialised, setFilterInitialised] = useState(false);
const { configuration: config } = useContext(ConfigurationContext);
@ -550,35 +538,11 @@ export function makeItemList<T extends QueryResult, E extends IDataItem>({
() => 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 { defaultFilter, loading: defaultFilterLoading } = useDefaultFilter(
filterMode,
view
);
const { data: defaultFilter, loading: defaultFilterLoading } =
useFindDefaultFilter(filterMode);
const updateQueryParams = useCallback(
(newFilter: ListFilterModel) => {
if (!alterQuery) return;
@ -593,11 +557,8 @@ export function makeItemList<T extends QueryResult, E extends IDataItem>({
(newFilter: ListFilterModel) => {
setFilter(newFilter);
updateQueryParams(newFilter);
if (persistState === PersistanceLevel.VIEW) {
updateSavedFilter(newFilter);
}
},
[persistState, updateSavedFilter, updateQueryParams]
[updateQueryParams]
);
// 'Startup' hook, initialises the filters
@ -605,53 +566,28 @@ export function makeItemList<T extends QueryResult, E extends IDataItem>({
// Only run once
if (filterInitialised) return;
let newFilter = new ListFilterModel(
filterMode,
config,
defaultSort,
defaultDisplayMode,
defaultZoomIndex
);
let newFilter = new ListFilterModel(filterMode, config, defaultZoomIndex);
let loadDefault = true;
if (alterQuery && location.search) {
loadDefault = false;
newFilter.configureFromQueryString(location.search);
}
if (persistState === PersistanceLevel.ALL) {
if (view) {
// 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
}
if (defaultFilter) {
newFilter = defaultFilter.clone();
// #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);
@ -664,12 +600,10 @@ export function makeItemList<T extends QueryResult, E extends IDataItem>({
defaultDisplayMode,
defaultZoomIndex,
alterQuery,
persistState,
view,
updateQueryParams,
defaultFilter,
defaultFilterLoading,
interfaceState,
persistanceKey,
]);
// This hook runs on every page location change (ie navigation),

View file

@ -1,11 +1,5 @@
import cloneDeep from "lodash-es/cloneDeep";
import React, {
HTMLAttributes,
useCallback,
useEffect,
useRef,
useState,
} from "react";
import React, { useCallback, useEffect, useRef, useState } from "react";
import cx from "classnames";
import Mousetrap from "mousetrap";
import { SortDirectionEnum } from "src/core/generated-graphql";
@ -27,10 +21,8 @@ import { ListFilterModel } from "src/models/list-filter/filter";
import useFocus from "src/utils/focus";
import { ListFilterOptions } from "src/models/list-filter/filter-options";
import { FormattedMessage, useIntl } from "react-intl";
import { PersistanceLevel } from "./ItemList";
import { SavedFilterList } from "./SavedFilterList";
import { SavedFilterDropdown } from "./SavedFilterList";
import {
faBookmark,
faCaretDown,
faCaretUp,
faCheck,
@ -39,12 +31,13 @@ import {
} from "@fortawesome/free-solid-svg-icons";
import { FilterButton } from "./Filters/FilterButton";
import { useDebounce } from "src/hooks/debounce";
import { View } from "./views";
interface IListFilterProps {
onFilterUpdate: (newFilter: ListFilterModel) => void;
filter: ListFilterModel;
filterOptions: ListFilterOptions;
persistState?: PersistanceLevel;
view?: View;
openFilterDialog: () => void;
}
@ -55,7 +48,7 @@ export const ListFilter: React.FC<IListFilterProps> = ({
filter,
filterOptions,
openFilterDialog,
persistState,
view,
}) => {
const [customPageSizeShowing, setCustomPageSizeShowing] = useState(false);
const [queryRef, setQueryFocus] = useFocus();
@ -191,22 +184,6 @@ export const ListFilter: React.FC<IListFilterProps> = ({
));
}
const SavedFilterDropdown = React.forwardRef<
HTMLDivElement,
HTMLAttributes<HTMLDivElement>
>(({ style, className }: HTMLAttributes<HTMLDivElement>, ref) => (
<div ref={ref} style={style} className={className}>
<SavedFilterList
filter={filter}
onSetFilter={(f) => {
onFilterUpdate(f);
}}
persistState={persistState}
/>
</div>
));
SavedFilterDropdown.displayName = "SavedFilterDropdown";
function render() {
const currentSortBy = filterOptions.sortByOptions.find(
(o) => o.value === filter.sortBy
@ -257,24 +234,13 @@ export const ListFilter: React.FC<IListFilterProps> = ({
</div>
<ButtonGroup className="mr-2 mb-2">
<Dropdown>
<OverlayTrigger
placement="top"
overlay={
<Tooltip id="filter-tooltip">
<FormattedMessage id="search_filter.saved_filters" />
</Tooltip>
}
>
<Dropdown.Toggle variant="secondary">
<Icon icon={faBookmark} />
</Dropdown.Toggle>
</OverlayTrigger>
<Dropdown.Menu
as={SavedFilterDropdown}
className="saved-filter-list-menu"
<SavedFilterDropdown
filter={filter}
onSetFilter={(f) => {
onFilterUpdate(f);
}}
view={view}
/>
</Dropdown>
<OverlayTrigger
placement="top"
overlay={

View file

@ -1,4 +1,4 @@
import React, { useState } from "react";
import React, { HTMLAttributes, useState } from "react";
import {
Button,
ButtonGroup,
@ -10,30 +10,30 @@ import {
Tooltip,
} from "react-bootstrap";
import {
useConfigureUI,
useFindSavedFilters,
useSavedFilterDestroy,
useSaveFilter,
useSetDefaultFilter,
} from "src/core/StashService";
import { useToast } from "src/hooks/Toast";
import { ListFilterModel } from "src/models/list-filter/filter";
import { SavedFilterDataFragment } from "src/core/generated-graphql";
import { PersistanceLevel } from "./ItemList";
import { View } from "./views";
import { FormattedMessage, useIntl } from "react-intl";
import { Icon } from "../Shared/Icon";
import { LoadingIndicator } from "../Shared/LoadingIndicator";
import { faSave, faTimes } from "@fortawesome/free-solid-svg-icons";
import { faBookmark, faSave, faTimes } from "@fortawesome/free-solid-svg-icons";
interface ISavedFilterListProps {
filter: ListFilterModel;
onSetFilter: (f: ListFilterModel) => void;
persistState?: PersistanceLevel;
view?: View;
}
export const SavedFilterList: React.FC<ISavedFilterListProps> = ({
filter,
onSetFilter,
persistState,
view,
}) => {
const Toast = useToast();
const intl = useIntl();
@ -51,7 +51,7 @@ export const SavedFilterList: React.FC<ISavedFilterListProps> = ({
const [saveFilter] = useSaveFilter();
const [destroyFilter] = useSavedFilterDestroy();
const [setDefaultFilter] = useSetDefaultFilter();
const [saveUI] = useConfigureUI();
const savedFilters = data?.findSavedFilters ?? [];
@ -127,20 +127,28 @@ export const SavedFilterList: React.FC<ISavedFilterListProps> = ({
}
async function onSetDefaultFilter() {
if (!view) {
return;
}
const filterCopy = filter.clone();
try {
setSaving(true);
await setDefaultFilter({
await saveUI({
variables: {
input: {
partial: {
defaultFilters: {
[view.toString()]: {
mode: filter.mode,
find_filter: filterCopy.makeFindFilter(),
object_filter: filterCopy.makeSavedFilter(),
ui_options: filterCopy.makeSavedUIOptions(),
},
},
},
},
});
Toast.success(
@ -302,17 +310,19 @@ export const SavedFilterList: React.FC<ISavedFilterListProps> = ({
}
function maybeRenderSetDefaultButton() {
if (persistState === PersistanceLevel.ALL) {
if (view) {
return (
<div className="mt-1">
<Button
<Dropdown.Item
as={Button}
title={intl.formatMessage({ id: "actions.set_as_default" })}
className="set-as-default-button"
variant="secondary"
size="sm"
onClick={() => onSetDefaultFilter()}
>
{intl.formatMessage({ id: "actions.set_as_default" })}
</Button>
</Dropdown.Item>
</div>
);
}
@ -357,3 +367,36 @@ export const SavedFilterList: React.FC<ISavedFilterListProps> = ({
</>
);
};
export const SavedFilterDropdown: React.FC<ISavedFilterListProps> = (props) => {
const SavedFilterDropdownRef = React.forwardRef<
HTMLDivElement,
HTMLAttributes<HTMLDivElement>
>(({ style, className }: HTMLAttributes<HTMLDivElement>, ref) => (
<div ref={ref} style={style} className={className}>
<SavedFilterList {...props} />
</div>
));
SavedFilterDropdownRef.displayName = "SavedFilterDropdown";
return (
<Dropdown>
<OverlayTrigger
placement="top"
overlay={
<Tooltip id="filter-tooltip">
<FormattedMessage id="search_filter.saved_filters" />
</Tooltip>
}
>
<Dropdown.Toggle variant="secondary">
<Icon icon={faBookmark} />
</Dropdown.Toggle>
</OverlayTrigger>
<Dropdown.Menu
as={SavedFilterDropdownRef}
className="saved-filter-list-menu"
/>
</Dropdown>
);
};

View file

@ -86,6 +86,8 @@ input[type="range"].zoom-slider {
.set-as-default-button {
float: right;
margin-right: 0.5rem;
padding: 0.25rem 0.5rem;
width: auto;
}
.LoadingIndicator {

View file

@ -0,0 +1,32 @@
import { useContext, useMemo } from "react";
import { ListFilterModel } from "src/models/list-filter/filter";
import * as GQL from "src/core/generated-graphql";
import { ConfigurationContext } from "src/hooks/Config";
import { View } from "./views";
export function useDefaultFilter(mode: GQL.FilterMode, view?: View) {
const emptyFilter = useMemo(() => new ListFilterModel(mode), [mode]);
const { configuration: config, loading } = useContext(ConfigurationContext);
const defaultFilter = useMemo(() => {
if (view && config?.ui.defaultFilters?.[view]) {
const savedFilter = config.ui.defaultFilters[view]!;
const newFilter = emptyFilter.clone();
newFilter.currentPage = 1;
try {
newFilter.configureFromSavedFilter(savedFilter);
} catch (err) {
console.log(err);
// ignore
}
// #1507 - reset random seed when loaded
newFilter.randomSeed = -1;
return newFilter;
}
}, [view, config?.ui.defaultFilters, emptyFilter]);
const retFilter = loading ? undefined : defaultFilter ?? emptyFilter;
return { defaultFilter: retFilter, loading };
}

View file

@ -0,0 +1,34 @@
export enum View {
Galleries = "galleries",
Images = "images",
Scenes = "scenes",
Movies = "movies",
Performers = "performers",
Tags = "tags",
SceneMarkers = "scene_markers",
Studios = "studios",
TagMarkers = "tag_markers",
TagGalleries = "tag_galleries",
TagScenes = "tag_scenes",
TagImages = "tag_images",
TagPerformers = "tag_performers",
PerformerScenes = "performer_scenes",
PerformerGalleries = "performer_galleries",
PerformerImages = "performer_images",
PerformerMovies = "performer_movies",
PerformerAppearsWith = "performer_appears_with",
StudioGalleries = "studio_galleries",
StudioImages = "studio_images",
GalleryImages = "gallery_images",
StudioScenes = "studio_scenes",
StudioMovies = "studio_movies",
StudioPerformers = "studio_performers",
StudioChildren = "studio_children",
MovieScenes = "movie_scenes",
}

View file

@ -3,6 +3,7 @@ import * as GQL from "src/core/generated-graphql";
import { MoviesCriterion } from "src/models/list-filter/criteria/movies";
import { ListFilterModel } from "src/models/list-filter/filter";
import { SceneList } from "src/components/Scenes/SceneList";
import { View } from "src/components/List/views";
interface IMovieScenesPanel {
active: boolean;
@ -51,6 +52,7 @@ export const MovieScenesPanel: React.FC<IMovieScenesPanel> = ({
filterHook={filterHook}
defaultSort="movie_scene_number"
alterQuery={active}
view={View.MovieScenes}
/>
);
}

View file

@ -11,15 +11,12 @@ import {
useFindMovies,
useMoviesDestroy,
} from "src/core/StashService";
import {
makeItemList,
PersistanceLevel,
showWhenSelected,
} from "../List/ItemList";
import { makeItemList, showWhenSelected } from "../List/ItemList";
import { ExportDialog } from "../Shared/ExportDialog";
import { DeleteEntityDialog } from "../Shared/DeleteEntityDialog";
import { MovieCardGrid } from "./MovieCardGrid";
import { EditMoviesDialog } from "./EditMoviesDialog";
import { View } from "../List/views";
const MovieItemList = makeItemList({
filterMode: GQL.FilterMode.Movies,
@ -34,10 +31,15 @@ const MovieItemList = makeItemList({
interface IMovieList {
filterHook?: (filter: ListFilterModel) => ListFilterModel;
view?: View;
alterQuery?: boolean;
}
export const MovieList: React.FC<IMovieList> = ({ filterHook, alterQuery }) => {
export const MovieList: React.FC<IMovieList> = ({
filterHook,
alterQuery,
view,
}) => {
const intl = useIntl();
const history = useHistory();
const [isExportDialogOpen, setIsExportDialogOpen] = useState(false);
@ -175,7 +177,7 @@ export const MovieList: React.FC<IMovieList> = ({ filterHook, alterQuery }) => {
<MovieItemList
selectable
filterHook={filterHook}
persistState={PersistanceLevel.ALL}
view={view}
alterQuery={alterQuery}
otherOperations={otherOperations}
addKeybinds={addKeybinds}

View file

@ -6,11 +6,12 @@ import Movie from "./MovieDetails/Movie";
import MovieCreate from "./MovieDetails/MovieCreate";
import { MovieList } from "./MovieList";
import { useScrollToTopOnMount } from "src/hooks/scrollToTop";
import { View } from "../List/views";
const Movies: React.FC = () => {
useScrollToTopOnMount();
return <MovieList />;
return <MovieList view={View.Movies} />;
};
const MovieRoutes: React.FC = () => {

View file

@ -2,6 +2,7 @@ import React from "react";
import * as GQL from "src/core/generated-graphql";
import { GalleryList } from "src/components/Galleries/GalleryList";
import { usePerformerFilterHook } from "src/core/performers";
import { View } from "src/components/List/views";
interface IPerformerDetailsProps {
active: boolean;
@ -13,5 +14,11 @@ export const PerformerGalleriesPanel: React.FC<IPerformerDetailsProps> = ({
performer,
}) => {
const filterHook = usePerformerFilterHook(performer);
return <GalleryList filterHook={filterHook} alterQuery={active} />;
return (
<GalleryList
filterHook={filterHook}
alterQuery={active}
view={View.PerformerGalleries}
/>
);
};

View file

@ -2,6 +2,7 @@ import React from "react";
import * as GQL from "src/core/generated-graphql";
import { ImageList } from "src/components/Images/ImageList";
import { usePerformerFilterHook } from "src/core/performers";
import { View } from "src/components/List/views";
interface IPerformerImagesPanel {
active: boolean;
@ -13,5 +14,11 @@ export const PerformerImagesPanel: React.FC<IPerformerImagesPanel> = ({
performer,
}) => {
const filterHook = usePerformerFilterHook(performer);
return <ImageList filterHook={filterHook} alterQuery={active} />;
return (
<ImageList
filterHook={filterHook}
alterQuery={active}
view={View.PerformerImages}
/>
);
};

View file

@ -2,6 +2,7 @@ import React from "react";
import * as GQL from "src/core/generated-graphql";
import { MovieList } from "src/components/Movies/MovieList";
import { usePerformerFilterHook } from "src/core/performers";
import { View } from "src/components/List/views";
interface IPerformerDetailsProps {
active: boolean;
@ -13,5 +14,11 @@ export const PerformerMoviesPanel: React.FC<IPerformerDetailsProps> = ({
performer,
}) => {
const filterHook = usePerformerFilterHook(performer);
return <MovieList filterHook={filterHook} alterQuery={active} />;
return (
<MovieList
filterHook={filterHook}
alterQuery={active}
view={View.PerformerMovies}
/>
);
};

View file

@ -2,6 +2,7 @@ import React from "react";
import * as GQL from "src/core/generated-graphql";
import { SceneList } from "src/components/Scenes/SceneList";
import { usePerformerFilterHook } from "src/core/performers";
import { View } from "src/components/List/views";
interface IPerformerDetailsProps {
active: boolean;
@ -13,5 +14,11 @@ export const PerformerScenesPanel: React.FC<IPerformerDetailsProps> = ({
performer,
}) => {
const filterHook = usePerformerFilterHook(performer);
return <SceneList filterHook={filterHook} alterQuery={active} />;
return (
<SceneList
filterHook={filterHook}
alterQuery={active}
view={View.PerformerScenes}
/>
);
};

View file

@ -2,6 +2,7 @@ import React from "react";
import * as GQL from "src/core/generated-graphql";
import { PerformerList } from "src/components/Performers/PerformerList";
import { usePerformerFilterHook } from "src/core/performers";
import { View } from "src/components/List/views";
interface IPerformerDetailsProps {
active: boolean;
@ -28,6 +29,7 @@ export const PerformerAppearsWithPanel: React.FC<IPerformerDetailsProps> = ({
filterHook={filterHook}
extraCriteria={extraCriteria}
alterQuery={active}
view={View.PerformerAppearsWith}
/>
);
};

View file

@ -9,11 +9,7 @@ import {
useFindPerformers,
usePerformersDestroy,
} from "src/core/StashService";
import {
makeItemList,
PersistanceLevel,
showWhenSelected,
} from "../List/ItemList";
import { makeItemList, showWhenSelected } from "../List/ItemList";
import { ListFilterModel } from "src/models/list-filter/filter";
import { DisplayMode } from "src/models/list-filter/types";
import { PerformerTagger } from "../Tagger/performers/PerformerTagger";
@ -25,6 +21,7 @@ import { EditPerformersDialog } from "./EditPerformersDialog";
import { cmToImperial, cmToInches, kgToLbs } from "src/utils/units";
import TextUtils from "src/utils/text";
import { PerformerCardGrid } from "./PerformerCardGrid";
import { View } from "../List/views";
const PerformerItemList = makeItemList({
filterMode: GQL.FilterMode.Performers,
@ -162,14 +159,14 @@ export const FormatPenisLength = (penis_length?: number | null) => {
interface IPerformerList {
filterHook?: (filter: ListFilterModel) => ListFilterModel;
persistState?: PersistanceLevel;
view?: View;
alterQuery?: boolean;
extraCriteria?: IPerformerCardExtraCriteria;
}
export const PerformerList: React.FC<IPerformerList> = ({
filterHook,
persistState,
view,
alterQuery,
extraCriteria,
}) => {
@ -325,7 +322,7 @@ export const PerformerList: React.FC<IPerformerList> = ({
<PerformerItemList
selectable
filterHook={filterHook}
persistState={persistState}
view={view}
alterQuery={alterQuery}
otherOperations={otherOperations}
addKeybinds={addKeybinds}

View file

@ -2,16 +2,16 @@ import React from "react";
import { Route, Switch } from "react-router-dom";
import { Helmet } from "react-helmet";
import { useTitleProps } from "src/hooks/title";
import { PersistanceLevel } from "../List/ItemList";
import Performer from "./PerformerDetails/Performer";
import PerformerCreate from "./PerformerDetails/PerformerCreate";
import { PerformerList } from "./PerformerList";
import { useScrollToTopOnMount } from "src/hooks/scrollToTop";
import { View } from "../List/views";
const Performers: React.FC = () => {
useScrollToTopOnMount();
return <PerformerList persistState={PersistanceLevel.ALL} />;
return <PerformerList view={View.Performers} />;
};
const PerformerRoutes: React.FC = () => {

View file

@ -5,11 +5,7 @@ import { useHistory } from "react-router-dom";
import Mousetrap from "mousetrap";
import * as GQL from "src/core/generated-graphql";
import { queryFindScenes, useFindScenes } from "src/core/StashService";
import {
makeItemList,
PersistanceLevel,
showWhenSelected,
} from "../List/ItemList";
import { makeItemList, showWhenSelected } from "../List/ItemList";
import { ListFilterModel } from "src/models/list-filter/filter";
import { DisplayMode } from "src/models/list-filter/types";
import { Tagger } from "../Tagger/scenes/SceneTagger";
@ -28,6 +24,7 @@ import { faPlay } from "@fortawesome/free-solid-svg-icons";
import { SceneMergeModal } from "./SceneMergeDialog";
import { objectTitle } from "src/core/files";
import TextUtils from "src/utils/text";
import { View } from "../List/views";
const SceneItemList = makeItemList({
filterMode: GQL.FilterMode.Scenes,
@ -78,14 +75,14 @@ const SceneItemList = makeItemList({
interface ISceneList {
filterHook?: (filter: ListFilterModel) => ListFilterModel;
defaultSort?: string;
persistState?: PersistanceLevel;
view?: View;
alterQuery?: boolean;
}
export const SceneList: React.FC<ISceneList> = ({
filterHook,
defaultSort,
persistState,
view,
alterQuery,
}) => {
const intl = useIntl();
@ -357,7 +354,7 @@ export const SceneList: React.FC<ISceneList> = ({
zoomable
selectable
filterHook={filterHook}
persistState={persistState}
view={view}
alterQuery={alterQuery}
otherOperations={otherOperations}
addKeybinds={addKeybinds}

View file

@ -9,10 +9,11 @@ import {
useFindSceneMarkers,
} from "src/core/StashService";
import NavUtils from "src/utils/navigation";
import { makeItemList, PersistanceLevel } from "../List/ItemList";
import { makeItemList } from "../List/ItemList";
import { ListFilterModel } from "src/models/list-filter/filter";
import { DisplayMode } from "src/models/list-filter/types";
import { MarkerWallPanel } from "../Wall/WallPanel";
import { View } from "../List/views";
const SceneMarkerItemList = makeItemList({
filterMode: GQL.FilterMode.SceneMarkers,
@ -27,11 +28,13 @@ const SceneMarkerItemList = makeItemList({
interface ISceneMarkerList {
filterHook?: (filter: ListFilterModel) => ListFilterModel;
view?: View;
alterQuery?: boolean;
}
export const SceneMarkerList: React.FC<ISceneMarkerList> = ({
filterHook,
view,
alterQuery,
}) => {
const intl = useIntl();
@ -96,7 +99,7 @@ export const SceneMarkerList: React.FC<ISceneMarkerList> = ({
return (
<SceneMarkerItemList
filterHook={filterHook}
persistState={PersistanceLevel.ALL}
view={view}
alterQuery={alterQuery}
otherOperations={otherOperations}
addKeybinds={addKeybinds}

View file

@ -2,9 +2,9 @@ import React from "react";
import { Route, Switch } from "react-router-dom";
import { Helmet } from "react-helmet";
import { useTitleProps } from "src/hooks/title";
import { PersistanceLevel } from "../List/ItemList";
import { lazyComponent } from "src/utils/lazyComponent";
import { useScrollToTopOnMount } from "src/hooks/scrollToTop";
import { View } from "../List/views";
const SceneList = lazyComponent(() => import("./SceneList"));
const SceneMarkerList = lazyComponent(() => import("./SceneMarkerList"));
@ -14,7 +14,7 @@ const SceneCreate = lazyComponent(() => import("./SceneDetails/SceneCreate"));
const Scenes: React.FC = () => {
useScrollToTopOnMount();
return <SceneList persistState={PersistanceLevel.ALL} />;
return <SceneList view={View.Scenes} />;
};
const SceneMarkers: React.FC = () => {
@ -24,7 +24,7 @@ const SceneMarkers: React.FC = () => {
return (
<>
<Helmet {...titleProps} />
<SceneMarkerList />
<SceneMarkerList view={View.SceneMarkers} />
</>
);
};

View file

@ -3,6 +3,7 @@ import * as GQL from "src/core/generated-graphql";
import { ParentStudiosCriterion } from "src/models/list-filter/criteria/studios";
import { ListFilterModel } from "src/models/list-filter/filter";
import { StudioList } from "../StudioList";
import { View } from "src/components/List/views";
interface IStudioChildrenPanel {
active: boolean;
@ -45,5 +46,12 @@ export const StudioChildrenPanel: React.FC<IStudioChildrenPanel> = ({
return filter;
}
return <StudioList fromParent filterHook={filterHook} alterQuery={active} />;
return (
<StudioList
fromParent
filterHook={filterHook}
alterQuery={active}
view={View.StudioChildren}
/>
);
};

View file

@ -2,6 +2,7 @@ import React from "react";
import * as GQL from "src/core/generated-graphql";
import { GalleryList } from "src/components/Galleries/GalleryList";
import { useStudioFilterHook } from "src/core/studios";
import { View } from "src/components/List/views";
interface IStudioGalleriesPanel {
active: boolean;
@ -13,5 +14,11 @@ export const StudioGalleriesPanel: React.FC<IStudioGalleriesPanel> = ({
studio,
}) => {
const filterHook = useStudioFilterHook(studio);
return <GalleryList filterHook={filterHook} alterQuery={active} />;
return (
<GalleryList
filterHook={filterHook}
alterQuery={active}
view={View.StudioGalleries}
/>
);
};

View file

@ -2,6 +2,7 @@ import React from "react";
import * as GQL from "src/core/generated-graphql";
import { useStudioFilterHook } from "src/core/studios";
import { ImageList } from "src/components/Images/ImageList";
import { View } from "src/components/List/views";
interface IStudioImagesPanel {
active: boolean;
@ -13,5 +14,11 @@ export const StudioImagesPanel: React.FC<IStudioImagesPanel> = ({
studio,
}) => {
const filterHook = useStudioFilterHook(studio);
return <ImageList filterHook={filterHook} alterQuery={active} />;
return (
<ImageList
filterHook={filterHook}
alterQuery={active}
view={View.StudioImages}
/>
);
};

View file

@ -2,6 +2,7 @@ import React from "react";
import * as GQL from "src/core/generated-graphql";
import { MovieList } from "src/components/Movies/MovieList";
import { useStudioFilterHook } from "src/core/studios";
import { View } from "src/components/List/views";
interface IStudioMoviesPanel {
active: boolean;
@ -13,5 +14,11 @@ export const StudioMoviesPanel: React.FC<IStudioMoviesPanel> = ({
studio,
}) => {
const filterHook = useStudioFilterHook(studio);
return <MovieList filterHook={filterHook} alterQuery={active} />;
return (
<MovieList
filterHook={filterHook}
alterQuery={active}
view={View.StudioMovies}
/>
);
};

View file

@ -3,6 +3,7 @@ import * as GQL from "src/core/generated-graphql";
import { useStudioFilterHook } from "src/core/studios";
import { PerformerList } from "src/components/Performers/PerformerList";
import { StudiosCriterion } from "src/models/list-filter/criteria/studios";
import { View } from "src/components/List/views";
interface IStudioPerformersPanel {
active: boolean;
@ -34,6 +35,7 @@ export const StudioPerformersPanel: React.FC<IStudioPerformersPanel> = ({
filterHook={filterHook}
extraCriteria={extraCriteria}
alterQuery={active}
view={View.StudioPerformers}
/>
);
};

View file

@ -2,6 +2,7 @@ import React from "react";
import * as GQL from "src/core/generated-graphql";
import { SceneList } from "src/components/Scenes/SceneList";
import { useStudioFilterHook } from "src/core/studios";
import { View } from "src/components/List/views";
interface IStudioScenesPanel {
active: boolean;
@ -13,5 +14,11 @@ export const StudioScenesPanel: React.FC<IStudioScenesPanel> = ({
studio,
}) => {
const filterHook = useStudioFilterHook(studio);
return <SceneList filterHook={filterHook} alterQuery={active} />;
return (
<SceneList
filterHook={filterHook}
alterQuery={active}
view={View.StudioScenes}
/>
);
};

View file

@ -9,17 +9,14 @@ import {
useFindStudios,
useStudiosDestroy,
} from "src/core/StashService";
import {
makeItemList,
PersistanceLevel,
showWhenSelected,
} from "../List/ItemList";
import { makeItemList, showWhenSelected } from "../List/ItemList";
import { ListFilterModel } from "src/models/list-filter/filter";
import { DisplayMode } from "src/models/list-filter/types";
import { ExportDialog } from "../Shared/ExportDialog";
import { DeleteEntityDialog } from "../Shared/DeleteEntityDialog";
import { StudioTagger } from "../Tagger/studios/StudioTagger";
import { StudioCardGrid } from "./StudioCardGrid";
import { View } from "../List/views";
const StudioItemList = makeItemList({
filterMode: GQL.FilterMode.Studios,
@ -35,12 +32,14 @@ const StudioItemList = makeItemList({
interface IStudioList {
fromParent?: boolean;
filterHook?: (filter: ListFilterModel) => ListFilterModel;
view?: View;
alterQuery?: boolean;
}
export const StudioList: React.FC<IStudioList> = ({
fromParent,
filterHook,
view,
alterQuery,
}) => {
const intl = useIntl();
@ -181,7 +180,7 @@ export const StudioList: React.FC<IStudioList> = ({
<StudioItemList
selectable
filterHook={filterHook}
persistState={fromParent ? PersistanceLevel.NONE : PersistanceLevel.ALL}
view={view}
alterQuery={alterQuery}
otherOperations={otherOperations}
addKeybinds={addKeybinds}

View file

@ -6,11 +6,12 @@ import Studio from "./StudioDetails/Studio";
import StudioCreate from "./StudioDetails/StudioCreate";
import { StudioList } from "./StudioList";
import { useScrollToTopOnMount } from "src/hooks/scrollToTop";
import { View } from "../List/views";
const Studios: React.FC = () => {
useScrollToTopOnMount();
return <StudioList />;
return <StudioList view={View.Studios} />;
};
const StudioRoutes: React.FC = () => {

View file

@ -2,6 +2,7 @@ import React from "react";
import * as GQL from "src/core/generated-graphql";
import { useTagFilterHook } from "src/core/tags";
import { GalleryList } from "src/components/Galleries/GalleryList";
import { View } from "src/components/List/views";
interface ITagGalleriesPanel {
active: boolean;
@ -13,5 +14,11 @@ export const TagGalleriesPanel: React.FC<ITagGalleriesPanel> = ({
tag,
}) => {
const filterHook = useTagFilterHook(tag);
return <GalleryList filterHook={filterHook} alterQuery={active} />;
return (
<GalleryList
filterHook={filterHook}
alterQuery={active}
view={View.TagGalleries}
/>
);
};

View file

@ -2,6 +2,7 @@ import React from "react";
import * as GQL from "src/core/generated-graphql";
import { useTagFilterHook } from "src/core/tags";
import { ImageList } from "src/components/Images/ImageList";
import { View } from "src/components/List/views";
interface ITagImagesPanel {
active: boolean;
@ -10,5 +11,11 @@ interface ITagImagesPanel {
export const TagImagesPanel: React.FC<ITagImagesPanel> = ({ active, tag }) => {
const filterHook = useTagFilterHook(tag);
return <ImageList filterHook={filterHook} alterQuery={active} />;
return (
<ImageList
filterHook={filterHook}
alterQuery={active}
view={View.TagImages}
/>
);
};

View file

@ -6,6 +6,7 @@ import {
TagsCriterionOption,
} from "src/models/list-filter/criteria/tags";
import { SceneMarkerList } from "src/components/Scenes/SceneMarkerList";
import { View } from "src/components/List/views";
interface ITagMarkersPanel {
active: boolean;
@ -52,5 +53,11 @@ export const TagMarkersPanel: React.FC<ITagMarkersPanel> = ({
return filter;
}
return <SceneMarkerList filterHook={filterHook} alterQuery={active} />;
return (
<SceneMarkerList
filterHook={filterHook}
alterQuery={active}
view={View.TagMarkers}
/>
);
};

View file

@ -2,6 +2,7 @@ import React from "react";
import * as GQL from "src/core/generated-graphql";
import { useTagFilterHook } from "src/core/tags";
import { PerformerList } from "src/components/Performers/PerformerList";
import { View } from "src/components/List/views";
interface ITagPerformersPanel {
active: boolean;
@ -13,5 +14,11 @@ export const TagPerformersPanel: React.FC<ITagPerformersPanel> = ({
tag,
}) => {
const filterHook = useTagFilterHook(tag);
return <PerformerList filterHook={filterHook} alterQuery={active} />;
return (
<PerformerList
filterHook={filterHook}
alterQuery={active}
view={View.TagPerformers}
/>
);
};

View file

@ -2,6 +2,7 @@ import React from "react";
import * as GQL from "src/core/generated-graphql";
import { SceneList } from "src/components/Scenes/SceneList";
import { useTagFilterHook } from "src/core/tags";
import { View } from "src/components/List/views";
interface ITagScenesPanel {
active: boolean;
@ -10,5 +11,11 @@ interface ITagScenesPanel {
export const TagScenesPanel: React.FC<ITagScenesPanel> = ({ active, tag }) => {
const filterHook = useTagFilterHook(tag);
return <SceneList filterHook={filterHook} alterQuery={active} />;
return (
<SceneList
filterHook={filterHook}
alterQuery={active}
view={View.TagScenes}
/>
);
};

View file

@ -3,11 +3,7 @@ import cloneDeep from "lodash-es/cloneDeep";
import Mousetrap from "mousetrap";
import { ListFilterModel } from "src/models/list-filter/filter";
import { DisplayMode } from "src/models/list-filter/types";
import {
makeItemList,
PersistanceLevel,
showWhenSelected,
} from "../List/ItemList";
import { makeItemList, showWhenSelected } from "../List/ItemList";
import { Button } from "react-bootstrap";
import { Link, useHistory } from "react-router-dom";
import * as GQL from "src/core/generated-graphql";
@ -29,6 +25,7 @@ import { tagRelationHook } from "../../core/tags";
import { faTrashAlt } from "@fortawesome/free-solid-svg-icons";
import { TagCardGrid } from "./TagCardGrid";
import { EditTagsDialog } from "./EditTagsDialog";
import { View } from "../List/views";
interface ITagList {
filterHook?: (filter: ListFilterModel) => ListFilterModel;
@ -363,7 +360,7 @@ export const TagList: React.FC<ITagList> = ({ filterHook, alterQuery }) => {
zoomable
defaultZoomIndex={0}
filterHook={filterHook}
persistState={PersistanceLevel.ALL}
view={View.Tags}
alterQuery={alterQuery}
otherOperations={otherOperations}
addKeybinds={addKeybinds}

View file

@ -452,11 +452,6 @@ export const useFindSavedFilters = (mode?: GQL.FilterMode) =>
variables: { mode },
});
export const useFindDefaultFilter = (mode: GQL.FilterMode) =>
GQL.useFindDefaultFilterQuery({
variables: { mode },
});
/// Object Mutations
// Increases/decreases the given field of the Stats query by diff
@ -1956,15 +1951,6 @@ export const useSaveFilter = () =>
},
});
export const useSetDefaultFilter = () =>
GQL.useSetDefaultFilterMutation({
update(cache, result) {
if (!result.data?.setDefaultFilter) return;
evictQueries(cache, [GQL.FindDefaultFilterDocument]);
},
});
export const useSavedFilterDestroy = () =>
GQL.useDestroySavedFilterMutation({
update(cache, result, { variables }) {
@ -1972,8 +1958,6 @@ export const useSavedFilterDestroy = () =>
const obj = { __typename: "SavedFilter", id: variables.input.id };
deleteObject(cache, obj, GQL.FindSavedFilterDocument);
evictQueries(cache, [GQL.FindDefaultFilterDocument]);
},
});

View file

@ -2,7 +2,12 @@ import { IntlShape } from "react-intl";
import { ITypename } from "src/utils/data";
import { ImageWallOptions } from "src/utils/imageWall";
import { RatingSystemOptions } from "src/utils/rating";
import { FilterMode, SortDirectionEnum } from "./generated-graphql";
import {
FilterMode,
SavedFilterDataFragment,
SortDirectionEnum,
} from "./generated-graphql";
import { View } from "src/components/List/views";
// NOTE: double capitals aren't converted correctly in the backend
@ -25,6 +30,10 @@ export interface ICustomFilter extends ITypename {
direction: SortDirectionEnum;
}
export type DefaultFilters = {
[P in View]?: SavedFilterDataFragment;
};
export type FrontPageContent = ISavedFilterRow | ICustomFilter;
export const defaultMaxOptionsShown = 200;
@ -86,6 +95,8 @@ export interface IUIConfig {
advancedMode?: boolean;
taskDefaults?: Record<string, {}>;
defaultFilters?: DefaultFilters;
}
export function getFrontPageContent(

View file

@ -61,9 +61,6 @@ const typePolicies: TypePolicies = {
findSavedFilter: {
read: readReference("SavedFilter"),
},
findDefaultFilter: {
read: readDanglingNull,
},
},
},
Scene: {

View file

@ -0,0 +1 @@
This migration moves default filters from the database into the configuration file. A backup of the current `config.yml` will be created in the same directory with the name `config.yml.59.<date and time>`. The exact filename is written to the log.

View file

@ -2,10 +2,12 @@ import migration32 from "./32.md";
import migration39 from "./39.md";
import migration48 from "./48.md";
import migration58 from "./58.md";
import migration60 from "./60.md";
export const migrationNotes: Record<number, string> = {
32: migration32,
39: migration39,
48: migration48,
58: migration58,
60: migration60,
};

View file

@ -17,6 +17,7 @@ import {
SavedObjectFilter,
SavedUIOptions,
} from "./types";
import { ListFilterOptions } from "./filter-options";
interface IDecodedParams {
perPage?: number;
@ -49,7 +50,8 @@ const DEFAULT_PARAMS = {
// TODO: handle customCriteria
export class ListFilterModel {
public mode: FilterMode;
public readonly mode: FilterMode;
public readonly options: ListFilterOptions;
private config?: ConfigDataFragment;
public searchTerm: string = "";
public currentPage = DEFAULT_PARAMS.currentPage;
@ -65,19 +67,18 @@ export class ListFilterModel {
public constructor(
mode: FilterMode,
config?: ConfigDataFragment,
defaultSort?: string,
defaultDisplayMode?: DisplayMode,
defaultZoomIndex?: number
) {
this.mode = mode;
this.config = config;
this.sortBy = defaultSort;
this.options = getFilterOptions(mode);
const { defaultSortBy, displayModeOptions } = this.options;
this.sortBy = defaultSortBy;
if (this.sortBy === "date") {
this.sortDirection = SortDirectionEnum.Desc;
}
if (defaultDisplayMode !== undefined) {
this.displayMode = defaultDisplayMode;
}
this.displayMode = displayModeOptions[0];
if (defaultZoomIndex !== undefined) {
this.defaultZoomIndex = defaultZoomIndex;
this.zoomIndex = defaultZoomIndex;

View file

@ -1,6 +1,5 @@
import { FilterMode, Scene } from "src/core/generated-graphql";
import { ListFilterModel } from "./list-filter/filter";
import { SceneListFilterOptions } from "./list-filter/scenes";
import { INamedObject } from "src/utils/navigation";
export type QueuedScene = Pick<Scene, "id" | "title" | "date" | "paths"> & {
@ -97,11 +96,7 @@ export class SceneQueue {
c: params.getAll("qfc"),
};
const decoded = ListFilterModel.decodeParams(translated);
const query = new ListFilterModel(
FilterMode.Scenes,
undefined,
SceneListFilterOptions.defaultSortBy
);
const query = new ListFilterModel(FilterMode.Scenes);
query.configureFromDecodedParams(decoded);
ret.query = query;
} else if (params.has("qs")) {

View file

@ -39,7 +39,6 @@ declare namespace PluginApi {
const EnableDlnaDocument: { [key: string]: any };
const ExportObjectsDocument: { [key: string]: any };
const FilterMode: { [key: string]: any };
const FindDefaultFilterDocument: { [key: string]: any };
const FindDuplicateScenesDocument: { [key: string]: any };
const FindGalleriesDocument: { [key: string]: any };
const FindGalleriesForSelectDocument: { [key: string]: any };
@ -208,7 +207,6 @@ declare namespace PluginApi {
const SelectPerformerDataFragmentDoc: { [key: string]: any };
const SelectStudioDataFragmentDoc: { [key: string]: any };
const SelectTagDataFragmentDoc: { [key: string]: any };
const SetDefaultFilterDocument: { [key: string]: any };
const SetPluginsEnabledDocument: { [key: string]: any };
const SetupDocument: { [key: string]: any };
const SlimGalleryDataFragmentDoc: { [key: string]: any };
@ -254,7 +252,6 @@ declare namespace PluginApi {
function refetchConfigurationQuery(...args: any[]): any;
function refetchDirectoryQuery(...args: any[]): any;
function refetchDlnaStatusQuery(...args: any[]): any;
function refetchFindDefaultFilterQuery(...args: any[]): any;
function refetchFindDuplicateScenesQuery(...args: any[]): any;
function refetchFindGalleriesForSelectQuery(...args: any[]): any;
function refetchFindGalleriesQuery(...args: any[]): any;
@ -349,9 +346,6 @@ declare namespace PluginApi {
function useDlnaStatusSuspenseQuery(...args: any[]): any;
function useEnableDlnaMutation(...args: any[]): any;
function useExportObjectsMutation(...args: any[]): any;
function useFindDefaultFilterLazyQuery(...args: any[]): any;
function useFindDefaultFilterQuery(...args: any[]): any;
function useFindDefaultFilterSuspenseQuery(...args: any[]): any;
function useFindDuplicateScenesLazyQuery(...args: any[]): any;
function useFindDuplicateScenesQuery(...args: any[]): any;
function useFindDuplicateScenesSuspenseQuery(...args: any[]): any;