mirror of
https://github.com/stashapp/stash.git
synced 2025-12-06 08:26:00 +01:00
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:
parent
4be60310c3
commit
f9a624b803
59 changed files with 611 additions and 403 deletions
2
go.mod
2
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
|
||||
|
|
|
|||
|
|
@ -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!
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ const (
|
|||
dbConnTimeout = 30
|
||||
)
|
||||
|
||||
var appSchemaVersion uint = 59
|
||||
var appSchemaVersion uint = 60
|
||||
|
||||
//go:embed migrations/*.sql
|
||||
var migrationsBox embed.FS
|
||||
|
|
|
|||
2
pkg/sqlite/migrations/60_default_filter_move.up.sql
Normal file
2
pkg/sqlite/migrations/60_default_filter_move.up.sql
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
-- no schema changes
|
||||
-- default filters will be removed in post-migration
|
||||
176
pkg/sqlite/migrations/60_postmigrate.go
Normal file
176
pkg/sqlite/migrations/60_postmigrate.go
Normal 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)
|
||||
}
|
||||
|
|
@ -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())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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!
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,3 @@ mutation SaveFilter($input: SaveFilterInput!) {
|
|||
mutation DestroySavedFilter($input: DestroyFilterInput!) {
|
||||
destroySavedFilter(input: $input)
|
||||
}
|
||||
|
||||
mutation SetDefaultFilter($input: SetDefaultFilterInput!) {
|
||||
setDefaultFilter(input: $input)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,9 +9,3 @@ query FindSavedFilters($mode: FilterMode) {
|
|||
...SavedFilterData
|
||||
}
|
||||
}
|
||||
|
||||
query FindDefaultFilter($mode: FilterMode!) {
|
||||
findDefaultFilter(mode: $mode) {
|
||||
...SavedFilterData
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = () => {
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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 = () => {
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
/>
|
||||
</Dropdown>
|
||||
<SavedFilterDropdown
|
||||
filter={filter}
|
||||
onSetFilter={(f) => {
|
||||
onFilterUpdate(f);
|
||||
}}
|
||||
view={view}
|
||||
/>
|
||||
<OverlayTrigger
|
||||
placement="top"
|
||||
overlay={
|
||||
|
|
|
|||
|
|
@ -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,18 +127,26 @@ 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: {
|
||||
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<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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
32
ui/v2.5/src/components/List/util.ts
Normal file
32
ui/v2.5/src/components/List/util.ts
Normal 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 };
|
||||
}
|
||||
34
ui/v2.5/src/components/List/views.ts
Normal file
34
ui/v2.5/src/components/List/views.ts
Normal 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",
|
||||
}
|
||||
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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 = () => {
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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 = () => {
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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 = () => {
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -61,9 +61,6 @@ const typePolicies: TypePolicies = {
|
|||
findSavedFilter: {
|
||||
read: readReference("SavedFilter"),
|
||||
},
|
||||
findDefaultFilter: {
|
||||
read: readDanglingNull,
|
||||
},
|
||||
},
|
||||
},
|
||||
Scene: {
|
||||
|
|
|
|||
1
ui/v2.5/src/docs/en/MigrationNotes/60.md
Normal file
1
ui/v2.5/src/docs/en/MigrationNotes/60.md
Normal 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.
|
||||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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")) {
|
||||
|
|
|
|||
6
ui/v2.5/src/pluginApi.d.ts
vendored
6
ui/v2.5/src/pluginApi.d.ts
vendored
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue