diff --git a/go.mod b/go.mod index 1795a6d34..3056e6a95 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/graphql/schema/schema.graphql b/graphql/schema/schema.graphql index c5b8d6089..a1f163ecc 100644 --- a/graphql/schema/schema.graphql +++ b/graphql/schema/schema.graphql @@ -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! diff --git a/internal/api/resolver_mutation_saved_filter.go b/internal/api/resolver_mutation_saved_filter.go index 13b5d87fa..e49c1214c 100644 --- a/internal/api/resolver_mutation_saved_filter.go +++ b/internal/api/resolver_mutation_saved_filter.go @@ -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() - if input.FindFilter == nil && input.ObjectFilter == nil && input.UIOptions == nil { - // clearing - def, err := qb.FindDefault(ctx, input.Mode) - if err != nil { - return err - } + uiConfig := config.GetUIConfiguration() + if uiConfig == nil { + uiConfig = make(map[string]interface{}) + } - if def != nil { - return qb.Destroy(ctx, def.ID) - } + m := utils.NestedMap(uiConfig) - return nil + if input.FindFilter == nil && input.ObjectFilter == nil && input.UIOptions == nil { + // clearing + m.Delete("defaultFilters." + strings.ToLower(input.Mode.String())) + config.SetUIConfiguration(m) + + if err := config.Write(); err != nil { + return false, err } - return qb.SetDefault(ctx, &models.SavedFilter{ - Mode: input.Mode, - FindFilter: input.FindFilter, - ObjectFilter: input.ObjectFilter, - UIOptions: input.UIOptions, - }) - }); err != nil { + return true, nil + } + + subMap := make(map[string]interface{}) + d, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ + TagName: "json", + WeaklyTypedInput: true, + Result: &subMap, + }) + + 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 } diff --git a/internal/api/resolver_query_find_saved_filter.go b/internal/api/resolver_query_find_saved_filter.go index 4f196fd65..1ba68e31d 100644 --- a/internal/api/resolver_query_find_saved_filter.go +++ b/internal/api/resolver_query_find_saved_filter.go @@ -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 } diff --git a/pkg/models/saved_filter.go b/pkg/models/saved_filter.go index a8e4f20c3..919f0a1a6 100644 --- a/pkg/models/saved_filter.go +++ b/pkg/models/saved_filter.go @@ -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 } diff --git a/pkg/sqlite/database.go b/pkg/sqlite/database.go index 3475e955a..7303400a3 100644 --- a/pkg/sqlite/database.go +++ b/pkg/sqlite/database.go @@ -30,7 +30,7 @@ const ( dbConnTimeout = 30 ) -var appSchemaVersion uint = 59 +var appSchemaVersion uint = 60 //go:embed migrations/*.sql var migrationsBox embed.FS diff --git a/pkg/sqlite/migrations/60_default_filter_move.up.sql b/pkg/sqlite/migrations/60_default_filter_move.up.sql new file mode 100644 index 000000000..2c6f6e1fc --- /dev/null +++ b/pkg/sqlite/migrations/60_default_filter_move.up.sql @@ -0,0 +1,2 @@ +-- no schema changes +-- default filters will be removed in post-migration \ No newline at end of file diff --git a/pkg/sqlite/migrations/60_postmigrate.go b/pkg/sqlite/migrations/60_postmigrate.go new file mode 100644 index 000000000..dfed33f18 --- /dev/null +++ b/pkg/sqlite/migrations/60_postmigrate.go @@ -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) +} diff --git a/pkg/sqlite/saved_filter.go b/pkg/sqlite/saved_filter.go index e4369bda5..49b1f45ed 100644 --- a/pkg/sqlite/saved_filter.go +++ b/pkg/sqlite/saved_filter.go @@ -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()) } diff --git a/pkg/sqlite/saved_filter_test.go b/pkg/sqlite/saved_filter_test.go index aa98121fd..60592a923 100644 --- a/pkg/sqlite/saved_filter_test.go +++ b/pkg/sqlite/saved_filter_test.go @@ -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 diff --git a/ui/v2.5/graphql/client-schema.graphql b/ui/v2.5/graphql/client-schema.graphql index 92f53cc3a..57eff3343 100644 --- a/ui/v2.5/graphql/client-schema.graphql +++ b/ui/v2.5/graphql/client-schema.graphql @@ -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! } diff --git a/ui/v2.5/graphql/mutations/filter.graphql b/ui/v2.5/graphql/mutations/filter.graphql index 5d8013123..68a6403a1 100644 --- a/ui/v2.5/graphql/mutations/filter.graphql +++ b/ui/v2.5/graphql/mutations/filter.graphql @@ -7,7 +7,3 @@ mutation SaveFilter($input: SaveFilterInput!) { mutation DestroySavedFilter($input: DestroyFilterInput!) { destroySavedFilter(input: $input) } - -mutation SetDefaultFilter($input: SetDefaultFilterInput!) { - setDefaultFilter(input: $input) -} diff --git a/ui/v2.5/graphql/queries/filter.graphql b/ui/v2.5/graphql/queries/filter.graphql index 67fbaf6cf..276c22d75 100644 --- a/ui/v2.5/graphql/queries/filter.graphql +++ b/ui/v2.5/graphql/queries/filter.graphql @@ -9,9 +9,3 @@ query FindSavedFilters($mode: FilterMode) { ...SavedFilterData } } - -query FindDefaultFilter($mode: FilterMode!) { - findDefaultFilter(mode: $mode) { - ...SavedFilterData - } -} diff --git a/ui/v2.5/src/components/Galleries/Galleries.tsx b/ui/v2.5/src/components/Galleries/Galleries.tsx index d61f124ac..cc2e84ff7 100644 --- a/ui/v2.5/src/components/Galleries/Galleries.tsx +++ b/ui/v2.5/src/components/Galleries/Galleries.tsx @@ -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 ; + return ; }; const GalleryRoutes: React.FC = () => { diff --git a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryImagesPanel.tsx b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryImagesPanel.tsx index c0c539fa9..ea28ffabf 100644 --- a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryImagesPanel.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryImagesPanel.tsx @@ -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 = ({ filterHook={filterHook} alterQuery={active} extraOperations={otherOperations} - persistState={PersistanceLevel.VIEW} - persistanceKey="galleryimages" + view={View.GalleryImages} chapters={gallery.chapters} /> ); diff --git a/ui/v2.5/src/components/Galleries/GalleryList.tsx b/ui/v2.5/src/components/Galleries/GalleryList.tsx index 3ef5acf1f..ba6334e33 100644 --- a/ui/v2.5/src/components/Galleries/GalleryList.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryList.tsx @@ -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 = ({ filterHook, - persistState, + view, alterQuery, }) => { const intl = useIntl(); @@ -192,7 +189,7 @@ export const GalleryList: React.FC = ({ zoomable selectable filterHook={filterHook} - persistState={persistState} + view={view} alterQuery={alterQuery} otherOperations={otherOperations} addKeybinds={addKeybinds} diff --git a/ui/v2.5/src/components/Images/ImageList.tsx b/ui/v2.5/src/components/Images/ImageList.tsx index c0339a44c..3000195d9 100644 --- a/ui/v2.5/src/components/Images/ImageList.tsx +++ b/ui/v2.5/src/components/Images/ImageList.tsx @@ -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[]; chapters?: GQL.GalleryChapterDataFragment[]; @@ -279,8 +278,7 @@ interface IImageList { export const ImageList: React.FC = ({ filterHook, - persistState, - persistanceKey, + view, alterQuery, extraOperations, chapters = [], @@ -421,8 +419,7 @@ export const ImageList: React.FC = ({ zoomable selectable filterHook={filterHook} - persistState={persistState} - persistanceKey={persistanceKey} + view={view} alterQuery={alterQuery} otherOperations={otherOperations} addKeybinds={addKeybinds} diff --git a/ui/v2.5/src/components/Images/Images.tsx b/ui/v2.5/src/components/Images/Images.tsx index 90c6858b0..c0a6b67c8 100644 --- a/ui/v2.5/src/components/Images/Images.tsx +++ b/ui/v2.5/src/components/Images/Images.tsx @@ -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 ; + return ; }; const ImageRoutes: React.FC = () => { diff --git a/ui/v2.5/src/components/List/ItemList.tsx b/ui/v2.5/src/components/List/ItemList.tsx index 7b91844c8..5ffe97d4e 100644 --- a/ui/v2.5/src/components/List/ItemList.tsx +++ b/ui/v2.5/src/components/List/ItemList.tsx @@ -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 { - persistState?: PersistanceLevel; - persistanceKey?: string; + view?: View; defaultSort?: string; filterHook?: (filter: ListFilterModel) => ListFilterModel; filterDialog?: ( @@ -140,7 +130,7 @@ export function makeItemList({ filterHook, onChangePage: _onChangePage, updateFilter, - persistState, + view, zoomable, selectable, otherOperations, @@ -480,7 +470,7 @@ export function makeItemList({ filter={filter} filterOptions={filterOptions} openFilterDialog={() => setShowEditFilter(true)} - persistState={persistState} + view={view} /> ({ const ItemList: React.FC> = (props) => { const { - persistState, - persistanceKey = filterMode, + view, defaultSort = filterOptions.defaultSortBy, defaultZoomIndex, alterQuery = true, @@ -540,7 +529,6 @@ export function makeItemList({ 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({ () => 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({ (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({ // 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({ defaultDisplayMode, defaultZoomIndex, alterQuery, - persistState, + view, updateQueryParams, defaultFilter, defaultFilterLoading, - interfaceState, - persistanceKey, ]); // This hook runs on every page location change (ie navigation), diff --git a/ui/v2.5/src/components/List/ListFilter.tsx b/ui/v2.5/src/components/List/ListFilter.tsx index b7023bfb9..ccfd32409 100644 --- a/ui/v2.5/src/components/List/ListFilter.tsx +++ b/ui/v2.5/src/components/List/ListFilter.tsx @@ -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 = ({ filter, filterOptions, openFilterDialog, - persistState, + view, }) => { const [customPageSizeShowing, setCustomPageSizeShowing] = useState(false); const [queryRef, setQueryFocus] = useFocus(); @@ -191,22 +184,6 @@ export const ListFilter: React.FC = ({ )); } - const SavedFilterDropdown = React.forwardRef< - HTMLDivElement, - HTMLAttributes - >(({ style, className }: HTMLAttributes, ref) => ( -
- { - onFilterUpdate(f); - }} - persistState={persistState} - /> -
- )); - SavedFilterDropdown.displayName = "SavedFilterDropdown"; - function render() { const currentSortBy = filterOptions.sortByOptions.find( (o) => o.value === filter.sortBy @@ -257,24 +234,13 @@ export const ListFilter: React.FC = ({ - - - - - } - > - - - - - - + { + onFilterUpdate(f); + }} + view={view} + /> void; - persistState?: PersistanceLevel; + view?: View; } export const SavedFilterList: React.FC = ({ filter, onSetFilter, - persistState, + view, }) => { const Toast = useToast(); const intl = useIntl(); @@ -51,7 +51,7 @@ export const SavedFilterList: React.FC = ({ const [saveFilter] = useSaveFilter(); const [destroyFilter] = useSavedFilterDestroy(); - const [setDefaultFilter] = useSetDefaultFilter(); + const [saveUI] = useConfigureUI(); const savedFilters = data?.findSavedFilters ?? []; @@ -127,18 +127,26 @@ export const SavedFilterList: React.FC = ({ } async function onSetDefaultFilter() { + if (!view) { + return; + } + const filterCopy = filter.clone(); try { setSaving(true); - await setDefaultFilter({ + await saveUI({ variables: { - input: { - mode: filter.mode, - find_filter: filterCopy.makeFindFilter(), - object_filter: filterCopy.makeSavedFilter(), - ui_options: filterCopy.makeSavedUIOptions(), + partial: { + defaultFilters: { + [view.toString()]: { + mode: filter.mode, + find_filter: filterCopy.makeFindFilter(), + object_filter: filterCopy.makeSavedFilter(), + ui_options: filterCopy.makeSavedUIOptions(), + }, + }, }, }, }); @@ -302,17 +310,19 @@ export const SavedFilterList: React.FC = ({ } function maybeRenderSetDefaultButton() { - if (persistState === PersistanceLevel.ALL) { + if (view) { return (
- +
); } @@ -357,3 +367,36 @@ export const SavedFilterList: React.FC = ({ ); }; + +export const SavedFilterDropdown: React.FC = (props) => { + const SavedFilterDropdownRef = React.forwardRef< + HTMLDivElement, + HTMLAttributes + >(({ style, className }: HTMLAttributes, ref) => ( +
+ +
+ )); + SavedFilterDropdownRef.displayName = "SavedFilterDropdown"; + + return ( + + + + + } + > + + + + + + + ); +}; diff --git a/ui/v2.5/src/components/List/styles.scss b/ui/v2.5/src/components/List/styles.scss index 1c8d993a3..5b1e3b845 100644 --- a/ui/v2.5/src/components/List/styles.scss +++ b/ui/v2.5/src/components/List/styles.scss @@ -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 { diff --git a/ui/v2.5/src/components/List/util.ts b/ui/v2.5/src/components/List/util.ts new file mode 100644 index 000000000..7ebe679b9 --- /dev/null +++ b/ui/v2.5/src/components/List/util.ts @@ -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 }; +} diff --git a/ui/v2.5/src/components/List/views.ts b/ui/v2.5/src/components/List/views.ts new file mode 100644 index 000000000..7e8880f9d --- /dev/null +++ b/ui/v2.5/src/components/List/views.ts @@ -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", +} diff --git a/ui/v2.5/src/components/Movies/MovieDetails/MovieScenesPanel.tsx b/ui/v2.5/src/components/Movies/MovieDetails/MovieScenesPanel.tsx index 9bfbf8b55..0ceff9b47 100644 --- a/ui/v2.5/src/components/Movies/MovieDetails/MovieScenesPanel.tsx +++ b/ui/v2.5/src/components/Movies/MovieDetails/MovieScenesPanel.tsx @@ -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 = ({ filterHook={filterHook} defaultSort="movie_scene_number" alterQuery={active} + view={View.MovieScenes} /> ); } diff --git a/ui/v2.5/src/components/Movies/MovieList.tsx b/ui/v2.5/src/components/Movies/MovieList.tsx index 55b34a783..8b42a3b73 100644 --- a/ui/v2.5/src/components/Movies/MovieList.tsx +++ b/ui/v2.5/src/components/Movies/MovieList.tsx @@ -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 = ({ filterHook, alterQuery }) => { +export const MovieList: React.FC = ({ + filterHook, + alterQuery, + view, +}) => { const intl = useIntl(); const history = useHistory(); const [isExportDialogOpen, setIsExportDialogOpen] = useState(false); @@ -175,7 +177,7 @@ export const MovieList: React.FC = ({ filterHook, alterQuery }) => { { useScrollToTopOnMount(); - return ; + return ; }; const MovieRoutes: React.FC = () => { diff --git a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerGalleriesPanel.tsx b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerGalleriesPanel.tsx index f1ea3db2c..4424ff740 100644 --- a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerGalleriesPanel.tsx +++ b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerGalleriesPanel.tsx @@ -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 = ({ performer, }) => { const filterHook = usePerformerFilterHook(performer); - return ; + return ( + + ); }; diff --git a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerImagesPanel.tsx b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerImagesPanel.tsx index 478f7027f..40c2d88b8 100644 --- a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerImagesPanel.tsx +++ b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerImagesPanel.tsx @@ -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 = ({ performer, }) => { const filterHook = usePerformerFilterHook(performer); - return ; + return ( + + ); }; diff --git a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerMoviesPanel.tsx b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerMoviesPanel.tsx index 4c417bac8..0f1c8b7d5 100644 --- a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerMoviesPanel.tsx +++ b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerMoviesPanel.tsx @@ -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 = ({ performer, }) => { const filterHook = usePerformerFilterHook(performer); - return ; + return ( + + ); }; diff --git a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerScenesPanel.tsx b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerScenesPanel.tsx index d05db77c4..5eca04f6c 100644 --- a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerScenesPanel.tsx +++ b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerScenesPanel.tsx @@ -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 = ({ performer, }) => { const filterHook = usePerformerFilterHook(performer); - return ; + return ( + + ); }; diff --git a/ui/v2.5/src/components/Performers/PerformerDetails/performerAppearsWithPanel.tsx b/ui/v2.5/src/components/Performers/PerformerDetails/performerAppearsWithPanel.tsx index a05ec5e9f..8806bd3ab 100644 --- a/ui/v2.5/src/components/Performers/PerformerDetails/performerAppearsWithPanel.tsx +++ b/ui/v2.5/src/components/Performers/PerformerDetails/performerAppearsWithPanel.tsx @@ -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 = ({ filterHook={filterHook} extraCriteria={extraCriteria} alterQuery={active} + view={View.PerformerAppearsWith} /> ); }; diff --git a/ui/v2.5/src/components/Performers/PerformerList.tsx b/ui/v2.5/src/components/Performers/PerformerList.tsx index 257079bce..9dd0ff277 100644 --- a/ui/v2.5/src/components/Performers/PerformerList.tsx +++ b/ui/v2.5/src/components/Performers/PerformerList.tsx @@ -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 = ({ filterHook, - persistState, + view, alterQuery, extraCriteria, }) => { @@ -325,7 +322,7 @@ export const PerformerList: React.FC = ({ { useScrollToTopOnMount(); - return ; + return ; }; const PerformerRoutes: React.FC = () => { diff --git a/ui/v2.5/src/components/Scenes/SceneList.tsx b/ui/v2.5/src/components/Scenes/SceneList.tsx index 89c52f429..4ffd25040 100644 --- a/ui/v2.5/src/components/Scenes/SceneList.tsx +++ b/ui/v2.5/src/components/Scenes/SceneList.tsx @@ -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 = ({ filterHook, defaultSort, - persistState, + view, alterQuery, }) => { const intl = useIntl(); @@ -357,7 +354,7 @@ export const SceneList: React.FC = ({ zoomable selectable filterHook={filterHook} - persistState={persistState} + view={view} alterQuery={alterQuery} otherOperations={otherOperations} addKeybinds={addKeybinds} diff --git a/ui/v2.5/src/components/Scenes/SceneMarkerList.tsx b/ui/v2.5/src/components/Scenes/SceneMarkerList.tsx index 6de661671..b81a4aecf 100644 --- a/ui/v2.5/src/components/Scenes/SceneMarkerList.tsx +++ b/ui/v2.5/src/components/Scenes/SceneMarkerList.tsx @@ -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 = ({ filterHook, + view, alterQuery, }) => { const intl = useIntl(); @@ -96,7 +99,7 @@ export const SceneMarkerList: React.FC = ({ return ( import("./SceneList")); const SceneMarkerList = lazyComponent(() => import("./SceneMarkerList")); @@ -14,7 +14,7 @@ const SceneCreate = lazyComponent(() => import("./SceneDetails/SceneCreate")); const Scenes: React.FC = () => { useScrollToTopOnMount(); - return ; + return ; }; const SceneMarkers: React.FC = () => { @@ -24,7 +24,7 @@ const SceneMarkers: React.FC = () => { return ( <> - + ); }; diff --git a/ui/v2.5/src/components/Studios/StudioDetails/StudioChildrenPanel.tsx b/ui/v2.5/src/components/Studios/StudioDetails/StudioChildrenPanel.tsx index 8048de66a..62d773a51 100644 --- a/ui/v2.5/src/components/Studios/StudioDetails/StudioChildrenPanel.tsx +++ b/ui/v2.5/src/components/Studios/StudioDetails/StudioChildrenPanel.tsx @@ -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 = ({ return filter; } - return ; + return ( + + ); }; diff --git a/ui/v2.5/src/components/Studios/StudioDetails/StudioGalleriesPanel.tsx b/ui/v2.5/src/components/Studios/StudioDetails/StudioGalleriesPanel.tsx index 42aca05dc..2519d41a3 100644 --- a/ui/v2.5/src/components/Studios/StudioDetails/StudioGalleriesPanel.tsx +++ b/ui/v2.5/src/components/Studios/StudioDetails/StudioGalleriesPanel.tsx @@ -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 = ({ studio, }) => { const filterHook = useStudioFilterHook(studio); - return ; + return ( + + ); }; diff --git a/ui/v2.5/src/components/Studios/StudioDetails/StudioImagesPanel.tsx b/ui/v2.5/src/components/Studios/StudioDetails/StudioImagesPanel.tsx index c2ecb4e0f..3e6f66f25 100644 --- a/ui/v2.5/src/components/Studios/StudioDetails/StudioImagesPanel.tsx +++ b/ui/v2.5/src/components/Studios/StudioDetails/StudioImagesPanel.tsx @@ -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 = ({ studio, }) => { const filterHook = useStudioFilterHook(studio); - return ; + return ( + + ); }; diff --git a/ui/v2.5/src/components/Studios/StudioDetails/StudioMoviesPanel.tsx b/ui/v2.5/src/components/Studios/StudioDetails/StudioMoviesPanel.tsx index 93a820242..3127f5251 100644 --- a/ui/v2.5/src/components/Studios/StudioDetails/StudioMoviesPanel.tsx +++ b/ui/v2.5/src/components/Studios/StudioDetails/StudioMoviesPanel.tsx @@ -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 = ({ studio, }) => { const filterHook = useStudioFilterHook(studio); - return ; + return ( + + ); }; diff --git a/ui/v2.5/src/components/Studios/StudioDetails/StudioPerformersPanel.tsx b/ui/v2.5/src/components/Studios/StudioDetails/StudioPerformersPanel.tsx index e13c8b2ec..0eb146a1b 100644 --- a/ui/v2.5/src/components/Studios/StudioDetails/StudioPerformersPanel.tsx +++ b/ui/v2.5/src/components/Studios/StudioDetails/StudioPerformersPanel.tsx @@ -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 = ({ filterHook={filterHook} extraCriteria={extraCriteria} alterQuery={active} + view={View.StudioPerformers} /> ); }; diff --git a/ui/v2.5/src/components/Studios/StudioDetails/StudioScenesPanel.tsx b/ui/v2.5/src/components/Studios/StudioDetails/StudioScenesPanel.tsx index 84ba8751d..47663f696 100644 --- a/ui/v2.5/src/components/Studios/StudioDetails/StudioScenesPanel.tsx +++ b/ui/v2.5/src/components/Studios/StudioDetails/StudioScenesPanel.tsx @@ -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 = ({ studio, }) => { const filterHook = useStudioFilterHook(studio); - return ; + return ( + + ); }; diff --git a/ui/v2.5/src/components/Studios/StudioList.tsx b/ui/v2.5/src/components/Studios/StudioList.tsx index 4e75c6405..952a6a6ee 100644 --- a/ui/v2.5/src/components/Studios/StudioList.tsx +++ b/ui/v2.5/src/components/Studios/StudioList.tsx @@ -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 = ({ fromParent, filterHook, + view, alterQuery, }) => { const intl = useIntl(); @@ -181,7 +180,7 @@ export const StudioList: React.FC = ({ { useScrollToTopOnMount(); - return ; + return ; }; const StudioRoutes: React.FC = () => { diff --git a/ui/v2.5/src/components/Tags/TagDetails/TagGalleriesPanel.tsx b/ui/v2.5/src/components/Tags/TagDetails/TagGalleriesPanel.tsx index 203715bfb..7d46c4e31 100644 --- a/ui/v2.5/src/components/Tags/TagDetails/TagGalleriesPanel.tsx +++ b/ui/v2.5/src/components/Tags/TagDetails/TagGalleriesPanel.tsx @@ -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 = ({ tag, }) => { const filterHook = useTagFilterHook(tag); - return ; + return ( + + ); }; diff --git a/ui/v2.5/src/components/Tags/TagDetails/TagImagesPanel.tsx b/ui/v2.5/src/components/Tags/TagDetails/TagImagesPanel.tsx index 1c6ea2ec9..61e235499 100644 --- a/ui/v2.5/src/components/Tags/TagDetails/TagImagesPanel.tsx +++ b/ui/v2.5/src/components/Tags/TagDetails/TagImagesPanel.tsx @@ -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 = ({ active, tag }) => { const filterHook = useTagFilterHook(tag); - return ; + return ( + + ); }; diff --git a/ui/v2.5/src/components/Tags/TagDetails/TagMarkersPanel.tsx b/ui/v2.5/src/components/Tags/TagDetails/TagMarkersPanel.tsx index 95d2d5607..2bd4658d5 100644 --- a/ui/v2.5/src/components/Tags/TagDetails/TagMarkersPanel.tsx +++ b/ui/v2.5/src/components/Tags/TagDetails/TagMarkersPanel.tsx @@ -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 = ({ return filter; } - return ; + return ( + + ); }; diff --git a/ui/v2.5/src/components/Tags/TagDetails/TagPerformersPanel.tsx b/ui/v2.5/src/components/Tags/TagDetails/TagPerformersPanel.tsx index 255acaf64..192b89dcc 100644 --- a/ui/v2.5/src/components/Tags/TagDetails/TagPerformersPanel.tsx +++ b/ui/v2.5/src/components/Tags/TagDetails/TagPerformersPanel.tsx @@ -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 = ({ tag, }) => { const filterHook = useTagFilterHook(tag); - return ; + return ( + + ); }; diff --git a/ui/v2.5/src/components/Tags/TagDetails/TagScenesPanel.tsx b/ui/v2.5/src/components/Tags/TagDetails/TagScenesPanel.tsx index 5de9ed916..39d850d35 100644 --- a/ui/v2.5/src/components/Tags/TagDetails/TagScenesPanel.tsx +++ b/ui/v2.5/src/components/Tags/TagDetails/TagScenesPanel.tsx @@ -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 = ({ active, tag }) => { const filterHook = useTagFilterHook(tag); - return ; + return ( + + ); }; diff --git a/ui/v2.5/src/components/Tags/TagList.tsx b/ui/v2.5/src/components/Tags/TagList.tsx index 2458a273b..42a6316f9 100644 --- a/ui/v2.5/src/components/Tags/TagList.tsx +++ b/ui/v2.5/src/components/Tags/TagList.tsx @@ -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 = ({ filterHook, alterQuery }) => { zoomable defaultZoomIndex={0} filterHook={filterHook} - persistState={PersistanceLevel.ALL} + view={View.Tags} alterQuery={alterQuery} otherOperations={otherOperations} addKeybinds={addKeybinds} diff --git a/ui/v2.5/src/core/StashService.ts b/ui/v2.5/src/core/StashService.ts index 251df72f5..d7518b6c7 100644 --- a/ui/v2.5/src/core/StashService.ts +++ b/ui/v2.5/src/core/StashService.ts @@ -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]); }, }); diff --git a/ui/v2.5/src/core/config.ts b/ui/v2.5/src/core/config.ts index 1e0d1030a..3e7585df7 100644 --- a/ui/v2.5/src/core/config.ts +++ b/ui/v2.5/src/core/config.ts @@ -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; + + defaultFilters?: DefaultFilters; } export function getFrontPageContent( diff --git a/ui/v2.5/src/core/createClient.ts b/ui/v2.5/src/core/createClient.ts index 045f27676..e5f502cca 100644 --- a/ui/v2.5/src/core/createClient.ts +++ b/ui/v2.5/src/core/createClient.ts @@ -61,9 +61,6 @@ const typePolicies: TypePolicies = { findSavedFilter: { read: readReference("SavedFilter"), }, - findDefaultFilter: { - read: readDanglingNull, - }, }, }, Scene: { diff --git a/ui/v2.5/src/docs/en/MigrationNotes/60.md b/ui/v2.5/src/docs/en/MigrationNotes/60.md new file mode 100644 index 000000000..5d2feaf0e --- /dev/null +++ b/ui/v2.5/src/docs/en/MigrationNotes/60.md @@ -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.`. The exact filename is written to the log. \ No newline at end of file diff --git a/ui/v2.5/src/docs/en/MigrationNotes/index.ts b/ui/v2.5/src/docs/en/MigrationNotes/index.ts index dd4f3fcd1..5a201a091 100644 --- a/ui/v2.5/src/docs/en/MigrationNotes/index.ts +++ b/ui/v2.5/src/docs/en/MigrationNotes/index.ts @@ -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 = { 32: migration32, 39: migration39, 48: migration48, 58: migration58, + 60: migration60, }; diff --git a/ui/v2.5/src/models/list-filter/filter.ts b/ui/v2.5/src/models/list-filter/filter.ts index cd84eaada..794fd2a7e 100644 --- a/ui/v2.5/src/models/list-filter/filter.ts +++ b/ui/v2.5/src/models/list-filter/filter.ts @@ -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; diff --git a/ui/v2.5/src/models/sceneQueue.ts b/ui/v2.5/src/models/sceneQueue.ts index 83b7e7820..ceec69090 100644 --- a/ui/v2.5/src/models/sceneQueue.ts +++ b/ui/v2.5/src/models/sceneQueue.ts @@ -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 & { @@ -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")) { diff --git a/ui/v2.5/src/pluginApi.d.ts b/ui/v2.5/src/pluginApi.d.ts index 4967eedbf..ae292ceee 100644 --- a/ui/v2.5/src/pluginApi.d.ts +++ b/ui/v2.5/src/pluginApi.d.ts @@ -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;