mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 04:14:39 +03:00
Saved filters (#1474)
* Refactor list filter * Filter/criterion refactor * Rename option value to type * Remove None from options * Add saved filter button * Integrate default filters
This commit is contained in:
@@ -52,5 +52,7 @@ models:
|
|||||||
model: github.com/stashapp/stash/pkg/models.ScrapedMovie
|
model: github.com/stashapp/stash/pkg/models.ScrapedMovie
|
||||||
ScrapedMovieStudio:
|
ScrapedMovieStudio:
|
||||||
model: github.com/stashapp/stash/pkg/models.ScrapedMovieStudio
|
model: github.com/stashapp/stash/pkg/models.ScrapedMovieStudio
|
||||||
|
SavedFilter:
|
||||||
|
model: github.com/stashapp/stash/pkg/models.SavedFilter
|
||||||
StashID:
|
StashID:
|
||||||
model: github.com/stashapp/stash/pkg/models.StashID
|
model: github.com/stashapp/stash/pkg/models.StashID
|
||||||
|
|||||||
6
graphql/documents/data/filter.graphql
Normal file
6
graphql/documents/data/filter.graphql
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
fragment SavedFilterData on SavedFilter {
|
||||||
|
id
|
||||||
|
mode
|
||||||
|
name
|
||||||
|
filter
|
||||||
|
}
|
||||||
13
graphql/documents/mutations/filter.graphql
Normal file
13
graphql/documents/mutations/filter.graphql
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
mutation SaveFilter($input: SaveFilterInput!) {
|
||||||
|
saveFilter(input: $input) {
|
||||||
|
...SavedFilterData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mutation DestroySavedFilter($input: DestroyFilterInput!) {
|
||||||
|
destroySavedFilter(input: $input)
|
||||||
|
}
|
||||||
|
|
||||||
|
mutation SetDefaultFilter($input: SetDefaultFilterInput!) {
|
||||||
|
setDefaultFilter(input: $input)
|
||||||
|
}
|
||||||
11
graphql/documents/queries/filter.graphql
Normal file
11
graphql/documents/queries/filter.graphql
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
query FindSavedFilters($mode: FilterMode!) {
|
||||||
|
findSavedFilters(mode: $mode) {
|
||||||
|
...SavedFilterData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
query FindDefaultFilter($mode: FilterMode!) {
|
||||||
|
findDefaultFilter(mode: $mode) {
|
||||||
|
...SavedFilterData
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,9 @@
|
|||||||
"""The query root for this schema"""
|
"""The query root for this schema"""
|
||||||
type Query {
|
type Query {
|
||||||
|
# Filters
|
||||||
|
findSavedFilters(mode: FilterMode!): [SavedFilter!]!
|
||||||
|
findDefaultFilter(mode: FilterMode!): SavedFilter
|
||||||
|
|
||||||
"""Find a scene by ID or Checksum"""
|
"""Find a scene by ID or Checksum"""
|
||||||
findScene(id: ID, checksum: String): Scene
|
findScene(id: ID, checksum: String): Scene
|
||||||
findSceneByHash(input: SceneHashInput!): Scene
|
findSceneByHash(input: SceneHashInput!): Scene
|
||||||
@@ -199,6 +203,11 @@ type Mutation {
|
|||||||
tagsDestroy(ids: [ID!]!): Boolean!
|
tagsDestroy(ids: [ID!]!): Boolean!
|
||||||
tagsMerge(input: TagsMergeInput!): Tag
|
tagsMerge(input: TagsMergeInput!): Tag
|
||||||
|
|
||||||
|
# Saved filters
|
||||||
|
saveFilter(input: SaveFilterInput!): SavedFilter!
|
||||||
|
destroySavedFilter(input: DestroyFilterInput!): Boolean!
|
||||||
|
setDefaultFilter(input: SetDefaultFilterInput!): Boolean!
|
||||||
|
|
||||||
"""Change general configuration options"""
|
"""Change general configuration options"""
|
||||||
configureGeneral(input: ConfigGeneralInput!): ConfigGeneralResult!
|
configureGeneral(input: ConfigGeneralInput!): ConfigGeneralResult!
|
||||||
configureInterface(input: ConfigInterfaceInput!): ConfigInterfaceResult!
|
configureInterface(input: ConfigInterfaceInput!): ConfigInterfaceResult!
|
||||||
|
|||||||
@@ -317,3 +317,41 @@ input HierarchicalMultiCriterionInput {
|
|||||||
modifier: CriterionModifier!
|
modifier: CriterionModifier!
|
||||||
depth: Int!
|
depth: Int!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum FilterMode {
|
||||||
|
SCENES,
|
||||||
|
PERFORMERS,
|
||||||
|
STUDIOS,
|
||||||
|
GALLERIES,
|
||||||
|
SCENE_MARKERS,
|
||||||
|
MOVIES,
|
||||||
|
TAGS,
|
||||||
|
IMAGES,
|
||||||
|
}
|
||||||
|
|
||||||
|
type SavedFilter {
|
||||||
|
id: ID!
|
||||||
|
mode: FilterMode!
|
||||||
|
name: String!
|
||||||
|
"""JSON-encoded filter string"""
|
||||||
|
filter: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
input SaveFilterInput {
|
||||||
|
"""provide ID to overwrite existing filter"""
|
||||||
|
id: ID
|
||||||
|
mode: FilterMode!
|
||||||
|
name: String!
|
||||||
|
"""JSON-encoded filter string"""
|
||||||
|
filter: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
input DestroyFilterInput {
|
||||||
|
id: ID!
|
||||||
|
}
|
||||||
|
|
||||||
|
input SetDefaultFilterInput {
|
||||||
|
mode: FilterMode!
|
||||||
|
"""JSON-encoded filter string - null to clear"""
|
||||||
|
filter: String
|
||||||
|
}
|
||||||
|
|||||||
89
pkg/api/resolver_mutation_saved_filter.go
Normal file
89
pkg/api/resolver_mutation_saved_filter.go
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/stashapp/stash/pkg/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (r *mutationResolver) SaveFilter(ctx context.Context, input models.SaveFilterInput) (ret *models.SavedFilter, err error) {
|
||||||
|
if strings.TrimSpace(input.Name) == "" {
|
||||||
|
return nil, errors.New("name must be non-empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
var id *int
|
||||||
|
if input.ID != nil {
|
||||||
|
idv, err := strconv.Atoi(*input.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
id = &idv
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||||
|
f := models.SavedFilter{
|
||||||
|
Mode: input.Mode,
|
||||||
|
Name: input.Name,
|
||||||
|
Filter: input.Filter,
|
||||||
|
}
|
||||||
|
if id == nil {
|
||||||
|
ret, err = repo.SavedFilter().Create(f)
|
||||||
|
} else {
|
||||||
|
f.ID = *id
|
||||||
|
ret, err = repo.SavedFilter().Update(f)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return ret, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *mutationResolver) DestroySavedFilter(ctx context.Context, input models.DestroyFilterInput) (bool, error) {
|
||||||
|
id, err := strconv.Atoi(input.ID)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||||
|
return repo.SavedFilter().Destroy(id)
|
||||||
|
}); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *mutationResolver) SetDefaultFilter(ctx context.Context, input models.SetDefaultFilterInput) (bool, error) {
|
||||||
|
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||||
|
qb := repo.SavedFilter()
|
||||||
|
|
||||||
|
if input.Filter == nil {
|
||||||
|
// clearing
|
||||||
|
def, err := qb.FindDefault(input.Mode)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if def != nil {
|
||||||
|
return qb.Destroy(def.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := qb.SetDefault(models.SavedFilter{
|
||||||
|
Mode: input.Mode,
|
||||||
|
Filter: *input.Filter,
|
||||||
|
})
|
||||||
|
|
||||||
|
return err
|
||||||
|
}); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
27
pkg/api/resolver_query_find_saved_filter.go
Normal file
27
pkg/api/resolver_query_find_saved_filter.go
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/stashapp/stash/pkg/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (r *queryResolver) FindSavedFilters(ctx context.Context, mode models.FilterMode) (ret []*models.SavedFilter, err error) {
|
||||||
|
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||||
|
ret, err = repo.SavedFilter().FindByMode(mode)
|
||||||
|
return err
|
||||||
|
}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return ret, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *queryResolver) FindDefaultFilter(ctx context.Context, mode models.FilterMode) (ret *models.SavedFilter, err error) {
|
||||||
|
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||||
|
ret, err = repo.SavedFilter().FindDefault(mode)
|
||||||
|
return err
|
||||||
|
}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return ret, err
|
||||||
|
}
|
||||||
@@ -23,7 +23,7 @@ import (
|
|||||||
var DB *sqlx.DB
|
var DB *sqlx.DB
|
||||||
var WriteMu *sync.Mutex
|
var WriteMu *sync.Mutex
|
||||||
var dbPath string
|
var dbPath string
|
||||||
var appSchemaVersion uint = 24
|
var appSchemaVersion uint = 25
|
||||||
var databaseSchemaVersion uint
|
var databaseSchemaVersion uint
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|||||||
8
pkg/database/migrations/25_saved_filters.up.sql
Normal file
8
pkg/database/migrations/25_saved_filters.up.sql
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
CREATE TABLE `saved_filters` (
|
||||||
|
`id` integer not null primary key autoincrement,
|
||||||
|
`name` varchar(510) not null,
|
||||||
|
`mode` varchar(255) not null,
|
||||||
|
`filter` blob not null
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX `index_saved_filters_on_mode_name_unique` on `saved_filters` (`mode`, `name`);
|
||||||
165
pkg/models/mocks/SavedFilterReaderWriter.go
Normal file
165
pkg/models/mocks/SavedFilterReaderWriter.go
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
// Code generated by mockery v0.0.0-dev. DO NOT EDIT.
|
||||||
|
|
||||||
|
package mocks
|
||||||
|
|
||||||
|
import (
|
||||||
|
models "github.com/stashapp/stash/pkg/models"
|
||||||
|
mock "github.com/stretchr/testify/mock"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SavedFilterReaderWriter is an autogenerated mock type for the SavedFilterReaderWriter type
|
||||||
|
type SavedFilterReaderWriter struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create provides a mock function with given fields: obj
|
||||||
|
func (_m *SavedFilterReaderWriter) Create(obj models.SavedFilter) (*models.SavedFilter, error) {
|
||||||
|
ret := _m.Called(obj)
|
||||||
|
|
||||||
|
var r0 *models.SavedFilter
|
||||||
|
if rf, ok := ret.Get(0).(func(models.SavedFilter) *models.SavedFilter); ok {
|
||||||
|
r0 = rf(obj)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).(*models.SavedFilter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(1).(func(models.SavedFilter) error); ok {
|
||||||
|
r1 = rf(obj)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Destroy provides a mock function with given fields: id
|
||||||
|
func (_m *SavedFilterReaderWriter) Destroy(id int) error {
|
||||||
|
ret := _m.Called(id)
|
||||||
|
|
||||||
|
var r0 error
|
||||||
|
if rf, ok := ret.Get(0).(func(int) error); ok {
|
||||||
|
r0 = rf(id)
|
||||||
|
} else {
|
||||||
|
r0 = ret.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find provides a mock function with given fields: id
|
||||||
|
func (_m *SavedFilterReaderWriter) Find(id int) (*models.SavedFilter, error) {
|
||||||
|
ret := _m.Called(id)
|
||||||
|
|
||||||
|
var r0 *models.SavedFilter
|
||||||
|
if rf, ok := ret.Get(0).(func(int) *models.SavedFilter); ok {
|
||||||
|
r0 = rf(id)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).(*models.SavedFilter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(1).(func(int) error); ok {
|
||||||
|
r1 = rf(id)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindByMode provides a mock function with given fields: mode
|
||||||
|
func (_m *SavedFilterReaderWriter) FindByMode(mode models.FilterMode) ([]*models.SavedFilter, error) {
|
||||||
|
ret := _m.Called(mode)
|
||||||
|
|
||||||
|
var r0 []*models.SavedFilter
|
||||||
|
if rf, ok := ret.Get(0).(func(models.FilterMode) []*models.SavedFilter); ok {
|
||||||
|
r0 = rf(mode)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).([]*models.SavedFilter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(1).(func(models.FilterMode) error); ok {
|
||||||
|
r1 = rf(mode)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindDefault provides a mock function with given fields: mode
|
||||||
|
func (_m *SavedFilterReaderWriter) FindDefault(mode models.FilterMode) (*models.SavedFilter, error) {
|
||||||
|
ret := _m.Called(mode)
|
||||||
|
|
||||||
|
var r0 *models.SavedFilter
|
||||||
|
if rf, ok := ret.Get(0).(func(models.FilterMode) *models.SavedFilter); ok {
|
||||||
|
r0 = rf(mode)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).(*models.SavedFilter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(1).(func(models.FilterMode) error); ok {
|
||||||
|
r1 = rf(mode)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDefault provides a mock function with given fields: obj
|
||||||
|
func (_m *SavedFilterReaderWriter) SetDefault(obj models.SavedFilter) (*models.SavedFilter, error) {
|
||||||
|
ret := _m.Called(obj)
|
||||||
|
|
||||||
|
var r0 *models.SavedFilter
|
||||||
|
if rf, ok := ret.Get(0).(func(models.SavedFilter) *models.SavedFilter); ok {
|
||||||
|
r0 = rf(obj)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).(*models.SavedFilter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(1).(func(models.SavedFilter) error); ok {
|
||||||
|
r1 = rf(obj)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update provides a mock function with given fields: obj
|
||||||
|
func (_m *SavedFilterReaderWriter) Update(obj models.SavedFilter) (*models.SavedFilter, error) {
|
||||||
|
ret := _m.Called(obj)
|
||||||
|
|
||||||
|
var r0 *models.SavedFilter
|
||||||
|
if rf, ok := ret.Get(0).(func(models.SavedFilter) *models.SavedFilter); ok {
|
||||||
|
r0 = rf(obj)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).(*models.SavedFilter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(1).(func(models.SavedFilter) error); ok {
|
||||||
|
r1 = rf(obj)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ type TransactionManager struct {
|
|||||||
scrapedItem models.ScrapedItemReaderWriter
|
scrapedItem models.ScrapedItemReaderWriter
|
||||||
studio models.StudioReaderWriter
|
studio models.StudioReaderWriter
|
||||||
tag models.TagReaderWriter
|
tag models.TagReaderWriter
|
||||||
|
savedFilter models.SavedFilterReaderWriter
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTransactionManager() *TransactionManager {
|
func NewTransactionManager() *TransactionManager {
|
||||||
@@ -29,6 +30,7 @@ func NewTransactionManager() *TransactionManager {
|
|||||||
scrapedItem: &ScrapedItemReaderWriter{},
|
scrapedItem: &ScrapedItemReaderWriter{},
|
||||||
studio: &StudioReaderWriter{},
|
studio: &StudioReaderWriter{},
|
||||||
tag: &TagReaderWriter{},
|
tag: &TagReaderWriter{},
|
||||||
|
savedFilter: &SavedFilterReaderWriter{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,6 +74,10 @@ func (t *TransactionManager) Tag() models.TagReaderWriter {
|
|||||||
return t.tag
|
return t.tag
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *TransactionManager) SavedFilter() models.SavedFilterReaderWriter {
|
||||||
|
return t.savedFilter
|
||||||
|
}
|
||||||
|
|
||||||
type ReadTransaction struct {
|
type ReadTransaction struct {
|
||||||
t *TransactionManager
|
t *TransactionManager
|
||||||
}
|
}
|
||||||
@@ -115,3 +121,7 @@ func (r *ReadTransaction) Studio() models.StudioReader {
|
|||||||
func (r *ReadTransaction) Tag() models.TagReader {
|
func (r *ReadTransaction) Tag() models.TagReader {
|
||||||
return r.t.tag
|
return r.t.tag
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *ReadTransaction) SavedFilter() models.SavedFilterReader {
|
||||||
|
return r.t.savedFilter
|
||||||
|
}
|
||||||
|
|||||||
19
pkg/models/model_saved_filter.go
Normal file
19
pkg/models/model_saved_filter.go
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
type SavedFilter struct {
|
||||||
|
ID int `db:"id" json:"id"`
|
||||||
|
Mode FilterMode `db:"mode" json:"mode"`
|
||||||
|
Name string `db:"name" json:"name"`
|
||||||
|
// JSON-encoded filter string
|
||||||
|
Filter string `db:"filter" json:"filter"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SavedFilters []*SavedFilter
|
||||||
|
|
||||||
|
func (m *SavedFilters) Append(o interface{}) {
|
||||||
|
*m = append(*m, o.(*SavedFilter))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *SavedFilters) New() interface{} {
|
||||||
|
return &SavedFilter{}
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ type Repository interface {
|
|||||||
ScrapedItem() ScrapedItemReaderWriter
|
ScrapedItem() ScrapedItemReaderWriter
|
||||||
Studio() StudioReaderWriter
|
Studio() StudioReaderWriter
|
||||||
Tag() TagReaderWriter
|
Tag() TagReaderWriter
|
||||||
|
SavedFilter() SavedFilterReaderWriter
|
||||||
}
|
}
|
||||||
|
|
||||||
type ReaderRepository interface {
|
type ReaderRepository interface {
|
||||||
@@ -22,4 +23,5 @@ type ReaderRepository interface {
|
|||||||
ScrapedItem() ScrapedItemReader
|
ScrapedItem() ScrapedItemReader
|
||||||
Studio() StudioReader
|
Studio() StudioReader
|
||||||
Tag() TagReader
|
Tag() TagReader
|
||||||
|
SavedFilter() SavedFilterReader
|
||||||
}
|
}
|
||||||
|
|||||||
19
pkg/models/saved_filter.go
Normal file
19
pkg/models/saved_filter.go
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
type SavedFilterReader interface {
|
||||||
|
Find(id int) (*SavedFilter, error)
|
||||||
|
FindByMode(mode FilterMode) ([]*SavedFilter, error)
|
||||||
|
FindDefault(mode FilterMode) (*SavedFilter, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type SavedFilterWriter interface {
|
||||||
|
Create(obj SavedFilter) (*SavedFilter, error)
|
||||||
|
Update(obj SavedFilter) (*SavedFilter, error)
|
||||||
|
SetDefault(obj SavedFilter) (*SavedFilter, error)
|
||||||
|
Destroy(id int) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type SavedFilterReaderWriter interface {
|
||||||
|
SavedFilterReader
|
||||||
|
SavedFilterWriter
|
||||||
|
}
|
||||||
109
pkg/sqlite/saved_filter.go
Normal file
109
pkg/sqlite/saved_filter.go
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
package sqlite
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/stashapp/stash/pkg/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
const savedFilterTable = "saved_filters"
|
||||||
|
const savedFilterDefaultName = ""
|
||||||
|
|
||||||
|
type savedFilterQueryBuilder struct {
|
||||||
|
repository
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSavedFilterReaderWriter(tx dbi) *savedFilterQueryBuilder {
|
||||||
|
return &savedFilterQueryBuilder{
|
||||||
|
repository{
|
||||||
|
tx: tx,
|
||||||
|
tableName: savedFilterTable,
|
||||||
|
idColumn: idColumn,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (qb *savedFilterQueryBuilder) Create(newObject models.SavedFilter) (*models.SavedFilter, error) {
|
||||||
|
var ret models.SavedFilter
|
||||||
|
if err := qb.insertObject(newObject, &ret); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ret, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (qb *savedFilterQueryBuilder) Update(updatedObject models.SavedFilter) (*models.SavedFilter, error) {
|
||||||
|
const partial = false
|
||||||
|
if err := qb.update(updatedObject.ID, updatedObject, partial); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var ret models.SavedFilter
|
||||||
|
if err := qb.get(updatedObject.ID, &ret); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ret, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (qb *savedFilterQueryBuilder) SetDefault(obj models.SavedFilter) (*models.SavedFilter, error) {
|
||||||
|
// find the existing default
|
||||||
|
existing, err := qb.FindDefault(obj.Mode)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
obj.Name = savedFilterDefaultName
|
||||||
|
|
||||||
|
if existing != nil {
|
||||||
|
obj.ID = existing.ID
|
||||||
|
return qb.Update(obj)
|
||||||
|
}
|
||||||
|
|
||||||
|
return qb.Create(obj)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (qb *savedFilterQueryBuilder) Destroy(id int) error {
|
||||||
|
return qb.destroyExisting([]int{id})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (qb *savedFilterQueryBuilder) Find(id int) (*models.SavedFilter, error) {
|
||||||
|
var ret models.SavedFilter
|
||||||
|
if err := qb.get(id, &ret); err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &ret, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (qb *savedFilterQueryBuilder) FindByMode(mode models.FilterMode) ([]*models.SavedFilter, error) {
|
||||||
|
// exclude empty-named filters - these are the internal default filters
|
||||||
|
|
||||||
|
query := fmt.Sprintf(`SELECT * FROM %s WHERE mode = ? AND name != ?`, savedFilterTable)
|
||||||
|
|
||||||
|
var ret models.SavedFilters
|
||||||
|
if err := qb.query(query, []interface{}{mode, savedFilterDefaultName}, &ret); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return []*models.SavedFilter(ret), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (qb *savedFilterQueryBuilder) FindDefault(mode models.FilterMode) (*models.SavedFilter, error) {
|
||||||
|
query := fmt.Sprintf(`SELECT * FROM %s WHERE mode = ? AND name = ?`, savedFilterTable)
|
||||||
|
|
||||||
|
var ret models.SavedFilters
|
||||||
|
if err := qb.query(query, []interface{}{mode, savedFilterDefaultName}, &ret); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(ret) > 0 {
|
||||||
|
return ret[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
123
pkg/sqlite/saved_filter_test.go
Normal file
123
pkg/sqlite/saved_filter_test.go
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
// +build integration
|
||||||
|
|
||||||
|
package sqlite_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stashapp/stash/pkg/models"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSavedFilterFind(t *testing.T) {
|
||||||
|
withTxn(func(r models.Repository) error {
|
||||||
|
savedFilter, err := r.SavedFilter().Find(savedFilterIDs[savedFilterIdxImage])
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error finding saved filter: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, savedFilterIDs[savedFilterIdxImage], savedFilter.ID)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSavedFilterFindByMode(t *testing.T) {
|
||||||
|
withTxn(func(r models.Repository) error {
|
||||||
|
savedFilters, err := r.SavedFilter().FindByMode(models.FilterModeScenes)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error finding saved filters: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Len(t, savedFilters, 1)
|
||||||
|
assert.Equal(t, savedFilterIDs[savedFilterIdxScene], savedFilters[0].ID)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSavedFilterDestroy(t *testing.T) {
|
||||||
|
const filterName = "filterToDestroy"
|
||||||
|
const testFilter = "{}"
|
||||||
|
var id int
|
||||||
|
|
||||||
|
// create the saved filter to destroy
|
||||||
|
withTxn(func(r models.Repository) error {
|
||||||
|
created, err := r.SavedFilter().Create(models.SavedFilter{
|
||||||
|
Name: filterName,
|
||||||
|
Mode: models.FilterModeScenes,
|
||||||
|
Filter: testFilter,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
id = created.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
|
||||||
|
withTxn(func(r models.Repository) error {
|
||||||
|
qb := r.SavedFilter()
|
||||||
|
|
||||||
|
return qb.Destroy(id)
|
||||||
|
})
|
||||||
|
|
||||||
|
// now try to find it
|
||||||
|
withTxn(func(r models.Repository) error {
|
||||||
|
found, err := r.SavedFilter().Find(id)
|
||||||
|
if err == nil {
|
||||||
|
assert.Nil(t, found)
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSavedFilterFindDefault(t *testing.T) {
|
||||||
|
withTxn(func(r models.Repository) error {
|
||||||
|
def, err := r.SavedFilter().FindDefault(models.FilterModeScenes)
|
||||||
|
if err == nil {
|
||||||
|
assert.Equal(t, savedFilterIDs[savedFilterIdxDefaultScene], def.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSavedFilterSetDefault(t *testing.T) {
|
||||||
|
const newFilter = "foo"
|
||||||
|
|
||||||
|
withTxn(func(r models.Repository) error {
|
||||||
|
_, err := r.SavedFilter().SetDefault(models.SavedFilter{
|
||||||
|
Mode: models.FilterModeMovies,
|
||||||
|
Filter: newFilter,
|
||||||
|
})
|
||||||
|
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
|
||||||
|
var defID int
|
||||||
|
withTxn(func(r models.Repository) error {
|
||||||
|
def, err := r.SavedFilter().FindDefault(models.FilterModeMovies)
|
||||||
|
if err == nil {
|
||||||
|
defID = def.ID
|
||||||
|
assert.Equal(t, newFilter, def.Filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
|
||||||
|
// destroy it again
|
||||||
|
withTxn(func(r models.Repository) error {
|
||||||
|
return r.SavedFilter().Destroy(defID)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO Update
|
||||||
|
// TODO Destroy
|
||||||
|
// TODO Find
|
||||||
|
// TODO GetMarkerStrings
|
||||||
|
// TODO Wall
|
||||||
|
// TODO Query
|
||||||
@@ -189,22 +189,34 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
pathField = "Path"
|
savedFilterIdxDefaultScene = iota
|
||||||
checksumField = "Checksum"
|
savedFilterIdxDefaultImage
|
||||||
titleField = "Title"
|
savedFilterIdxScene
|
||||||
urlField = "URL"
|
savedFilterIdxImage
|
||||||
zipPath = "zipPath.zip"
|
|
||||||
|
// new indexes above
|
||||||
|
totalSavedFilters
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
pathField = "Path"
|
||||||
|
checksumField = "Checksum"
|
||||||
|
titleField = "Title"
|
||||||
|
urlField = "URL"
|
||||||
|
zipPath = "zipPath.zip"
|
||||||
|
firstSavedFilterName = "firstSavedFilterName"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
sceneIDs []int
|
sceneIDs []int
|
||||||
imageIDs []int
|
imageIDs []int
|
||||||
performerIDs []int
|
performerIDs []int
|
||||||
movieIDs []int
|
movieIDs []int
|
||||||
galleryIDs []int
|
galleryIDs []int
|
||||||
tagIDs []int
|
tagIDs []int
|
||||||
studioIDs []int
|
studioIDs []int
|
||||||
markerIDs []int
|
markerIDs []int
|
||||||
|
savedFilterIDs []int
|
||||||
|
|
||||||
tagNames []string
|
tagNames []string
|
||||||
studioNames []string
|
studioNames []string
|
||||||
@@ -423,6 +435,10 @@ func populateDB() error {
|
|||||||
return fmt.Errorf("error creating studios: %s", err.Error())
|
return fmt.Errorf("error creating studios: %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := createSavedFilters(r.SavedFilter(), totalSavedFilters); err != nil {
|
||||||
|
return fmt.Errorf("error creating saved filters: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
if err := linkPerformerTags(r.Performer()); err != nil {
|
if err := linkPerformerTags(r.Performer()); err != nil {
|
||||||
return fmt.Errorf("error linking performer tags: %s", err.Error())
|
return fmt.Errorf("error linking performer tags: %s", err.Error())
|
||||||
}
|
}
|
||||||
@@ -979,6 +995,51 @@ func createMarker(mqb models.SceneMarkerReaderWriter, sceneIdx, primaryTagIdx in
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getSavedFilterMode(index int) models.FilterMode {
|
||||||
|
switch index {
|
||||||
|
case savedFilterIdxScene, savedFilterIdxDefaultScene:
|
||||||
|
return models.FilterModeScenes
|
||||||
|
case savedFilterIdxImage, savedFilterIdxDefaultImage:
|
||||||
|
return models.FilterModeImages
|
||||||
|
default:
|
||||||
|
return models.FilterModeScenes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getSavedFilterName(index int) string {
|
||||||
|
if index <= savedFilterIdxDefaultImage {
|
||||||
|
// empty string for default filters
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if index <= savedFilterIdxImage {
|
||||||
|
// use the same name for the first two - should be possible
|
||||||
|
return firstSavedFilterName
|
||||||
|
}
|
||||||
|
|
||||||
|
return getPrefixedStringValue("savedFilter", index, "Name")
|
||||||
|
}
|
||||||
|
|
||||||
|
func createSavedFilters(qb models.SavedFilterReaderWriter, n int) error {
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
savedFilter := models.SavedFilter{
|
||||||
|
Mode: getSavedFilterMode(i),
|
||||||
|
Name: getSavedFilterName(i),
|
||||||
|
Filter: getPrefixedStringValue("savedFilter", i, "Filter"),
|
||||||
|
}
|
||||||
|
|
||||||
|
created, err := qb.Create(savedFilter)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error creating saved filter %v+: %s", savedFilter, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
savedFilterIDs = append(savedFilterIDs, created.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func doLinks(links [][2]int, fn func(idx1, idx2 int) error) error {
|
func doLinks(links [][2]int, fn func(idx1, idx2 int) error) error {
|
||||||
for _, l := range links {
|
for _, l := range links {
|
||||||
if err := fn(l[0], l[1]); err != nil {
|
if err := fn(l[0], l[1]); err != nil {
|
||||||
|
|||||||
@@ -125,6 +125,11 @@ func (t *transaction) Tag() models.TagReaderWriter {
|
|||||||
return NewTagReaderWriter(t.tx)
|
return NewTagReaderWriter(t.tx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *transaction) SavedFilter() models.SavedFilterReaderWriter {
|
||||||
|
t.ensureTx()
|
||||||
|
return NewSavedFilterReaderWriter(t.tx)
|
||||||
|
}
|
||||||
|
|
||||||
type ReadTransaction struct{}
|
type ReadTransaction struct{}
|
||||||
|
|
||||||
func (t *ReadTransaction) Begin() error {
|
func (t *ReadTransaction) Begin() error {
|
||||||
@@ -183,6 +188,10 @@ func (t *ReadTransaction) Tag() models.TagReader {
|
|||||||
return NewTagReaderWriter(database.DB)
|
return NewTagReaderWriter(database.DB)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *ReadTransaction) SavedFilter() models.SavedFilterReader {
|
||||||
|
return NewSavedFilterReaderWriter(database.DB)
|
||||||
|
}
|
||||||
|
|
||||||
type TransactionManager struct {
|
type TransactionManager struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
### ✨ New Features
|
### ✨ New Features
|
||||||
|
* Added support for saved and default filters. ([#1474](https://github.com/stashapp/stash/pull/1474))
|
||||||
* Added merge tags functionality. ([#1481](https://github.com/stashapp/stash/pull/1481))
|
* Added merge tags functionality. ([#1481](https://github.com/stashapp/stash/pull/1481))
|
||||||
* Added support for triggering plugin tasks during operations. ([#1452](https://github.com/stashapp/stash/pull/1452))
|
* Added support for triggering plugin tasks during operations. ([#1452](https://github.com/stashapp/stash/pull/1452))
|
||||||
* Support Studio filter including child studios. ([#1397](https://github.com/stashapp/stash/pull/1397))
|
* Support Studio filter including child studios. ([#1397](https://github.com/stashapp/stash/pull/1397))
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export const GalleryAddPanel: React.FC<IGalleryAddProps> = ({ gallery }) => {
|
|||||||
};
|
};
|
||||||
// if galleries is already present, then we modify it, otherwise add
|
// if galleries is already present, then we modify it, otherwise add
|
||||||
let galleryCriterion = filter.criteria.find((c) => {
|
let galleryCriterion = filter.criteria.find((c) => {
|
||||||
return c.criterionOption.value === "galleries";
|
return c.criterionOption.type === "galleries";
|
||||||
}) as GalleriesCriterion;
|
}) as GalleriesCriterion;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export const GalleryImagesPanel: React.FC<IGalleryDetailsProps> = ({
|
|||||||
};
|
};
|
||||||
// if galleries is already present, then we modify it, otherwise add
|
// if galleries is already present, then we modify it, otherwise add
|
||||||
let galleryCriterion = filter.criteria.find((c) => {
|
let galleryCriterion = filter.criteria.find((c) => {
|
||||||
return c.criterionOption.value === "galleries";
|
return c.criterionOption.type === "galleries";
|
||||||
}) as GalleriesCriterion;
|
}) as GalleriesCriterion;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import React, { useEffect, useRef, useState } from "react";
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
import { Button, Form, Modal, OverlayTrigger, Tooltip } from "react-bootstrap";
|
import { Button, Form, Modal } from "react-bootstrap";
|
||||||
import Mousetrap from "mousetrap";
|
import { FilterSelect, DurationInput } from "src/components/Shared";
|
||||||
import { Icon, FilterSelect, DurationInput } from "src/components/Shared";
|
|
||||||
import { CriterionModifier } from "src/core/generated-graphql";
|
import { CriterionModifier } from "src/core/generated-graphql";
|
||||||
import {
|
import {
|
||||||
DurationCriterion,
|
DurationCriterion,
|
||||||
@@ -10,7 +9,10 @@ import {
|
|||||||
Criterion,
|
Criterion,
|
||||||
IHierarchicalLabeledIdCriterion,
|
IHierarchicalLabeledIdCriterion,
|
||||||
} from "src/models/list-filter/criteria/criterion";
|
} from "src/models/list-filter/criteria/criterion";
|
||||||
import { NoneCriterion } from "src/models/list-filter/criteria/none";
|
import {
|
||||||
|
NoneCriterion,
|
||||||
|
NoneCriterionOption,
|
||||||
|
} from "src/models/list-filter/criteria/none";
|
||||||
import { makeCriteria } from "src/models/list-filter/criteria/factory";
|
import { makeCriteria } from "src/models/list-filter/criteria/factory";
|
||||||
import { ListFilterOptions } from "src/models/list-filter/filter-options";
|
import { ListFilterOptions } from "src/models/list-filter/filter-options";
|
||||||
import { defineMessages, FormattedMessage, useIntl } from "react-intl";
|
import { defineMessages, FormattedMessage, useIntl } from "react-intl";
|
||||||
@@ -29,15 +31,18 @@ interface IAddFilterProps {
|
|||||||
editingCriterion?: Criterion<CriterionValue>;
|
editingCriterion?: Criterion<CriterionValue>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AddFilter: React.FC<IAddFilterProps> = (
|
export const AddFilterDialog: React.FC<IAddFilterProps> = ({
|
||||||
props: IAddFilterProps
|
onAddCriterion,
|
||||||
) => {
|
onCancel,
|
||||||
|
filterOptions,
|
||||||
|
editingCriterion,
|
||||||
|
}) => {
|
||||||
const defaultValue = useRef<string | number | undefined>();
|
const defaultValue = useRef<string | number | undefined>();
|
||||||
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
const [criterion, setCriterion] = useState<Criterion<CriterionValue>>(
|
const [criterion, setCriterion] = useState<Criterion<CriterionValue>>(
|
||||||
new NoneCriterion()
|
new NoneCriterion()
|
||||||
);
|
);
|
||||||
|
const { options, modifierOptions } = criterion.criterionOption;
|
||||||
|
|
||||||
const valueStage = useRef<CriterionValue>(criterion.value);
|
const valueStage = useRef<CriterionValue>(criterion.value);
|
||||||
|
|
||||||
@@ -50,23 +55,14 @@ export const AddFilter: React.FC<IAddFilterProps> = (
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// configure keyboard shortcuts
|
|
||||||
useEffect(() => {
|
|
||||||
Mousetrap.bind("f", () => setIsOpen(true));
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
Mousetrap.unbind("f");
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// Configure if we are editing an existing criterion
|
// Configure if we are editing an existing criterion
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!props.editingCriterion) {
|
if (!editingCriterion) {
|
||||||
return;
|
setCriterion(makeCriteria());
|
||||||
|
} else {
|
||||||
|
setCriterion(editingCriterion);
|
||||||
}
|
}
|
||||||
setIsOpen(true);
|
}, [editingCriterion]);
|
||||||
setCriterion(props.editingCriterion);
|
|
||||||
}, [props.editingCriterion]);
|
|
||||||
|
|
||||||
function onChangedCriteriaType(event: React.ChangeEvent<HTMLSelectElement>) {
|
function onChangedCriteriaType(event: React.ChangeEvent<HTMLSelectElement>) {
|
||||||
const newCriterionType = event.target.value as CriterionType;
|
const newCriterionType = event.target.value as CriterionType;
|
||||||
@@ -107,38 +103,27 @@ export const AddFilter: React.FC<IAddFilterProps> = (
|
|||||||
if (!Array.isArray(criterion.value) && defaultValue.current !== undefined) {
|
if (!Array.isArray(criterion.value) && defaultValue.current !== undefined) {
|
||||||
const value = defaultValue.current;
|
const value = defaultValue.current;
|
||||||
if (
|
if (
|
||||||
criterion.options &&
|
options &&
|
||||||
(value === undefined || value === "" || typeof value === "number")
|
(value === undefined || value === "" || typeof value === "number")
|
||||||
) {
|
) {
|
||||||
criterion.value = criterion.options[0].toString();
|
criterion.value = options[0].toString();
|
||||||
} else if (typeof value === "number" && value === undefined) {
|
} else if (typeof value === "number" && value === undefined) {
|
||||||
criterion.value = 0;
|
criterion.value = 0;
|
||||||
} else if (value === undefined) {
|
} else if (value === undefined) {
|
||||||
criterion.value = "";
|
criterion.value = "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const oldId = props.editingCriterion
|
const oldId = editingCriterion ? editingCriterion.getId() : undefined;
|
||||||
? props.editingCriterion.getId()
|
onAddCriterion(criterion, oldId);
|
||||||
: undefined;
|
|
||||||
props.onAddCriterion(criterion, oldId);
|
|
||||||
onToggle();
|
|
||||||
}
|
|
||||||
|
|
||||||
function onToggle() {
|
|
||||||
if (isOpen) {
|
|
||||||
props.onCancel();
|
|
||||||
}
|
|
||||||
setIsOpen(!isOpen);
|
|
||||||
setCriterion(makeCriteria());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const maybeRenderFilterPopoverContents = () => {
|
const maybeRenderFilterPopoverContents = () => {
|
||||||
if (criterion.criterionOption.value === "none") {
|
if (criterion.criterionOption.type === "none") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderModifier() {
|
function renderModifier() {
|
||||||
if (criterion.modifierOptions.length === 0) {
|
if (modifierOptions.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
@@ -148,9 +133,9 @@ export const AddFilter: React.FC<IAddFilterProps> = (
|
|||||||
value={criterion.modifier}
|
value={criterion.modifier}
|
||||||
className="btn-secondary"
|
className="btn-secondary"
|
||||||
>
|
>
|
||||||
{criterion.modifierOptions.map((c) => (
|
{modifierOptions.map((c) => (
|
||||||
<option key={c.value} value={c.value}>
|
<option key={c.value} value={c.value}>
|
||||||
{c.label}
|
{c.label ? intl.formatMessage({ id: c.label }) : ""}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</Form.Control>
|
</Form.Control>
|
||||||
@@ -168,19 +153,19 @@ export const AddFilter: React.FC<IAddFilterProps> = (
|
|||||||
|
|
||||||
if (Array.isArray(criterion.value)) {
|
if (Array.isArray(criterion.value)) {
|
||||||
if (
|
if (
|
||||||
criterion.criterionOption.value !== "performers" &&
|
criterion.criterionOption.type !== "performers" &&
|
||||||
criterion.criterionOption.value !== "studios" &&
|
criterion.criterionOption.type !== "studios" &&
|
||||||
criterion.criterionOption.value !== "parent_studios" &&
|
criterion.criterionOption.type !== "parent_studios" &&
|
||||||
criterion.criterionOption.value !== "tags" &&
|
criterion.criterionOption.type !== "tags" &&
|
||||||
criterion.criterionOption.value !== "sceneTags" &&
|
criterion.criterionOption.type !== "sceneTags" &&
|
||||||
criterion.criterionOption.value !== "performerTags" &&
|
criterion.criterionOption.type !== "performerTags" &&
|
||||||
criterion.criterionOption.value !== "movies"
|
criterion.criterionOption.type !== "movies"
|
||||||
)
|
)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FilterSelect
|
<FilterSelect
|
||||||
type={criterion.criterionOption.value}
|
type={criterion.criterionOption.type}
|
||||||
isMulti
|
isMulti
|
||||||
onSelect={(items) => {
|
onSelect={(items) => {
|
||||||
const newCriterion = _.cloneDeep(criterion);
|
const newCriterion = _.cloneDeep(criterion);
|
||||||
@@ -195,11 +180,11 @@ export const AddFilter: React.FC<IAddFilterProps> = (
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (criterion instanceof IHierarchicalLabeledIdCriterion) {
|
if (criterion instanceof IHierarchicalLabeledIdCriterion) {
|
||||||
if (criterion.criterionOption.value !== "studios") return;
|
if (criterion.criterionOption.type !== "studios") return;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FilterSelect
|
<FilterSelect
|
||||||
type={criterion.criterionOption.value}
|
type={criterion.criterionOption.type}
|
||||||
isMulti
|
isMulti
|
||||||
onSelect={(items) => {
|
onSelect={(items) => {
|
||||||
const newCriterion = _.cloneDeep(criterion);
|
const newCriterion = _.cloneDeep(criterion);
|
||||||
@@ -213,10 +198,7 @@ export const AddFilter: React.FC<IAddFilterProps> = (
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (
|
if (options && !criterionIsHierarchicalLabelValue(criterion.value)) {
|
||||||
criterion.options &&
|
|
||||||
!criterionIsHierarchicalLabelValue(criterion.value)
|
|
||||||
) {
|
|
||||||
defaultValue.current = criterion.value;
|
defaultValue.current = criterion.value;
|
||||||
return (
|
return (
|
||||||
<Form.Control
|
<Form.Control
|
||||||
@@ -225,7 +207,7 @@ export const AddFilter: React.FC<IAddFilterProps> = (
|
|||||||
value={criterion.value.toString()}
|
value={criterion.value.toString()}
|
||||||
className="btn-secondary"
|
className="btn-secondary"
|
||||||
>
|
>
|
||||||
{criterion.options.map((c) => (
|
{options.map((c) => (
|
||||||
<option key={c.toString()} value={c.toString()}>
|
<option key={c.toString()} value={c.toString()}>
|
||||||
{c}
|
{c}
|
||||||
</option>
|
</option>
|
||||||
@@ -245,7 +227,7 @@ export const AddFilter: React.FC<IAddFilterProps> = (
|
|||||||
return (
|
return (
|
||||||
<Form.Control
|
<Form.Control
|
||||||
className="btn-secondary"
|
className="btn-secondary"
|
||||||
type={criterion.inputType}
|
type={criterion.criterionOption.inputType}
|
||||||
onChange={onChangedInput}
|
onChange={onChangedInput}
|
||||||
onBlur={onBlurInput}
|
onBlur={onBlurInput}
|
||||||
defaultValue={criterion.value ? criterion.value.toString() : ""}
|
defaultValue={criterion.value ? criterion.value.toString() : ""}
|
||||||
@@ -259,7 +241,7 @@ export const AddFilter: React.FC<IAddFilterProps> = (
|
|||||||
<Form.Group>
|
<Form.Group>
|
||||||
<Form.Check
|
<Form.Check
|
||||||
checked={criterion.value.depth !== 0}
|
checked={criterion.value.depth !== 0}
|
||||||
label="Include child studios"
|
label={intl.formatMessage({ id: "include_child_studios" })}
|
||||||
onChange={() => {
|
onChange={() => {
|
||||||
const newCriterion = _.cloneDeep(criterion);
|
const newCriterion = _.cloneDeep(criterion);
|
||||||
newCriterion.value.depth =
|
newCriterion.value.depth =
|
||||||
@@ -304,7 +286,7 @@ export const AddFilter: React.FC<IAddFilterProps> = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
function maybeRenderFilterCriterion() {
|
function maybeRenderFilterCriterion() {
|
||||||
if (!props.editingCriterion) {
|
if (!editingCriterion) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -312,7 +294,7 @@ export const AddFilter: React.FC<IAddFilterProps> = (
|
|||||||
<Form.Group>
|
<Form.Group>
|
||||||
<strong>
|
<strong>
|
||||||
{intl.formatMessage({
|
{intl.formatMessage({
|
||||||
id: props.editingCriterion.criterionOption.messageID,
|
id: editingCriterion.criterionOption.messageID,
|
||||||
})}
|
})}
|
||||||
</strong>
|
</strong>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
@@ -320,14 +302,15 @@ export const AddFilter: React.FC<IAddFilterProps> = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
function maybeRenderFilterSelect() {
|
function maybeRenderFilterSelect() {
|
||||||
if (props.editingCriterion) {
|
if (editingCriterion) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const options = props.filterOptions.criterionOptions
|
const thisOptions = [NoneCriterionOption]
|
||||||
|
.concat(filterOptions.criterionOptions)
|
||||||
.map((c) => {
|
.map((c) => {
|
||||||
return {
|
return {
|
||||||
value: c.value,
|
value: c.type,
|
||||||
text: intl.formatMessage({ id: c.messageID }),
|
text: intl.formatMessage({ id: c.messageID }),
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
@@ -345,10 +328,10 @@ export const AddFilter: React.FC<IAddFilterProps> = (
|
|||||||
<Form.Control
|
<Form.Control
|
||||||
as="select"
|
as="select"
|
||||||
onChange={onChangedCriteriaType}
|
onChange={onChangedCriteriaType}
|
||||||
value={criterion.criterionOption.value}
|
value={criterion.criterionOption.type}
|
||||||
className="btn-secondary"
|
className="btn-secondary"
|
||||||
>
|
>
|
||||||
{options.map((c) => (
|
{thisOptions.map((c) => (
|
||||||
<option key={c.value} value={c.value} disabled={c.value === "none"}>
|
<option key={c.value} value={c.value} disabled={c.value === "none"}>
|
||||||
{c.text}
|
{c.text}
|
||||||
</option>
|
</option>
|
||||||
@@ -358,25 +341,12 @@ export const AddFilter: React.FC<IAddFilterProps> = (
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const title = !props.editingCriterion
|
const title = !editingCriterion
|
||||||
? intl.formatMessage({ id: "search_filter.add_filter" })
|
? intl.formatMessage({ id: "search_filter.add_filter" })
|
||||||
: intl.formatMessage({ id: "search_filter.update_filter" });
|
: intl.formatMessage({ id: "search_filter.update_filter" });
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<OverlayTrigger
|
<Modal show onHide={() => onCancel()}>
|
||||||
placement="top"
|
|
||||||
overlay={
|
|
||||||
<Tooltip id="filter-tooltip">
|
|
||||||
<FormattedMessage id="search_filter.name" />
|
|
||||||
</Tooltip>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Button variant="secondary" onClick={() => onToggle()} active={isOpen}>
|
|
||||||
<Icon icon="filter" />
|
|
||||||
</Button>
|
|
||||||
</OverlayTrigger>
|
|
||||||
|
|
||||||
<Modal show={isOpen} onHide={() => onToggle()}>
|
|
||||||
<Modal.Header>{title}</Modal.Header>
|
<Modal.Header>{title}</Modal.Header>
|
||||||
<Modal.Body>
|
<Modal.Body>
|
||||||
<div className="dialog-content">
|
<div className="dialog-content">
|
||||||
@@ -388,7 +358,7 @@ export const AddFilter: React.FC<IAddFilterProps> = (
|
|||||||
<Modal.Footer>
|
<Modal.Footer>
|
||||||
<Button
|
<Button
|
||||||
onClick={onAddFilter}
|
onClick={onAddFilter}
|
||||||
disabled={criterion.criterionOption.value === "none"}
|
disabled={criterion.criterionOption.type === "none"}
|
||||||
>
|
>
|
||||||
{title}
|
{title}
|
||||||
</Button>
|
</Button>
|
||||||
60
ui/v2.5/src/components/List/FilterTags.tsx
Normal file
60
ui/v2.5/src/components/List/FilterTags.tsx
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Badge, Button } from "react-bootstrap";
|
||||||
|
import {
|
||||||
|
Criterion,
|
||||||
|
CriterionValue,
|
||||||
|
} from "src/models/list-filter/criteria/criterion";
|
||||||
|
import { useIntl } from "react-intl";
|
||||||
|
import { Icon } from "../Shared";
|
||||||
|
|
||||||
|
interface IFilterTagsProps {
|
||||||
|
criteria: Criterion<CriterionValue>[];
|
||||||
|
onEditCriterion: (c: Criterion<CriterionValue>) => void;
|
||||||
|
onRemoveCriterion: (c: Criterion<CriterionValue>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FilterTags: React.FC<IFilterTagsProps> = ({
|
||||||
|
criteria,
|
||||||
|
onEditCriterion,
|
||||||
|
onRemoveCriterion,
|
||||||
|
}) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
function onRemoveCriterionTag(
|
||||||
|
criterion: Criterion<CriterionValue>,
|
||||||
|
$event: React.MouseEvent<HTMLElement, MouseEvent>
|
||||||
|
) {
|
||||||
|
if (!criterion) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onRemoveCriterion(criterion);
|
||||||
|
$event.stopPropagation();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onClickCriterionTag(criterion: Criterion<CriterionValue>) {
|
||||||
|
onEditCriterion(criterion);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderFilterTags() {
|
||||||
|
return criteria.map((criterion) => (
|
||||||
|
<Badge
|
||||||
|
className="tag-item"
|
||||||
|
variant="secondary"
|
||||||
|
key={criterion.getId()}
|
||||||
|
onClick={() => onClickCriterionTag(criterion)}
|
||||||
|
>
|
||||||
|
{criterion.getLabel(intl)}
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
onClick={($event) => onRemoveCriterionTag(criterion, $event)}
|
||||||
|
>
|
||||||
|
<Icon icon="times" />
|
||||||
|
</Button>
|
||||||
|
</Badge>
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="d-flex justify-content-center">{renderFilterTags()}</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
import _, { debounce } from "lodash";
|
import _, { debounce } from "lodash";
|
||||||
import React, { useState, useEffect } from "react";
|
import React, { HTMLAttributes, useEffect } from "react";
|
||||||
import Mousetrap from "mousetrap";
|
import Mousetrap from "mousetrap";
|
||||||
import { SortDirectionEnum } from "src/core/generated-graphql";
|
import { SortDirectionEnum } from "src/core/generated-graphql";
|
||||||
import {
|
import {
|
||||||
Badge,
|
|
||||||
Button,
|
Button,
|
||||||
ButtonGroup,
|
ButtonGroup,
|
||||||
Dropdown,
|
Dropdown,
|
||||||
@@ -12,61 +11,44 @@ import {
|
|||||||
Tooltip,
|
Tooltip,
|
||||||
InputGroup,
|
InputGroup,
|
||||||
FormControl,
|
FormControl,
|
||||||
ButtonToolbar,
|
|
||||||
} from "react-bootstrap";
|
} from "react-bootstrap";
|
||||||
|
|
||||||
import { Icon } from "src/components/Shared";
|
import { Icon } from "src/components/Shared";
|
||||||
import { ListFilterModel } from "src/models/list-filter/filter";
|
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||||
import { DisplayMode } from "src/models/list-filter/types";
|
|
||||||
import { useFocus } from "src/utils";
|
import { useFocus } from "src/utils";
|
||||||
import { ListFilterOptions } from "src/models/list-filter/filter-options";
|
import { ListFilterOptions } from "src/models/list-filter/filter-options";
|
||||||
import { FormattedMessage, useIntl } from "react-intl";
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
import {
|
import { PersistanceLevel } from "src/hooks/ListHook";
|
||||||
Criterion,
|
import { SavedFilterList } from "./SavedFilterList";
|
||||||
CriterionValue,
|
|
||||||
} from "src/models/list-filter/criteria/criterion";
|
|
||||||
import { AddFilter } from "./AddFilter";
|
|
||||||
|
|
||||||
interface IListFilterOperation {
|
|
||||||
text: string;
|
|
||||||
onClick: () => void;
|
|
||||||
isDisplayed?: () => boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IListFilterProps {
|
interface IListFilterProps {
|
||||||
onFilterUpdate: (newFilter: ListFilterModel) => void;
|
onFilterUpdate: (newFilter: ListFilterModel) => void;
|
||||||
zoomIndex?: number;
|
|
||||||
onChangeZoom?: (zoomIndex: number) => void;
|
|
||||||
onSelectAll?: () => void;
|
|
||||||
onSelectNone?: () => void;
|
|
||||||
onEdit?: () => void;
|
|
||||||
onDelete?: () => void;
|
|
||||||
otherOperations?: IListFilterOperation[];
|
|
||||||
filter: ListFilterModel;
|
filter: ListFilterModel;
|
||||||
filterOptions: ListFilterOptions;
|
filterOptions: ListFilterOptions;
|
||||||
itemsSelected?: boolean;
|
filterDialogOpen?: boolean;
|
||||||
|
persistState?: PersistanceLevel;
|
||||||
|
openFilterDialog: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PAGE_SIZE_OPTIONS = ["20", "40", "60", "120", "250", "500", "1000"];
|
const PAGE_SIZE_OPTIONS = ["20", "40", "60", "120", "250", "500", "1000"];
|
||||||
const minZoom = 0;
|
|
||||||
const maxZoom = 3;
|
|
||||||
|
|
||||||
export const ListFilter: React.FC<IListFilterProps> = (
|
export const ListFilter: React.FC<IListFilterProps> = ({
|
||||||
props: IListFilterProps
|
onFilterUpdate,
|
||||||
) => {
|
filter,
|
||||||
|
filterOptions,
|
||||||
|
filterDialogOpen,
|
||||||
|
openFilterDialog,
|
||||||
|
persistState,
|
||||||
|
}) => {
|
||||||
const [queryRef, setQueryFocus] = useFocus();
|
const [queryRef, setQueryFocus] = useFocus();
|
||||||
|
|
||||||
const searchCallback = debounce((value: string) => {
|
const searchCallback = debounce((value: string) => {
|
||||||
const newFilter = _.cloneDeep(props.filter);
|
const newFilter = _.cloneDeep(filter);
|
||||||
newFilter.searchTerm = value;
|
newFilter.searchTerm = value;
|
||||||
newFilter.currentPage = 1;
|
newFilter.currentPage = 1;
|
||||||
props.onFilterUpdate(newFilter);
|
onFilterUpdate(newFilter);
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
||||||
const [editingCriterion, setEditingCriterion] = useState<
|
|
||||||
Criterion<CriterionValue> | undefined
|
|
||||||
>(undefined);
|
|
||||||
|
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -76,81 +58,20 @@ export const ListFilter: React.FC<IListFilterProps> = (
|
|||||||
});
|
});
|
||||||
|
|
||||||
Mousetrap.bind("r", () => onReshuffleRandomSort());
|
Mousetrap.bind("r", () => onReshuffleRandomSort());
|
||||||
Mousetrap.bind("v g", () => {
|
|
||||||
if (props.filterOptions.displayModeOptions.includes(DisplayMode.Grid)) {
|
|
||||||
onChangeDisplayMode(DisplayMode.Grid);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
Mousetrap.bind("v l", () => {
|
|
||||||
if (props.filterOptions.displayModeOptions.includes(DisplayMode.List)) {
|
|
||||||
onChangeDisplayMode(DisplayMode.List);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
Mousetrap.bind("v w", () => {
|
|
||||||
if (props.filterOptions.displayModeOptions.includes(DisplayMode.Wall)) {
|
|
||||||
onChangeDisplayMode(DisplayMode.Wall);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
Mousetrap.bind("+", () => {
|
|
||||||
if (
|
|
||||||
props.onChangeZoom &&
|
|
||||||
props.zoomIndex !== undefined &&
|
|
||||||
props.zoomIndex < maxZoom
|
|
||||||
) {
|
|
||||||
props.onChangeZoom(props.zoomIndex + 1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
Mousetrap.bind("-", () => {
|
|
||||||
if (
|
|
||||||
props.onChangeZoom &&
|
|
||||||
props.zoomIndex !== undefined &&
|
|
||||||
props.zoomIndex > minZoom
|
|
||||||
) {
|
|
||||||
props.onChangeZoom(props.zoomIndex - 1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
Mousetrap.bind("s a", () => onSelectAll());
|
|
||||||
Mousetrap.bind("s n", () => onSelectNone());
|
|
||||||
|
|
||||||
if (props.itemsSelected) {
|
|
||||||
Mousetrap.bind("e", () => {
|
|
||||||
if (props.onEdit) {
|
|
||||||
props.onEdit();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Mousetrap.bind("d d", () => {
|
|
||||||
if (props.onDelete) {
|
|
||||||
props.onDelete();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
Mousetrap.unbind("/");
|
Mousetrap.unbind("/");
|
||||||
Mousetrap.unbind("r");
|
Mousetrap.unbind("r");
|
||||||
Mousetrap.unbind("v g");
|
|
||||||
Mousetrap.unbind("v l");
|
|
||||||
Mousetrap.unbind("v w");
|
|
||||||
Mousetrap.unbind("+");
|
|
||||||
Mousetrap.unbind("-");
|
|
||||||
Mousetrap.unbind("s a");
|
|
||||||
Mousetrap.unbind("s n");
|
|
||||||
|
|
||||||
if (props.itemsSelected) {
|
|
||||||
Mousetrap.unbind("e");
|
|
||||||
Mousetrap.unbind("d d");
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
function onChangePageSize(event: React.ChangeEvent<HTMLSelectElement>) {
|
function onChangePageSize(event: React.ChangeEvent<HTMLSelectElement>) {
|
||||||
const val = event.currentTarget.value;
|
const val = event.currentTarget.value;
|
||||||
|
|
||||||
const newFilter = _.cloneDeep(props.filter);
|
const newFilter = _.cloneDeep(filter);
|
||||||
newFilter.itemsPerPage = parseInt(val, 10);
|
newFilter.itemsPerPage = parseInt(val, 10);
|
||||||
newFilter.currentPage = 1;
|
newFilter.currentPage = 1;
|
||||||
props.onFilterUpdate(newFilter);
|
onFilterUpdate(newFilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onChangeQuery(event: React.FormEvent<HTMLInputElement>) {
|
function onChangeQuery(event: React.FormEvent<HTMLInputElement>) {
|
||||||
@@ -158,95 +79,32 @@ export const ListFilter: React.FC<IListFilterProps> = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onChangeSortDirection() {
|
function onChangeSortDirection() {
|
||||||
const newFilter = _.cloneDeep(props.filter);
|
const newFilter = _.cloneDeep(filter);
|
||||||
if (props.filter.sortDirection === SortDirectionEnum.Asc) {
|
if (filter.sortDirection === SortDirectionEnum.Asc) {
|
||||||
newFilter.sortDirection = SortDirectionEnum.Desc;
|
newFilter.sortDirection = SortDirectionEnum.Desc;
|
||||||
} else {
|
} else {
|
||||||
newFilter.sortDirection = SortDirectionEnum.Asc;
|
newFilter.sortDirection = SortDirectionEnum.Asc;
|
||||||
}
|
}
|
||||||
|
|
||||||
props.onFilterUpdate(newFilter);
|
onFilterUpdate(newFilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onChangeSortBy(eventKey: string | null) {
|
function onChangeSortBy(eventKey: string | null) {
|
||||||
const newFilter = _.cloneDeep(props.filter);
|
const newFilter = _.cloneDeep(filter);
|
||||||
newFilter.sortBy = eventKey ?? undefined;
|
newFilter.sortBy = eventKey ?? undefined;
|
||||||
newFilter.currentPage = 1;
|
newFilter.currentPage = 1;
|
||||||
props.onFilterUpdate(newFilter);
|
onFilterUpdate(newFilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onReshuffleRandomSort() {
|
function onReshuffleRandomSort() {
|
||||||
const newFilter = _.cloneDeep(props.filter);
|
const newFilter = _.cloneDeep(filter);
|
||||||
newFilter.currentPage = 1;
|
newFilter.currentPage = 1;
|
||||||
newFilter.randomSeed = -1;
|
newFilter.randomSeed = -1;
|
||||||
props.onFilterUpdate(newFilter);
|
onFilterUpdate(newFilter);
|
||||||
}
|
|
||||||
|
|
||||||
function onChangeDisplayMode(displayMode: DisplayMode) {
|
|
||||||
const newFilter = _.cloneDeep(props.filter);
|
|
||||||
newFilter.displayMode = displayMode;
|
|
||||||
props.onFilterUpdate(newFilter);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onAddCriterion(
|
|
||||||
criterion: Criterion<CriterionValue>,
|
|
||||||
oldId?: string
|
|
||||||
) {
|
|
||||||
const newFilter = _.cloneDeep(props.filter);
|
|
||||||
|
|
||||||
// Find if we are editing an existing criteria, then modify that. Or create a new one.
|
|
||||||
const existingIndex = newFilter.criteria.findIndex((c) => {
|
|
||||||
// If we modified an existing criterion, then look for the old id.
|
|
||||||
const id = oldId || criterion.getId();
|
|
||||||
return c.getId() === id;
|
|
||||||
});
|
|
||||||
if (existingIndex === -1) {
|
|
||||||
newFilter.criteria.push(criterion);
|
|
||||||
} else {
|
|
||||||
newFilter.criteria[existingIndex] = criterion;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove duplicate modifiers
|
|
||||||
newFilter.criteria = newFilter.criteria.filter((obj, pos, arr) => {
|
|
||||||
return arr.map((mapObj) => mapObj.getId()).indexOf(obj.getId()) === pos;
|
|
||||||
});
|
|
||||||
|
|
||||||
newFilter.currentPage = 1;
|
|
||||||
props.onFilterUpdate(newFilter);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onCancelAddCriterion() {
|
|
||||||
setEditingCriterion(undefined);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onRemoveCriterion(removedCriterion: Criterion<CriterionValue>) {
|
|
||||||
const newFilter = _.cloneDeep(props.filter);
|
|
||||||
newFilter.criteria = newFilter.criteria.filter(
|
|
||||||
(criterion) => criterion.getId() !== removedCriterion.getId()
|
|
||||||
);
|
|
||||||
newFilter.currentPage = 1;
|
|
||||||
props.onFilterUpdate(newFilter);
|
|
||||||
}
|
|
||||||
|
|
||||||
let removedCriterionId = "";
|
|
||||||
function onRemoveCriterionTag(criterion?: Criterion<CriterionValue>) {
|
|
||||||
if (!criterion) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setEditingCriterion(undefined);
|
|
||||||
removedCriterionId = criterion.getId();
|
|
||||||
onRemoveCriterion(criterion);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onClickCriterionTag(criterion?: Criterion<CriterionValue>) {
|
|
||||||
if (!criterion || removedCriterionId !== "") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setEditingCriterion(criterion);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderSortByOptions() {
|
function renderSortByOptions() {
|
||||||
return props.filterOptions.sortByOptions
|
return filterOptions.sortByOptions
|
||||||
.map((o) => {
|
.map((o) => {
|
||||||
return {
|
return {
|
||||||
message: intl.formatMessage({ id: o.messageID }),
|
message: intl.formatMessage({ id: o.messageID }),
|
||||||
@@ -266,323 +124,134 @@ export const ListFilter: React.FC<IListFilterProps> = (
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderDisplayModeOptions() {
|
const SavedFilterDropdown = React.forwardRef<
|
||||||
function getIcon(option: DisplayMode) {
|
HTMLDivElement,
|
||||||
switch (option) {
|
HTMLAttributes<HTMLDivElement>
|
||||||
case DisplayMode.Grid:
|
>(({ style, className }, ref) => (
|
||||||
return "th-large";
|
<div ref={ref} style={style} className={className}>
|
||||||
case DisplayMode.List:
|
<SavedFilterList
|
||||||
return "list";
|
filter={filter}
|
||||||
case DisplayMode.Wall:
|
onSetFilter={(f) => {
|
||||||
return "square";
|
onFilterUpdate(f);
|
||||||
case DisplayMode.Tagger:
|
}}
|
||||||
return "tags";
|
persistState={persistState}
|
||||||
}
|
/>
|
||||||
}
|
</div>
|
||||||
function getLabel(option: DisplayMode) {
|
));
|
||||||
let displayModeId = "unknown";
|
|
||||||
switch (option) {
|
|
||||||
case DisplayMode.Grid:
|
|
||||||
displayModeId = "grid";
|
|
||||||
break;
|
|
||||||
case DisplayMode.List:
|
|
||||||
displayModeId = "list";
|
|
||||||
break;
|
|
||||||
case DisplayMode.Wall:
|
|
||||||
displayModeId = "wall";
|
|
||||||
break;
|
|
||||||
case DisplayMode.Tagger:
|
|
||||||
displayModeId = "tagger";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return intl.formatMessage({ id: `display_mode.${displayModeId}` });
|
|
||||||
}
|
|
||||||
|
|
||||||
return props.filterOptions.displayModeOptions.map((option) => (
|
|
||||||
<OverlayTrigger
|
|
||||||
key={option}
|
|
||||||
overlay={
|
|
||||||
<Tooltip id="display-mode-tooltip">{getLabel(option)}</Tooltip>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
active={props.filter.displayMode === option}
|
|
||||||
onClick={() => onChangeDisplayMode(option)}
|
|
||||||
>
|
|
||||||
<Icon icon={getIcon(option)} />
|
|
||||||
</Button>
|
|
||||||
</OverlayTrigger>
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderFilterTags() {
|
|
||||||
return props.filter.criteria.map((criterion) => (
|
|
||||||
<Badge
|
|
||||||
className="tag-item"
|
|
||||||
variant="secondary"
|
|
||||||
key={criterion.getId()}
|
|
||||||
onClick={() => onClickCriterionTag(criterion)}
|
|
||||||
>
|
|
||||||
{criterion.getLabel(intl)}
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
onClick={() => onRemoveCriterionTag(criterion)}
|
|
||||||
>
|
|
||||||
<Icon icon="times" />
|
|
||||||
</Button>
|
|
||||||
</Badge>
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
function onSelectAll() {
|
|
||||||
if (props.onSelectAll) {
|
|
||||||
props.onSelectAll();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onSelectNone() {
|
|
||||||
if (props.onSelectNone) {
|
|
||||||
props.onSelectNone();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onEdit() {
|
|
||||||
if (props.onEdit) {
|
|
||||||
props.onEdit();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onDelete() {
|
|
||||||
if (props.onDelete) {
|
|
||||||
props.onDelete();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderSelectAll() {
|
|
||||||
if (props.onSelectAll) {
|
|
||||||
return (
|
|
||||||
<Dropdown.Item
|
|
||||||
key="select-all"
|
|
||||||
className="bg-secondary text-white"
|
|
||||||
onClick={() => onSelectAll()}
|
|
||||||
>
|
|
||||||
<FormattedMessage id="actions.select_all" />
|
|
||||||
</Dropdown.Item>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderSelectNone() {
|
|
||||||
if (props.onSelectNone) {
|
|
||||||
return (
|
|
||||||
<Dropdown.Item
|
|
||||||
key="select-none"
|
|
||||||
className="bg-secondary text-white"
|
|
||||||
onClick={() => onSelectNone()}
|
|
||||||
>
|
|
||||||
<FormattedMessage id="actions.select_none" />
|
|
||||||
</Dropdown.Item>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderMore() {
|
|
||||||
const options = [renderSelectAll(), renderSelectNone()].filter((o) => o);
|
|
||||||
|
|
||||||
if (props.otherOperations) {
|
|
||||||
props.otherOperations
|
|
||||||
.filter((o) => {
|
|
||||||
if (!o.isDisplayed) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return o.isDisplayed();
|
|
||||||
})
|
|
||||||
.forEach((o) => {
|
|
||||||
options.push(
|
|
||||||
<Dropdown.Item
|
|
||||||
key={o.text}
|
|
||||||
className="bg-secondary text-white"
|
|
||||||
onClick={o.onClick}
|
|
||||||
>
|
|
||||||
{o.text}
|
|
||||||
</Dropdown.Item>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.length > 0) {
|
|
||||||
return (
|
|
||||||
<Dropdown>
|
|
||||||
<Dropdown.Toggle variant="secondary" id="more-menu">
|
|
||||||
<Icon icon="ellipsis-h" />
|
|
||||||
</Dropdown.Toggle>
|
|
||||||
<Dropdown.Menu className="bg-secondary text-white">
|
|
||||||
{options}
|
|
||||||
</Dropdown.Menu>
|
|
||||||
</Dropdown>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onChangeZoom(v: number) {
|
|
||||||
if (props.onChangeZoom) {
|
|
||||||
props.onChangeZoom(v);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function maybeRenderZoom() {
|
|
||||||
if (props.onChangeZoom && props.filter.displayMode === DisplayMode.Grid) {
|
|
||||||
return (
|
|
||||||
<div className="align-middle">
|
|
||||||
<Form.Control
|
|
||||||
className="zoom-slider d-none d-sm-inline-flex ml-3"
|
|
||||||
type="range"
|
|
||||||
min={minZoom}
|
|
||||||
max={maxZoom}
|
|
||||||
value={props.zoomIndex}
|
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
|
||||||
onChangeZoom(Number.parseInt(e.currentTarget.value, 10))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function maybeRenderSelectedButtons() {
|
|
||||||
if (props.itemsSelected && (props.onEdit || props.onDelete)) {
|
|
||||||
return (
|
|
||||||
<ButtonGroup className="ml-2">
|
|
||||||
{props.onEdit && (
|
|
||||||
<OverlayTrigger
|
|
||||||
overlay={
|
|
||||||
<Tooltip id="edit">
|
|
||||||
{intl.formatMessage({ id: "actions.edit" })}
|
|
||||||
</Tooltip>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Button variant="secondary" onClick={onEdit}>
|
|
||||||
<Icon icon="pencil-alt" />
|
|
||||||
</Button>
|
|
||||||
</OverlayTrigger>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{props.onDelete && (
|
|
||||||
<OverlayTrigger
|
|
||||||
overlay={
|
|
||||||
<Tooltip id="delete">
|
|
||||||
{intl.formatMessage({ id: "actions.delete" })}
|
|
||||||
</Tooltip>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Button variant="danger" onClick={onDelete}>
|
|
||||||
<Icon icon="trash" />
|
|
||||||
</Button>
|
|
||||||
</OverlayTrigger>
|
|
||||||
)}
|
|
||||||
</ButtonGroup>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function render() {
|
function render() {
|
||||||
const currentSortBy = props.filterOptions.sortByOptions.find(
|
const currentSortBy = filterOptions.sortByOptions.find(
|
||||||
(o) => o.value === props.filter.sortBy
|
(o) => o.value === filter.sortBy
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ButtonToolbar className="align-items-center justify-content-center mb-2">
|
<div className="d-flex mb-1">
|
||||||
<div className="d-flex">
|
<InputGroup className="mr-2 flex-grow-1">
|
||||||
<InputGroup className="mr-2 flex-grow-1">
|
<InputGroup.Prepend>
|
||||||
<FormControl
|
<Dropdown>
|
||||||
ref={queryRef}
|
|
||||||
placeholder={`${intl.formatMessage({ id: "actions.search" })}…`}
|
|
||||||
defaultValue={props.filter.searchTerm}
|
|
||||||
onInput={onChangeQuery}
|
|
||||||
className="bg-secondary text-white border-secondary w-50"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<InputGroup.Append>
|
|
||||||
<AddFilter
|
|
||||||
filterOptions={props.filterOptions}
|
|
||||||
onAddCriterion={onAddCriterion}
|
|
||||||
onCancel={onCancelAddCriterion}
|
|
||||||
editingCriterion={editingCriterion}
|
|
||||||
/>
|
|
||||||
</InputGroup.Append>
|
|
||||||
</InputGroup>
|
|
||||||
|
|
||||||
<Dropdown as={ButtonGroup} className="mr-2">
|
|
||||||
<Dropdown.Toggle split variant="secondary" id="more-menu">
|
|
||||||
{currentSortBy
|
|
||||||
? intl.formatMessage({ id: currentSortBy.messageID })
|
|
||||||
: ""}
|
|
||||||
</Dropdown.Toggle>
|
|
||||||
<Dropdown.Menu className="bg-secondary text-white">
|
|
||||||
{renderSortByOptions()}
|
|
||||||
</Dropdown.Menu>
|
|
||||||
<OverlayTrigger
|
|
||||||
overlay={
|
|
||||||
<Tooltip id="sort-direction-tooltip">
|
|
||||||
{props.filter.sortDirection === SortDirectionEnum.Asc
|
|
||||||
? intl.formatMessage({ id: "ascending" })
|
|
||||||
: intl.formatMessage({ id: "descending" })}
|
|
||||||
</Tooltip>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Button variant="secondary" onClick={onChangeSortDirection}>
|
|
||||||
<Icon
|
|
||||||
icon={
|
|
||||||
props.filter.sortDirection === SortDirectionEnum.Asc
|
|
||||||
? "caret-up"
|
|
||||||
: "caret-down"
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
</OverlayTrigger>
|
|
||||||
{props.filter.sortBy === "random" && (
|
|
||||||
<OverlayTrigger
|
<OverlayTrigger
|
||||||
|
placement="top"
|
||||||
overlay={
|
overlay={
|
||||||
<Tooltip id="sort-reshuffle-tooltip">
|
<Tooltip id="filter-tooltip">
|
||||||
{intl.formatMessage({ id: "actions.reshuffle" })}
|
<FormattedMessage id="search_filter.saved_filters" />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Button variant="secondary" onClick={onReshuffleRandomSort}>
|
<Dropdown.Toggle variant="secondary">
|
||||||
<Icon icon="random" />
|
<Icon icon="bookmark" />
|
||||||
</Button>
|
</Dropdown.Toggle>
|
||||||
</OverlayTrigger>
|
</OverlayTrigger>
|
||||||
)}
|
<Dropdown.Menu
|
||||||
</Dropdown>
|
as={SavedFilterDropdown}
|
||||||
</div>
|
className="saved-filter-list-menu"
|
||||||
|
/>
|
||||||
|
</Dropdown>
|
||||||
|
</InputGroup.Prepend>
|
||||||
|
<FormControl
|
||||||
|
ref={queryRef}
|
||||||
|
placeholder={`${intl.formatMessage({ id: "actions.search" })}…`}
|
||||||
|
defaultValue={filter.searchTerm}
|
||||||
|
onInput={onChangeQuery}
|
||||||
|
className="query-text-field bg-secondary text-white border-secondary"
|
||||||
|
/>
|
||||||
|
|
||||||
<Form.Control
|
<InputGroup.Append>
|
||||||
as="select"
|
<OverlayTrigger
|
||||||
onChange={onChangePageSize}
|
placement="top"
|
||||||
value={props.filter.itemsPerPage.toString()}
|
overlay={
|
||||||
className="btn-secondary mx-1"
|
<Tooltip id="filter-tooltip">
|
||||||
>
|
<FormattedMessage id="search_filter.name" />
|
||||||
{PAGE_SIZE_OPTIONS.map((s) => (
|
</Tooltip>
|
||||||
<option value={s} key={s}>
|
}
|
||||||
{s}
|
>
|
||||||
</option>
|
<Button
|
||||||
))}
|
variant="secondary"
|
||||||
</Form.Control>
|
onClick={() => openFilterDialog()}
|
||||||
|
active={filterDialogOpen}
|
||||||
{maybeRenderSelectedButtons()}
|
>
|
||||||
|
<Icon icon="filter" />
|
||||||
<div className="mx-2">{renderMore()}</div>
|
</Button>
|
||||||
|
</OverlayTrigger>
|
||||||
<ButtonGroup>{renderDisplayModeOptions()}</ButtonGroup>
|
</InputGroup.Append>
|
||||||
{maybeRenderZoom()}
|
</InputGroup>
|
||||||
</ButtonToolbar>
|
|
||||||
|
|
||||||
<div className="d-flex justify-content-center">
|
|
||||||
{renderFilterTags()}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Dropdown as={ButtonGroup} className="mr-2 mb-1">
|
||||||
|
<Dropdown.Toggle variant="secondary">
|
||||||
|
{currentSortBy
|
||||||
|
? intl.formatMessage({ id: currentSortBy.messageID })
|
||||||
|
: ""}
|
||||||
|
</Dropdown.Toggle>
|
||||||
|
<Dropdown.Menu className="bg-secondary text-white">
|
||||||
|
{renderSortByOptions()}
|
||||||
|
</Dropdown.Menu>
|
||||||
|
<OverlayTrigger
|
||||||
|
overlay={
|
||||||
|
<Tooltip id="sort-direction-tooltip">
|
||||||
|
{filter.sortDirection === SortDirectionEnum.Asc
|
||||||
|
? intl.formatMessage({ id: "ascending" })
|
||||||
|
: intl.formatMessage({ id: "descending" })}
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Button variant="secondary" onClick={onChangeSortDirection}>
|
||||||
|
<Icon
|
||||||
|
icon={
|
||||||
|
filter.sortDirection === SortDirectionEnum.Asc
|
||||||
|
? "caret-up"
|
||||||
|
: "caret-down"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</OverlayTrigger>
|
||||||
|
{filter.sortBy === "random" && (
|
||||||
|
<OverlayTrigger
|
||||||
|
overlay={
|
||||||
|
<Tooltip id="sort-reshuffle-tooltip">
|
||||||
|
{intl.formatMessage({ id: "actions.reshuffle" })}
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Button variant="secondary" onClick={onReshuffleRandomSort}>
|
||||||
|
<Icon icon="random" />
|
||||||
|
</Button>
|
||||||
|
</OverlayTrigger>
|
||||||
|
)}
|
||||||
|
</Dropdown>
|
||||||
|
|
||||||
|
<Form.Control
|
||||||
|
as="select"
|
||||||
|
onChange={onChangePageSize}
|
||||||
|
value={filter.itemsPerPage.toString()}
|
||||||
|
className="btn-secondary mx-1 mb-1"
|
||||||
|
>
|
||||||
|
{PAGE_SIZE_OPTIONS.map((s) => (
|
||||||
|
<option value={s} key={s}>
|
||||||
|
{s}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</Form.Control>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
173
ui/v2.5/src/components/List/ListOperationButtons.tsx
Normal file
173
ui/v2.5/src/components/List/ListOperationButtons.tsx
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
import React, { useEffect } from "react";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
ButtonGroup,
|
||||||
|
Dropdown,
|
||||||
|
OverlayTrigger,
|
||||||
|
Tooltip,
|
||||||
|
} from "react-bootstrap";
|
||||||
|
import Mousetrap from "mousetrap";
|
||||||
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
|
import { Icon } from "../Shared";
|
||||||
|
|
||||||
|
interface IListFilterOperation {
|
||||||
|
text: string;
|
||||||
|
onClick: () => void;
|
||||||
|
isDisplayed?: () => boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IListOperationButtonsProps {
|
||||||
|
onSelectAll?: () => void;
|
||||||
|
onSelectNone?: () => void;
|
||||||
|
onEdit?: () => void;
|
||||||
|
onDelete?: () => void;
|
||||||
|
itemsSelected?: boolean;
|
||||||
|
otherOperations?: IListFilterOperation[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ListOperationButtons: React.FC<IListOperationButtonsProps> = ({
|
||||||
|
onSelectAll,
|
||||||
|
onSelectNone,
|
||||||
|
onEdit,
|
||||||
|
onDelete,
|
||||||
|
itemsSelected,
|
||||||
|
otherOperations,
|
||||||
|
}) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
Mousetrap.bind("s a", () => onSelectAll?.());
|
||||||
|
Mousetrap.bind("s n", () => onSelectNone?.());
|
||||||
|
|
||||||
|
if (itemsSelected) {
|
||||||
|
Mousetrap.bind("e", () => {
|
||||||
|
onEdit?.();
|
||||||
|
});
|
||||||
|
|
||||||
|
Mousetrap.bind("d d", () => {
|
||||||
|
onDelete?.();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
Mousetrap.unbind("s a");
|
||||||
|
Mousetrap.unbind("s n");
|
||||||
|
|
||||||
|
if (itemsSelected) {
|
||||||
|
Mousetrap.unbind("e");
|
||||||
|
Mousetrap.unbind("d d");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
function maybeRenderSelectedButtons() {
|
||||||
|
if (itemsSelected && (onEdit || onDelete)) {
|
||||||
|
return (
|
||||||
|
<ButtonGroup className="ml-2 mb-1">
|
||||||
|
{onEdit && (
|
||||||
|
<OverlayTrigger
|
||||||
|
overlay={
|
||||||
|
<Tooltip id="edit">
|
||||||
|
{intl.formatMessage({ id: "actions.edit" })}
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Button variant="secondary" onClick={onEdit}>
|
||||||
|
<Icon icon="pencil-alt" />
|
||||||
|
</Button>
|
||||||
|
</OverlayTrigger>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{onDelete && (
|
||||||
|
<OverlayTrigger
|
||||||
|
overlay={
|
||||||
|
<Tooltip id="delete">
|
||||||
|
{intl.formatMessage({ id: "actions.delete" })}
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Button variant="danger" onClick={onDelete}>
|
||||||
|
<Icon icon="trash" />
|
||||||
|
</Button>
|
||||||
|
</OverlayTrigger>
|
||||||
|
)}
|
||||||
|
</ButtonGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSelectAll() {
|
||||||
|
if (onSelectAll) {
|
||||||
|
return (
|
||||||
|
<Dropdown.Item
|
||||||
|
key="select-all"
|
||||||
|
className="bg-secondary text-white"
|
||||||
|
onClick={() => onSelectAll?.()}
|
||||||
|
>
|
||||||
|
<FormattedMessage id="actions.select_all" />
|
||||||
|
</Dropdown.Item>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSelectNone() {
|
||||||
|
if (onSelectNone) {
|
||||||
|
return (
|
||||||
|
<Dropdown.Item
|
||||||
|
key="select-none"
|
||||||
|
className="bg-secondary text-white"
|
||||||
|
onClick={() => onSelectNone?.()}
|
||||||
|
>
|
||||||
|
<FormattedMessage id="actions.select_none" />
|
||||||
|
</Dropdown.Item>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderMore() {
|
||||||
|
const options = [renderSelectAll(), renderSelectNone()].filter((o) => o);
|
||||||
|
|
||||||
|
if (otherOperations) {
|
||||||
|
otherOperations
|
||||||
|
.filter((o) => {
|
||||||
|
if (!o.isDisplayed) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return o.isDisplayed();
|
||||||
|
})
|
||||||
|
.forEach((o) => {
|
||||||
|
options.push(
|
||||||
|
<Dropdown.Item
|
||||||
|
key={o.text}
|
||||||
|
className="bg-secondary text-white"
|
||||||
|
onClick={o.onClick}
|
||||||
|
>
|
||||||
|
{o.text}
|
||||||
|
</Dropdown.Item>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.length > 0) {
|
||||||
|
return (
|
||||||
|
<Dropdown className="mb-1">
|
||||||
|
<Dropdown.Toggle variant="secondary" id="more-menu">
|
||||||
|
<Icon icon="ellipsis-h" />
|
||||||
|
</Dropdown.Toggle>
|
||||||
|
<Dropdown.Menu className="bg-secondary text-white">
|
||||||
|
{options}
|
||||||
|
</Dropdown.Menu>
|
||||||
|
</Dropdown>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{maybeRenderSelectedButtons()}
|
||||||
|
|
||||||
|
<div className="mx-2">{renderMore()}</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
159
ui/v2.5/src/components/List/ListViewOptions.tsx
Normal file
159
ui/v2.5/src/components/List/ListViewOptions.tsx
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
import React, { useEffect } from "react";
|
||||||
|
import Mousetrap from "mousetrap";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
ButtonGroup,
|
||||||
|
Form,
|
||||||
|
OverlayTrigger,
|
||||||
|
Tooltip,
|
||||||
|
} from "react-bootstrap";
|
||||||
|
import { DisplayMode } from "src/models/list-filter/types";
|
||||||
|
import { useIntl } from "react-intl";
|
||||||
|
import { Icon } from "../Shared";
|
||||||
|
|
||||||
|
interface IListViewOptionsProps {
|
||||||
|
zoomIndex?: number;
|
||||||
|
onSetZoom?: (zoomIndex: number) => void;
|
||||||
|
displayMode: DisplayMode;
|
||||||
|
onSetDisplayMode: (m: DisplayMode) => void;
|
||||||
|
displayModeOptions: DisplayMode[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ListViewOptions: React.FC<IListViewOptionsProps> = ({
|
||||||
|
zoomIndex,
|
||||||
|
onSetZoom,
|
||||||
|
displayMode,
|
||||||
|
onSetDisplayMode,
|
||||||
|
displayModeOptions,
|
||||||
|
}) => {
|
||||||
|
const minZoom = 0;
|
||||||
|
const maxZoom = 3;
|
||||||
|
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
Mousetrap.bind("v g", () => {
|
||||||
|
if (displayModeOptions.includes(DisplayMode.Grid)) {
|
||||||
|
onSetDisplayMode(DisplayMode.Grid);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Mousetrap.bind("v l", () => {
|
||||||
|
if (displayModeOptions.includes(DisplayMode.List)) {
|
||||||
|
onSetDisplayMode(DisplayMode.List);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Mousetrap.bind("v w", () => {
|
||||||
|
if (displayModeOptions.includes(DisplayMode.Wall)) {
|
||||||
|
onSetDisplayMode(DisplayMode.Wall);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Mousetrap.bind("+", () => {
|
||||||
|
if (onSetZoom && zoomIndex !== undefined && zoomIndex < maxZoom) {
|
||||||
|
onSetZoom(zoomIndex + 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Mousetrap.bind("-", () => {
|
||||||
|
if (onSetZoom && zoomIndex !== undefined && zoomIndex > minZoom) {
|
||||||
|
onSetZoom(zoomIndex - 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
Mousetrap.unbind("v g");
|
||||||
|
Mousetrap.unbind("v l");
|
||||||
|
Mousetrap.unbind("v w");
|
||||||
|
Mousetrap.unbind("+");
|
||||||
|
Mousetrap.unbind("-");
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
function maybeRenderDisplayModeOptions() {
|
||||||
|
function getIcon(option: DisplayMode) {
|
||||||
|
switch (option) {
|
||||||
|
case DisplayMode.Grid:
|
||||||
|
return "th-large";
|
||||||
|
case DisplayMode.List:
|
||||||
|
return "list";
|
||||||
|
case DisplayMode.Wall:
|
||||||
|
return "square";
|
||||||
|
case DisplayMode.Tagger:
|
||||||
|
return "tags";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function getLabel(option: DisplayMode) {
|
||||||
|
let displayModeId = "unknown";
|
||||||
|
switch (option) {
|
||||||
|
case DisplayMode.Grid:
|
||||||
|
displayModeId = "grid";
|
||||||
|
break;
|
||||||
|
case DisplayMode.List:
|
||||||
|
displayModeId = "list";
|
||||||
|
break;
|
||||||
|
case DisplayMode.Wall:
|
||||||
|
displayModeId = "wall";
|
||||||
|
break;
|
||||||
|
case DisplayMode.Tagger:
|
||||||
|
displayModeId = "tagger";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return intl.formatMessage({ id: `display_mode.${displayModeId}` });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (displayModeOptions.length < 2) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ButtonGroup>
|
||||||
|
{displayModeOptions.map((option) => (
|
||||||
|
<OverlayTrigger
|
||||||
|
key={option}
|
||||||
|
overlay={
|
||||||
|
<Tooltip id="display-mode-tooltip">{getLabel(option)}</Tooltip>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
active={displayMode === option}
|
||||||
|
onClick={() => onSetDisplayMode(option)}
|
||||||
|
>
|
||||||
|
<Icon icon={getIcon(option)} />
|
||||||
|
</Button>
|
||||||
|
</OverlayTrigger>
|
||||||
|
))}
|
||||||
|
</ButtonGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onChangeZoom(v: number) {
|
||||||
|
if (onSetZoom) {
|
||||||
|
onSetZoom(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function maybeRenderZoom() {
|
||||||
|
if (onSetZoom && displayMode === DisplayMode.Grid) {
|
||||||
|
return (
|
||||||
|
<div className="align-middle">
|
||||||
|
<Form.Control
|
||||||
|
className="zoom-slider d-none d-sm-inline-flex ml-3"
|
||||||
|
type="range"
|
||||||
|
min={minZoom}
|
||||||
|
max={maxZoom}
|
||||||
|
value={zoomIndex}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
onChangeZoom(Number.parseInt(e.currentTarget.value, 10))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ButtonGroup>{maybeRenderDisplayModeOptions()}</ButtonGroup>
|
||||||
|
{maybeRenderZoom()}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
354
ui/v2.5/src/components/List/SavedFilterList.tsx
Normal file
354
ui/v2.5/src/components/List/SavedFilterList.tsx
Normal file
@@ -0,0 +1,354 @@
|
|||||||
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
ButtonGroup,
|
||||||
|
Dropdown,
|
||||||
|
FormControl,
|
||||||
|
InputGroup,
|
||||||
|
Modal,
|
||||||
|
OverlayTrigger,
|
||||||
|
Tooltip,
|
||||||
|
} from "react-bootstrap";
|
||||||
|
import {
|
||||||
|
useFindSavedFilters,
|
||||||
|
useSavedFilterDestroy,
|
||||||
|
useSaveFilter,
|
||||||
|
useSetDefaultFilter,
|
||||||
|
} from "src/core/StashService";
|
||||||
|
import { useToast } from "src/hooks";
|
||||||
|
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||||
|
import { SavedFilterDataFragment } from "src/core/generated-graphql";
|
||||||
|
import { LoadingIndicator } from "src/components/Shared";
|
||||||
|
import { PersistanceLevel } from "src/hooks/ListHook";
|
||||||
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
|
import { Icon } from "../Shared";
|
||||||
|
|
||||||
|
interface ISavedFilterListProps {
|
||||||
|
filter: ListFilterModel;
|
||||||
|
onSetFilter: (f: ListFilterModel) => void;
|
||||||
|
persistState?: PersistanceLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SavedFilterList: React.FC<ISavedFilterListProps> = ({
|
||||||
|
filter,
|
||||||
|
onSetFilter,
|
||||||
|
persistState,
|
||||||
|
}) => {
|
||||||
|
const Toast = useToast();
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
const { data, error, loading, refetch } = useFindSavedFilters(filter.mode);
|
||||||
|
const oldError = useRef(error);
|
||||||
|
|
||||||
|
const [filterName, setFilterName] = useState("");
|
||||||
|
const [saving, setSaving] = useState(false);
|
||||||
|
const [deletingFilter, setDeletingFilter] = useState<
|
||||||
|
SavedFilterDataFragment | undefined
|
||||||
|
>();
|
||||||
|
const [overwritingFilter, setOverwritingFilter] = useState<
|
||||||
|
SavedFilterDataFragment | undefined
|
||||||
|
>();
|
||||||
|
|
||||||
|
const [saveFilter] = useSaveFilter();
|
||||||
|
const [destroyFilter] = useSavedFilterDestroy();
|
||||||
|
const [setDefaultFilter] = useSetDefaultFilter();
|
||||||
|
|
||||||
|
const savedFilters = data?.findSavedFilters ?? [];
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (error && error !== oldError.current) {
|
||||||
|
Toast.error(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
oldError.current = error;
|
||||||
|
}, [error, Toast, oldError]);
|
||||||
|
|
||||||
|
async function onSaveFilter(name: string, id?: string) {
|
||||||
|
const filterCopy = filter.clone();
|
||||||
|
filterCopy.currentPage = 1;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setSaving(true);
|
||||||
|
await saveFilter({
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
id,
|
||||||
|
mode: filter.mode,
|
||||||
|
name,
|
||||||
|
filter: JSON.stringify(filterCopy.getSavedQueryParameters()),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
Toast.success({
|
||||||
|
content: intl.formatMessage(
|
||||||
|
{
|
||||||
|
id: "toast.saved_entity",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
entity: intl.formatMessage({ id: "filter" }).toLocaleLowerCase(),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
});
|
||||||
|
setFilterName("");
|
||||||
|
setOverwritingFilter(undefined);
|
||||||
|
refetch();
|
||||||
|
} catch (err) {
|
||||||
|
Toast.error(err);
|
||||||
|
} finally {
|
||||||
|
setSaving(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onDeleteFilter(f: SavedFilterDataFragment) {
|
||||||
|
try {
|
||||||
|
setSaving(true);
|
||||||
|
|
||||||
|
await destroyFilter({
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
id: f.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
Toast.success({
|
||||||
|
content: intl.formatMessage(
|
||||||
|
{
|
||||||
|
id: "toast.delete_past_tense",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
count: 1,
|
||||||
|
singularEntity: intl.formatMessage({ id: "filter" }),
|
||||||
|
pluralEntity: intl.formatMessage({ id: "filters" }),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
});
|
||||||
|
refetch();
|
||||||
|
} catch (err) {
|
||||||
|
Toast.error(err);
|
||||||
|
} finally {
|
||||||
|
setSaving(false);
|
||||||
|
setDeletingFilter(undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onSetDefaultFilter() {
|
||||||
|
const filterCopy = filter.clone();
|
||||||
|
filterCopy.currentPage = 1;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setSaving(true);
|
||||||
|
|
||||||
|
await setDefaultFilter({
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
mode: filter.mode,
|
||||||
|
filter: JSON.stringify(filterCopy.getSavedQueryParameters()),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
Toast.success({
|
||||||
|
content: intl.formatMessage({
|
||||||
|
id: "toast.default_filter_set",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
Toast.error(err);
|
||||||
|
} finally {
|
||||||
|
setSaving(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterClicked(f: SavedFilterDataFragment) {
|
||||||
|
const newFilter = filter.clone();
|
||||||
|
newFilter.currentPage = 1;
|
||||||
|
newFilter.configureFromQueryParameters(JSON.parse(f.filter));
|
||||||
|
|
||||||
|
onSetFilter(newFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ISavedFilterItem {
|
||||||
|
item: SavedFilterDataFragment;
|
||||||
|
}
|
||||||
|
const SavedFilterItem: React.FC<ISavedFilterItem> = ({ item }) => {
|
||||||
|
return (
|
||||||
|
<div className="dropdown-item-container">
|
||||||
|
<Dropdown.Item onClick={() => filterClicked(item)} title={item.name}>
|
||||||
|
<span>{item.name}</span>
|
||||||
|
</Dropdown.Item>
|
||||||
|
<ButtonGroup>
|
||||||
|
<Button
|
||||||
|
className="save-button"
|
||||||
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
|
title={intl.formatMessage({ id: "actions.overwrite" })}
|
||||||
|
onClick={(e) => {
|
||||||
|
setOverwritingFilter(item);
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon icon="save" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className="delete-button"
|
||||||
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
|
title={intl.formatMessage({ id: "actions.delete" })}
|
||||||
|
onClick={(e) => {
|
||||||
|
setDeletingFilter(item);
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon icon="times" />
|
||||||
|
</Button>
|
||||||
|
</ButtonGroup>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
function maybeRenderDeleteAlert() {
|
||||||
|
if (!deletingFilter) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal show>
|
||||||
|
<Modal.Body>
|
||||||
|
<FormattedMessage
|
||||||
|
id="dialogs.delete_confirm"
|
||||||
|
values={{
|
||||||
|
entityName: deletingFilter.name,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Modal.Body>
|
||||||
|
<Modal.Footer>
|
||||||
|
<Button
|
||||||
|
variant="danger"
|
||||||
|
onClick={() => onDeleteFilter(deletingFilter)}
|
||||||
|
>
|
||||||
|
{intl.formatMessage({ id: "actions.delete" })}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => setDeletingFilter(undefined)}
|
||||||
|
>
|
||||||
|
{intl.formatMessage({ id: "actions.cancel" })}
|
||||||
|
</Button>
|
||||||
|
</Modal.Footer>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function maybeRenderOverwriteAlert() {
|
||||||
|
if (!overwritingFilter) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal show>
|
||||||
|
<Modal.Body>
|
||||||
|
<FormattedMessage
|
||||||
|
id="dialogs.overwrite_filter_confirm"
|
||||||
|
values={{
|
||||||
|
entityName: overwritingFilter.name,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Modal.Body>
|
||||||
|
<Modal.Footer>
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
onClick={() =>
|
||||||
|
onSaveFilter(overwritingFilter.name, overwritingFilter.id)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{intl.formatMessage({ id: "actions.overwrite" })}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => setOverwritingFilter(undefined)}
|
||||||
|
>
|
||||||
|
{intl.formatMessage({ id: "actions.cancel" })}
|
||||||
|
</Button>
|
||||||
|
</Modal.Footer>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSavedFilters() {
|
||||||
|
if (loading || saving) {
|
||||||
|
return (
|
||||||
|
<div className="loading">
|
||||||
|
<LoadingIndicator message="" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ul className="saved-filter-list">
|
||||||
|
{savedFilters
|
||||||
|
.filter(
|
||||||
|
(f) => !filterName || f.name.toLowerCase().includes(filterName)
|
||||||
|
)
|
||||||
|
.map((f) => (
|
||||||
|
<SavedFilterItem key={f.name} item={f} />
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function maybeRenderSetDefaultButton() {
|
||||||
|
if (persistState === PersistanceLevel.ALL) {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
className="set-as-default-button"
|
||||||
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => onSetDefaultFilter()}
|
||||||
|
>
|
||||||
|
{intl.formatMessage({ id: "actions.set_as_default" })}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{maybeRenderDeleteAlert()}
|
||||||
|
{maybeRenderOverwriteAlert()}
|
||||||
|
<InputGroup>
|
||||||
|
<FormControl
|
||||||
|
className="bg-secondary text-white border-secondary"
|
||||||
|
placeholder={`${intl.formatMessage({ id: "filter_name" })}…`}
|
||||||
|
value={filterName}
|
||||||
|
onChange={(e) => setFilterName(e.target.value)}
|
||||||
|
/>
|
||||||
|
<InputGroup.Append>
|
||||||
|
<OverlayTrigger
|
||||||
|
placement="top"
|
||||||
|
overlay={
|
||||||
|
<Tooltip id="filter-tooltip">
|
||||||
|
<FormattedMessage id="actions.save_filter" />
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
disabled={
|
||||||
|
!filterName || !!savedFilters.find((f) => f.name === filterName)
|
||||||
|
}
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => {
|
||||||
|
onSaveFilter(filterName);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon icon="save" />
|
||||||
|
</Button>
|
||||||
|
</OverlayTrigger>
|
||||||
|
</InputGroup.Append>
|
||||||
|
</InputGroup>
|
||||||
|
{renderSavedFilters()}
|
||||||
|
{maybeRenderSetDefaultButton()}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -24,3 +24,54 @@ input[type="range"].zoom-slider {
|
|||||||
padding-left: 0;
|
padding-left: 0;
|
||||||
padding-right: 0;
|
padding-right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.query-text-field {
|
||||||
|
border: 0;
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.saved-filter-list-menu {
|
||||||
|
width: 300px;
|
||||||
|
|
||||||
|
.set-as-default-button {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.LoadingIndicator {
|
||||||
|
height: auto;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
.spinner-border {
|
||||||
|
height: 1.5rem;
|
||||||
|
width: 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.saved-filter-list {
|
||||||
|
list-style: none;
|
||||||
|
margin-bottom: 0;
|
||||||
|
max-height: 230px;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding-left: 0;
|
||||||
|
|
||||||
|
.dropdown-item-container {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
.dropdown-item {
|
||||||
|
align-items: center;
|
||||||
|
display: inline;
|
||||||
|
overflow-x: hidden;
|
||||||
|
padding-right: 0.25rem;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-group {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-button {
|
||||||
|
color: $danger;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export const MovieScenesPanel: React.FC<IMovieScenesPanel> = ({ movie }) => {
|
|||||||
const movieValue = { id: movie.id!, label: movie.name! };
|
const movieValue = { id: movie.id!, label: movie.name! };
|
||||||
// if movie is already present, then we modify it, otherwise add
|
// if movie is already present, then we modify it, otherwise add
|
||||||
let movieCriterion = filter.criteria.find((c) => {
|
let movieCriterion = filter.criteria.find((c) => {
|
||||||
return c.criterionOption.value === "movies";
|
return c.criterionOption.type === "movies";
|
||||||
}) as MoviesCriterion;
|
}) as MoviesCriterion;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ import React from "react";
|
|||||||
import { FormattedMessage, useIntl } from "react-intl";
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
import { TagLink } from "src/components/Shared";
|
import { TagLink } from "src/components/Shared";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { genderToString } from "src/core/StashService";
|
|
||||||
import { TextUtils } from "src/utils";
|
import { TextUtils } from "src/utils";
|
||||||
import { TextField, URLField } from "src/utils/field";
|
import { TextField, URLField } from "src/utils/field";
|
||||||
import { RatingStars } from "src/components/Scenes/SceneDetails/RatingStars";
|
import { RatingStars } from "src/components/Scenes/SceneDetails/RatingStars";
|
||||||
|
import { genderToString } from "src/utils/gender";
|
||||||
|
|
||||||
interface IPerformerDetails {
|
interface IPerformerDetails {
|
||||||
performer: Partial<GQL.PerformerDataFragment>;
|
performer: Partial<GQL.PerformerDataFragment>;
|
||||||
|
|||||||
@@ -14,10 +14,7 @@ import Mousetrap from "mousetrap";
|
|||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import * as yup from "yup";
|
import * as yup from "yup";
|
||||||
import {
|
import {
|
||||||
getGenderStrings,
|
|
||||||
useListPerformerScrapers,
|
useListPerformerScrapers,
|
||||||
genderToString,
|
|
||||||
stringToGender,
|
|
||||||
queryScrapePerformer,
|
queryScrapePerformer,
|
||||||
mutateReloadScrapers,
|
mutateReloadScrapers,
|
||||||
usePerformerUpdate,
|
usePerformerUpdate,
|
||||||
@@ -40,6 +37,11 @@ import { useToast } from "src/hooks";
|
|||||||
import { Prompt, useHistory } from "react-router-dom";
|
import { Prompt, useHistory } from "react-router-dom";
|
||||||
import { useFormik } from "formik";
|
import { useFormik } from "formik";
|
||||||
import { RatingStars } from "src/components/Scenes/SceneDetails/RatingStars";
|
import { RatingStars } from "src/components/Scenes/SceneDetails/RatingStars";
|
||||||
|
import {
|
||||||
|
genderStrings,
|
||||||
|
genderToString,
|
||||||
|
stringToGender,
|
||||||
|
} from "src/utils/gender";
|
||||||
import { PerformerScrapeDialog } from "./PerformerScrapeDialog";
|
import { PerformerScrapeDialog } from "./PerformerScrapeDialog";
|
||||||
import PerformerScrapeModal from "./PerformerScrapeModal";
|
import PerformerScrapeModal from "./PerformerScrapeModal";
|
||||||
import PerformerStashBoxModal, { IStashBox } from "./PerformerStashBoxModal";
|
import PerformerStashBoxModal, { IStashBox } from "./PerformerStashBoxModal";
|
||||||
@@ -92,7 +94,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
|||||||
|
|
||||||
const [createTag] = useTagCreate();
|
const [createTag] = useTagCreate();
|
||||||
|
|
||||||
const genderOptions = [""].concat(getGenderStrings());
|
const genderOptions = [""].concat(genderStrings);
|
||||||
|
|
||||||
const labelXS = 3;
|
const labelXS = 3;
|
||||||
const labelXL = 2;
|
const labelXL = 2;
|
||||||
|
|||||||
@@ -9,23 +9,23 @@ import {
|
|||||||
ScrapeDialogRow,
|
ScrapeDialogRow,
|
||||||
ScrapedTextAreaRow,
|
ScrapedTextAreaRow,
|
||||||
} from "src/components/Shared/ScrapeDialog";
|
} from "src/components/Shared/ScrapeDialog";
|
||||||
import {
|
import { useTagCreate } from "src/core/StashService";
|
||||||
getGenderStrings,
|
|
||||||
genderToString,
|
|
||||||
stringToGender,
|
|
||||||
useTagCreate,
|
|
||||||
} from "src/core/StashService";
|
|
||||||
import { Form } from "react-bootstrap";
|
import { Form } from "react-bootstrap";
|
||||||
import { TagSelect } from "src/components/Shared";
|
import { TagSelect } from "src/components/Shared";
|
||||||
import { useToast } from "src/hooks";
|
import { useToast } from "src/hooks";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
|
import {
|
||||||
|
genderStrings,
|
||||||
|
genderToString,
|
||||||
|
stringToGender,
|
||||||
|
} from "src/utils/gender";
|
||||||
|
|
||||||
function renderScrapedGender(
|
function renderScrapedGender(
|
||||||
result: ScrapeResult<string>,
|
result: ScrapeResult<string>,
|
||||||
isNew?: boolean,
|
isNew?: boolean,
|
||||||
onChange?: (value: string) => void
|
onChange?: (value: string) => void
|
||||||
) {
|
) {
|
||||||
const selectOptions = [""].concat(getGenderStrings());
|
const selectOptions = [""].concat(genderStrings);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form.Control
|
<Form.Control
|
||||||
|
|||||||
@@ -233,7 +233,7 @@ export const Scene: React.FC = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const filterCopy = Object.assign(new ListFilterModel(), sceneQueue.query);
|
const filterCopy = sceneQueue.query.clone();
|
||||||
const newStart = queueStart - filterCopy.itemsPerPage;
|
const newStart = queueStart - filterCopy.itemsPerPage;
|
||||||
filterCopy.currentPage = Math.ceil(newStart / filterCopy.itemsPerPage);
|
filterCopy.currentPage = Math.ceil(newStart / filterCopy.itemsPerPage);
|
||||||
const query = await queryFindScenes(filterCopy);
|
const query = await queryFindScenes(filterCopy);
|
||||||
@@ -254,7 +254,7 @@ export const Scene: React.FC = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const filterCopy = Object.assign(new ListFilterModel(), sceneQueue.query);
|
const filterCopy = sceneQueue.query.clone();
|
||||||
const newStart = queueStart + queueScenes.length;
|
const newStart = queueStart + queueScenes.length;
|
||||||
filterCopy.currentPage = Math.ceil(newStart / filterCopy.itemsPerPage);
|
filterCopy.currentPage = Math.ceil(newStart / filterCopy.itemsPerPage);
|
||||||
const query = await queryFindScenes(filterCopy);
|
const query = await queryFindScenes(filterCopy);
|
||||||
@@ -291,7 +291,7 @@ export const Scene: React.FC = () => {
|
|||||||
const pages = Math.ceil(queueTotal / query.itemsPerPage);
|
const pages = Math.ceil(queueTotal / query.itemsPerPage);
|
||||||
const page = Math.floor(Math.random() * pages) + 1;
|
const page = Math.floor(Math.random() * pages) + 1;
|
||||||
const index = Math.floor(Math.random() * query.itemsPerPage);
|
const index = Math.floor(Math.random() * query.itemsPerPage);
|
||||||
const filterCopy = Object.assign(new ListFilterModel(), sceneQueue.query);
|
const filterCopy = sceneQueue.query.clone();
|
||||||
filterCopy.currentPage = page;
|
filterCopy.currentPage = page;
|
||||||
const queryResults = await queryFindScenes(filterCopy);
|
const queryResults = await queryFindScenes(filterCopy);
|
||||||
if (queryResults.data.findScenes.scenes.length > index) {
|
if (queryResults.data.findScenes.scenes.length > index) {
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export const StudioChildrenPanel: React.FC<IStudioChildrenPanel> = ({
|
|||||||
const studioValue = { id: studio.id!, label: studio.name! };
|
const studioValue = { id: studio.id!, label: studio.name! };
|
||||||
// if studio is already present, then we modify it, otherwise add
|
// if studio is already present, then we modify it, otherwise add
|
||||||
let parentStudioCriterion = filter.criteria.find((c) => {
|
let parentStudioCriterion = filter.criteria.find((c) => {
|
||||||
return c.criterionOption.value === "parent_studios";
|
return c.criterionOption.type === "parent_studios";
|
||||||
}) as ParentStudiosCriterion;
|
}) as ParentStudiosCriterion;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ import {
|
|||||||
TruncatedText,
|
TruncatedText,
|
||||||
} from "src/components/Shared";
|
} from "src/components/Shared";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { genderToString } from "src/core/StashService";
|
|
||||||
import { TextUtils } from "src/utils";
|
import { TextUtils } from "src/utils";
|
||||||
|
import { genderToString } from "src/utils/gender";
|
||||||
import { IStashBoxPerformer } from "./utils";
|
import { IStashBoxPerformer } from "./utils";
|
||||||
|
|
||||||
interface IPerformerModalProps {
|
interface IPerformerModalProps {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export const TagMarkersPanel: React.FC<ITagMarkersPanel> = ({ tag }) => {
|
|||||||
const tagValue = { id: tag.id!, label: tag.name! };
|
const tagValue = { id: tag.id!, label: tag.name! };
|
||||||
// if tag is already present, then we modify it, otherwise add
|
// if tag is already present, then we modify it, otherwise add
|
||||||
let tagCriterion = filter.criteria.find((c) => {
|
let tagCriterion = filter.criteria.find((c) => {
|
||||||
return c.criterionOption.value === "tags";
|
return c.criterionOption.type === "tags";
|
||||||
}) as TagsCriterion;
|
}) as TagsCriterion;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
getQueryDefinition,
|
getQueryDefinition,
|
||||||
getOperationName,
|
getOperationName,
|
||||||
} from "@apollo/client/utilities";
|
} from "@apollo/client/utilities";
|
||||||
|
import { stringToGender } from "src/utils/gender";
|
||||||
import { filterData } from "../utils";
|
import { filterData } from "../utils";
|
||||||
import { ListFilterModel } from "../models/list-filter/filter";
|
import { ListFilterModel } from "../models/list-filter/filter";
|
||||||
import * as GQL from "./generated-graphql";
|
import * as GQL from "./generated-graphql";
|
||||||
@@ -43,6 +44,20 @@ const deleteCache = (queries: DocumentNode[]) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useFindSavedFilters = (mode: GQL.FilterMode) =>
|
||||||
|
GQL.useFindSavedFiltersQuery({
|
||||||
|
variables: {
|
||||||
|
mode,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const useFindDefaultFilter = (mode: GQL.FilterMode) =>
|
||||||
|
GQL.useFindDefaultFilterQuery({
|
||||||
|
variables: {
|
||||||
|
mode,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export const useFindGalleries = (filter: ListFilterModel) =>
|
export const useFindGalleries = (filter: ListFilterModel) =>
|
||||||
GQL.useFindGalleriesQuery({
|
GQL.useFindGalleriesQuery({
|
||||||
variables: {
|
variables: {
|
||||||
@@ -680,6 +695,29 @@ export const useTagsDestroy = (input: GQL.TagsDestroyMutationVariables) =>
|
|||||||
update: deleteCache(tagMutationImpactedQueries),
|
update: deleteCache(tagMutationImpactedQueries),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const savedFilterMutationImpactedQueries = [
|
||||||
|
GQL.FindSavedFiltersDocument,
|
||||||
|
];
|
||||||
|
|
||||||
|
export const useSaveFilter = () =>
|
||||||
|
GQL.useSaveFilterMutation({
|
||||||
|
update: deleteCache(savedFilterMutationImpactedQueries),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const savedFilterDefaultMutationImpactedQueries = [
|
||||||
|
GQL.FindDefaultFilterDocument,
|
||||||
|
];
|
||||||
|
|
||||||
|
export const useSetDefaultFilter = () =>
|
||||||
|
GQL.useSetDefaultFilterMutation({
|
||||||
|
update: deleteCache(savedFilterDefaultMutationImpactedQueries),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const useSavedFilterDestroy = () =>
|
||||||
|
GQL.useDestroySavedFilterMutation({
|
||||||
|
update: deleteCache(savedFilterMutationImpactedQueries),
|
||||||
|
});
|
||||||
|
|
||||||
export const useTagsMerge = () =>
|
export const useTagsMerge = () =>
|
||||||
GQL.useTagsMergeMutation({
|
GQL.useTagsMergeMutation({
|
||||||
update: deleteCache(tagMutationImpactedQueries),
|
update: deleteCache(tagMutationImpactedQueries),
|
||||||
@@ -973,54 +1011,6 @@ export const queryParseSceneFilenames = (
|
|||||||
fetchPolicy: "network-only",
|
fetchPolicy: "network-only",
|
||||||
});
|
});
|
||||||
|
|
||||||
export const stringGenderMap = new Map<string, GQL.GenderEnum>([
|
|
||||||
["Male", GQL.GenderEnum.Male],
|
|
||||||
["Female", GQL.GenderEnum.Female],
|
|
||||||
["Transgender Male", GQL.GenderEnum.TransgenderMale],
|
|
||||||
["Transgender Female", GQL.GenderEnum.TransgenderFemale],
|
|
||||||
["Intersex", GQL.GenderEnum.Intersex],
|
|
||||||
["Non-Binary", GQL.GenderEnum.NonBinary],
|
|
||||||
]);
|
|
||||||
|
|
||||||
export const genderToString = (value?: GQL.GenderEnum | string) => {
|
|
||||||
if (!value) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const foundEntry = Array.from(stringGenderMap.entries()).find((e) => {
|
|
||||||
return e[1] === value;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (foundEntry) {
|
|
||||||
return foundEntry[0];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const stringToGender = (
|
|
||||||
value?: string | null,
|
|
||||||
caseInsensitive?: boolean
|
|
||||||
) => {
|
|
||||||
if (!value) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ret = stringGenderMap.get(value);
|
|
||||||
if (ret || !caseInsensitive) {
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
const asUpper = value.toUpperCase();
|
|
||||||
const foundEntry = Array.from(stringGenderMap.entries()).find((e) => {
|
|
||||||
return e[0].toUpperCase() === asUpper;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (foundEntry) {
|
|
||||||
return foundEntry[1];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getGenderStrings = () => Array.from(stringGenderMap.keys());
|
|
||||||
|
|
||||||
export const makePerformerCreateInput = (
|
export const makePerformerCreateInput = (
|
||||||
toCreate: GQL.ScrapedScenePerformer
|
toCreate: GQL.ScrapedScenePerformer
|
||||||
) => {
|
) => {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export const performerFilterHook = (
|
|||||||
const performerValue = { id: performer.id!, label: performer.name! };
|
const performerValue = { id: performer.id!, label: performer.name! };
|
||||||
// if performers is already present, then we modify it, otherwise add
|
// if performers is already present, then we modify it, otherwise add
|
||||||
let performerCriterion = filter.criteria.find((c) => {
|
let performerCriterion = filter.criteria.find((c) => {
|
||||||
return c.criterionOption.value === "performers";
|
return c.criterionOption.type === "performers";
|
||||||
}) as PerformersCriterion;
|
}) as PerformersCriterion;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export const studioFilterHook = (studio: Partial<GQL.StudioDataFragment>) => {
|
|||||||
const studioValue = { id: studio.id!, label: studio.name! };
|
const studioValue = { id: studio.id!, label: studio.name! };
|
||||||
// if studio is already present, then we modify it, otherwise add
|
// if studio is already present, then we modify it, otherwise add
|
||||||
let studioCriterion = filter.criteria.find((c) => {
|
let studioCriterion = filter.criteria.find((c) => {
|
||||||
return c.criterionOption.value === "studios";
|
return c.criterionOption.type === "studios";
|
||||||
}) as StudiosCriterion;
|
}) as StudiosCriterion;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export const tagFilterHook = (tag: GQL.TagDataFragment) => {
|
|||||||
const tagValue = { id: tag.id, label: tag.name };
|
const tagValue = { id: tag.id, label: tag.name };
|
||||||
// if tag is already present, then we modify it, otherwise add
|
// if tag is already present, then we modify it, otherwise add
|
||||||
let tagCriterion = filter.criteria.find((c) => {
|
let tagCriterion = filter.criteria.find((c) => {
|
||||||
return c.criterionOption.value === "tags";
|
return c.criterionOption.type === "tags";
|
||||||
}) as TagsCriterion;
|
}) as TagsCriterion;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -27,12 +27,15 @@ import {
|
|||||||
TagDataFragment,
|
TagDataFragment,
|
||||||
FindImagesQueryResult,
|
FindImagesQueryResult,
|
||||||
SlimImageDataFragment,
|
SlimImageDataFragment,
|
||||||
|
FilterMode,
|
||||||
} from "src/core/generated-graphql";
|
} from "src/core/generated-graphql";
|
||||||
import { useInterfaceLocalForage } from "src/hooks/LocalForage";
|
import { useInterfaceLocalForage } from "src/hooks/LocalForage";
|
||||||
import { LoadingIndicator } from "src/components/Shared";
|
import { LoadingIndicator } from "src/components/Shared";
|
||||||
import { ListFilter } from "src/components/List/ListFilter";
|
import { ListFilter } from "src/components/List/ListFilter";
|
||||||
|
import { FilterTags } from "src/components/List/FilterTags";
|
||||||
import { Pagination, PaginationIndex } from "src/components/List/Pagination";
|
import { Pagination, PaginationIndex } from "src/components/List/Pagination";
|
||||||
import {
|
import {
|
||||||
|
useFindDefaultFilter,
|
||||||
useFindScenes,
|
useFindScenes,
|
||||||
useFindSceneMarkers,
|
useFindSceneMarkers,
|
||||||
useFindImages,
|
useFindImages,
|
||||||
@@ -43,9 +46,17 @@ import {
|
|||||||
useFindTags,
|
useFindTags,
|
||||||
} from "src/core/StashService";
|
} from "src/core/StashService";
|
||||||
import { ListFilterModel } from "src/models/list-filter/filter";
|
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||||
import { FilterMode } from "src/models/list-filter/types";
|
import { DisplayMode } from "src/models/list-filter/types";
|
||||||
import { ListFilterOptions } from "src/models/list-filter/filter-options";
|
import { ListFilterOptions } from "src/models/list-filter/filter-options";
|
||||||
import { getFilterOptions } from "src/models/list-filter/factory";
|
import { getFilterOptions } from "src/models/list-filter/factory";
|
||||||
|
import { ButtonToolbar } from "react-bootstrap";
|
||||||
|
import { ListViewOptions } from "src/components/List/ListViewOptions";
|
||||||
|
import { ListOperationButtons } from "src/components/List/ListOperationButtons";
|
||||||
|
import {
|
||||||
|
Criterion,
|
||||||
|
CriterionValue,
|
||||||
|
} from "src/models/list-filter/criteria/criterion";
|
||||||
|
import { AddFilterDialog } from "src/components/List/AddFilterDialog";
|
||||||
|
|
||||||
const getSelectedData = <I extends IDataItem>(
|
const getSelectedData = <I extends IDataItem>(
|
||||||
result: I[],
|
result: I[],
|
||||||
@@ -88,8 +99,11 @@ export interface IListHookOperation<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export enum PersistanceLevel {
|
export enum PersistanceLevel {
|
||||||
|
// do not load default query or persist display mode
|
||||||
NONE,
|
NONE,
|
||||||
|
// load default query, don't load or persist display mode
|
||||||
ALL,
|
ALL,
|
||||||
|
// load and persist display mode only
|
||||||
VIEW,
|
VIEW,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,6 +112,10 @@ interface IListHookOptions<T, E> {
|
|||||||
persistanceKey?: string;
|
persistanceKey?: string;
|
||||||
defaultSort?: string;
|
defaultSort?: string;
|
||||||
filterHook?: (filter: ListFilterModel) => ListFilterModel;
|
filterHook?: (filter: ListFilterModel) => ListFilterModel;
|
||||||
|
filterDialog?: (
|
||||||
|
criteria: Criterion<CriterionValue>[],
|
||||||
|
setCriteria: (v: Criterion<CriterionValue>[]) => void
|
||||||
|
) => React.ReactNode;
|
||||||
zoomable?: boolean;
|
zoomable?: boolean;
|
||||||
selectable?: boolean;
|
selectable?: boolean;
|
||||||
defaultZoomIndex?: number;
|
defaultZoomIndex?: number;
|
||||||
@@ -167,6 +185,8 @@ const RenderList = <
|
|||||||
renderEditDialog,
|
renderEditDialog,
|
||||||
renderDeleteDialog,
|
renderDeleteDialog,
|
||||||
updateQueryParams,
|
updateQueryParams,
|
||||||
|
filterDialog,
|
||||||
|
persistState,
|
||||||
}: IListHookOptions<QueryResult, QueryData> &
|
}: IListHookOptions<QueryResult, QueryData> &
|
||||||
IQuery<QueryResult, QueryData> &
|
IQuery<QueryResult, QueryData> &
|
||||||
IRenderListProps) => {
|
IRenderListProps) => {
|
||||||
@@ -176,6 +196,11 @@ const RenderList = <
|
|||||||
const [lastClickedId, setLastClickedId] = useState<string | undefined>();
|
const [lastClickedId, setLastClickedId] = useState<string | undefined>();
|
||||||
const [zoomIndex, setZoomIndex] = useState<number>(defaultZoomIndex ?? 1);
|
const [zoomIndex, setZoomIndex] = useState<number>(defaultZoomIndex ?? 1);
|
||||||
|
|
||||||
|
const [editingCriterion, setEditingCriterion] = useState<
|
||||||
|
Criterion<CriterionValue> | undefined
|
||||||
|
>(undefined);
|
||||||
|
const [newCriterion, setNewCriterion] = useState(false);
|
||||||
|
|
||||||
const result = useData(filter);
|
const result = useData(filter);
|
||||||
const totalCount = getCount(result);
|
const totalCount = getCount(result);
|
||||||
const items = getData(result);
|
const items = getData(result);
|
||||||
@@ -189,6 +214,7 @@ const RenderList = <
|
|||||||
}, [pages, filter.currentPage, onChangePage]);
|
}, [pages, filter.currentPage, onChangePage]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
Mousetrap.bind("f", () => setNewCriterion(true));
|
||||||
Mousetrap.bind("right", () => {
|
Mousetrap.bind("right", () => {
|
||||||
const maxPage = totalCount / filter.itemsPerPage;
|
const maxPage = totalCount / filter.itemsPerPage;
|
||||||
if (filter.currentPage < maxPage) {
|
if (filter.currentPage < maxPage) {
|
||||||
@@ -397,21 +423,104 @@ const RenderList = <
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onChangeDisplayMode(displayMode: DisplayMode) {
|
||||||
|
const newFilter = _.cloneDeep(filter);
|
||||||
|
newFilter.displayMode = displayMode;
|
||||||
|
updateQueryParams(newFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onAddCriterion(
|
||||||
|
criterion: Criterion<CriterionValue>,
|
||||||
|
oldId?: string
|
||||||
|
) {
|
||||||
|
const newFilter = _.cloneDeep(filter);
|
||||||
|
|
||||||
|
// Find if we are editing an existing criteria, then modify that. Or create a new one.
|
||||||
|
const existingIndex = newFilter.criteria.findIndex((c) => {
|
||||||
|
// If we modified an existing criterion, then look for the old id.
|
||||||
|
const id = oldId || criterion.getId();
|
||||||
|
return c.getId() === id;
|
||||||
|
});
|
||||||
|
if (existingIndex === -1) {
|
||||||
|
newFilter.criteria.push(criterion);
|
||||||
|
} else {
|
||||||
|
newFilter.criteria[existingIndex] = criterion;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove duplicate modifiers
|
||||||
|
newFilter.criteria = newFilter.criteria.filter((obj, pos, arr) => {
|
||||||
|
return arr.map((mapObj) => mapObj.getId()).indexOf(obj.getId()) === pos;
|
||||||
|
});
|
||||||
|
|
||||||
|
newFilter.currentPage = 1;
|
||||||
|
updateQueryParams(newFilter);
|
||||||
|
setEditingCriterion(undefined);
|
||||||
|
setNewCriterion(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onRemoveCriterion(removedCriterion: Criterion<CriterionValue>) {
|
||||||
|
const newFilter = _.cloneDeep(filter);
|
||||||
|
newFilter.criteria = newFilter.criteria.filter(
|
||||||
|
(criterion) => criterion.getId() !== removedCriterion.getId()
|
||||||
|
);
|
||||||
|
newFilter.currentPage = 1;
|
||||||
|
updateQueryParams(newFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateCriteria(c: Criterion<CriterionValue>[]) {
|
||||||
|
const newFilter = _.cloneDeep(filter);
|
||||||
|
newFilter.criteria = c.slice();
|
||||||
|
setNewCriterion(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onCancelAddCriterion() {
|
||||||
|
setEditingCriterion(undefined);
|
||||||
|
setNewCriterion(false);
|
||||||
|
}
|
||||||
|
|
||||||
const content = (
|
const content = (
|
||||||
<div>
|
<div>
|
||||||
<ListFilter
|
<ButtonToolbar className="align-items-center justify-content-center mb-2">
|
||||||
onFilterUpdate={updateQueryParams}
|
<ListFilter
|
||||||
onSelectAll={selectable ? onSelectAll : undefined}
|
onFilterUpdate={updateQueryParams}
|
||||||
onSelectNone={selectable ? onSelectNone : undefined}
|
filter={filter}
|
||||||
zoomIndex={zoomable ? zoomIndex : undefined}
|
filterOptions={filterOptions}
|
||||||
onChangeZoom={zoomable ? onChangeZoom : undefined}
|
openFilterDialog={() => setNewCriterion(true)}
|
||||||
otherOperations={operations}
|
filterDialogOpen={newCriterion ?? editingCriterion}
|
||||||
itemsSelected={selectedIds.size > 0}
|
persistState={persistState}
|
||||||
onEdit={renderEditDialog ? onEdit : undefined}
|
/>
|
||||||
onDelete={renderDeleteDialog ? onDelete : undefined}
|
<ListOperationButtons
|
||||||
filter={filter}
|
onSelectAll={selectable ? onSelectAll : undefined}
|
||||||
filterOptions={filterOptions}
|
onSelectNone={selectable ? onSelectNone : undefined}
|
||||||
|
otherOperations={operations}
|
||||||
|
itemsSelected={selectedIds.size > 0}
|
||||||
|
onEdit={renderEditDialog ? onEdit : undefined}
|
||||||
|
onDelete={renderDeleteDialog ? onDelete : undefined}
|
||||||
|
/>
|
||||||
|
<ListViewOptions
|
||||||
|
displayMode={filter.displayMode}
|
||||||
|
displayModeOptions={filterOptions.displayModeOptions}
|
||||||
|
onSetDisplayMode={onChangeDisplayMode}
|
||||||
|
zoomIndex={zoomable ? zoomIndex : undefined}
|
||||||
|
onSetZoom={zoomable ? onChangeZoom : undefined}
|
||||||
|
/>
|
||||||
|
</ButtonToolbar>
|
||||||
|
<FilterTags
|
||||||
|
criteria={filter.criteria}
|
||||||
|
onEditCriterion={(c) => setEditingCriterion(c)}
|
||||||
|
onRemoveCriterion={onRemoveCriterion}
|
||||||
/>
|
/>
|
||||||
|
{(newCriterion || editingCriterion) && !filterDialog && (
|
||||||
|
<AddFilterDialog
|
||||||
|
filterOptions={filterOptions}
|
||||||
|
onAddCriterion={onAddCriterion}
|
||||||
|
onCancel={onCancelAddCriterion}
|
||||||
|
editingCriterion={editingCriterion}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{newCriterion &&
|
||||||
|
filterDialog &&
|
||||||
|
filterDialog(filter.criteria, (c) => updateCriteria(c))}
|
||||||
{isEditDialogOpen &&
|
{isEditDialogOpen &&
|
||||||
renderEditDialog &&
|
renderEditDialog &&
|
||||||
renderEditDialog(
|
renderEditDialog(
|
||||||
@@ -454,6 +563,7 @@ const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
|
|||||||
const defaultDisplayMode = filterOptions.displayModeOptions[0];
|
const defaultDisplayMode = filterOptions.displayModeOptions[0];
|
||||||
const [filter, setFilter] = useState<ListFilterModel>(
|
const [filter, setFilter] = useState<ListFilterModel>(
|
||||||
new ListFilterModel(
|
new ListFilterModel(
|
||||||
|
options.filterMode,
|
||||||
queryString.parse(location.search),
|
queryString.parse(location.search),
|
||||||
defaultSort,
|
defaultSort,
|
||||||
defaultDisplayMode
|
defaultDisplayMode
|
||||||
@@ -462,8 +572,8 @@ const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
|
|||||||
|
|
||||||
const updateInterfaceConfig = useCallback(
|
const updateInterfaceConfig = useCallback(
|
||||||
(updatedFilter: ListFilterModel, level: PersistanceLevel) => {
|
(updatedFilter: ListFilterModel, level: PersistanceLevel) => {
|
||||||
setInterfaceState((prevState) => {
|
if (level === PersistanceLevel.VIEW) {
|
||||||
if (level === PersistanceLevel.VIEW) {
|
setInterfaceState((prevState) => {
|
||||||
return {
|
return {
|
||||||
[persistanceKey]: {
|
[persistanceKey]: {
|
||||||
...prevState[persistanceKey],
|
...prevState[persistanceKey],
|
||||||
@@ -473,84 +583,16 @@ const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
|
|||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
});
|
||||||
return {
|
}
|
||||||
[persistanceKey]: {
|
|
||||||
filter: updatedFilter.makeQueryParameters(),
|
|
||||||
itemsPerPage: updatedFilter.itemsPerPage,
|
|
||||||
currentPage: updatedFilter.currentPage,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
[persistanceKey, setInterfaceState]
|
[persistanceKey, setInterfaceState]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
const {
|
||||||
if (
|
data: defaultFilter,
|
||||||
interfaceState.loading ||
|
loading: defaultFilterLoading,
|
||||||
// Only update query params on page the hook was mounted on
|
} = useFindDefaultFilter(options.filterMode);
|
||||||
history.location.pathname !== originalPathName.current
|
|
||||||
)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (!forageInitialised) setForageInitialised(true);
|
|
||||||
|
|
||||||
if (!options.persistState) return;
|
|
||||||
|
|
||||||
const storedQuery = interfaceState.data?.[persistanceKey];
|
|
||||||
if (!storedQuery) return;
|
|
||||||
|
|
||||||
const queryFilter = queryString.parse(history.location.search);
|
|
||||||
const storedFilter = queryString.parse(storedQuery.filter);
|
|
||||||
|
|
||||||
const activeFilter =
|
|
||||||
options.persistState === PersistanceLevel.ALL
|
|
||||||
? storedFilter
|
|
||||||
: { disp: storedFilter.disp };
|
|
||||||
const query = history.location.search
|
|
||||||
? {
|
|
||||||
sortby: activeFilter.sortby,
|
|
||||||
sortdir: activeFilter.sortdir,
|
|
||||||
disp: activeFilter.disp,
|
|
||||||
perPage: activeFilter.perPage,
|
|
||||||
...queryFilter,
|
|
||||||
}
|
|
||||||
: activeFilter;
|
|
||||||
|
|
||||||
const newFilter = new ListFilterModel(
|
|
||||||
query,
|
|
||||||
defaultSort,
|
|
||||||
defaultDisplayMode
|
|
||||||
);
|
|
||||||
|
|
||||||
// Compare constructed filter with current filter.
|
|
||||||
// If different it's the result of navigation, and we update the filter.
|
|
||||||
const newLocation = { ...history.location };
|
|
||||||
newLocation.search = newFilter.makeQueryParameters();
|
|
||||||
if (newLocation.search !== filter.makeQueryParameters()) {
|
|
||||||
setFilter(newFilter);
|
|
||||||
updateInterfaceConfig(newFilter, options.persistState);
|
|
||||||
}
|
|
||||||
// If constructed search is different from current, update it as well
|
|
||||||
if (newLocation.search !== location.search) {
|
|
||||||
newLocation.search = newFilter.makeQueryParameters();
|
|
||||||
history.replace(newLocation);
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
defaultSort,
|
|
||||||
defaultDisplayMode,
|
|
||||||
filter,
|
|
||||||
interfaceState.data,
|
|
||||||
interfaceState.loading,
|
|
||||||
history,
|
|
||||||
location.search,
|
|
||||||
options.filterMode,
|
|
||||||
persistanceKey,
|
|
||||||
forageInitialised,
|
|
||||||
updateInterfaceConfig,
|
|
||||||
options.persistState,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const updateQueryParams = useCallback(
|
const updateQueryParams = useCallback(
|
||||||
(listFilter: ListFilterModel) => {
|
(listFilter: ListFilterModel) => {
|
||||||
@@ -565,6 +607,67 @@ const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
|
|||||||
[setFilter, history, location, options.persistState, updateInterfaceConfig]
|
[setFilter, history, location, options.persistState, updateInterfaceConfig]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
// defer processing this until forage is initialised and
|
||||||
|
// default filter is loaded
|
||||||
|
interfaceState.loading ||
|
||||||
|
defaultFilterLoading ||
|
||||||
|
// Only update query params on page the hook was mounted on
|
||||||
|
history.location.pathname !== originalPathName.current
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!forageInitialised) setForageInitialised(true);
|
||||||
|
|
||||||
|
if (!options.persistState) return;
|
||||||
|
|
||||||
|
const newFilter = filter.clone();
|
||||||
|
let update = false;
|
||||||
|
|
||||||
|
// if default query is set and no search params are set, then
|
||||||
|
// load the default query
|
||||||
|
if (!location.search && defaultFilter?.findDefaultFilter) {
|
||||||
|
newFilter.currentPage = 1;
|
||||||
|
newFilter.configureFromQueryParameters(
|
||||||
|
JSON.parse(defaultFilter.findDefaultFilter.filter)
|
||||||
|
);
|
||||||
|
update = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// set the display type if persisted
|
||||||
|
const storedQuery = interfaceState.data?.[persistanceKey];
|
||||||
|
|
||||||
|
if (options.persistState === PersistanceLevel.VIEW && storedQuery) {
|
||||||
|
const storedFilter = queryString.parse(storedQuery.filter);
|
||||||
|
|
||||||
|
if (storedFilter.disp !== undefined) {
|
||||||
|
const displayMode = Number.parseInt(storedFilter.disp as string, 10);
|
||||||
|
if (displayMode !== newFilter.displayMode) {
|
||||||
|
newFilter.displayMode = displayMode;
|
||||||
|
update = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (update) {
|
||||||
|
updateQueryParams(newFilter);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
defaultSort,
|
||||||
|
defaultDisplayMode,
|
||||||
|
filter,
|
||||||
|
interfaceState,
|
||||||
|
history,
|
||||||
|
location.search,
|
||||||
|
updateQueryParams,
|
||||||
|
defaultFilter,
|
||||||
|
defaultFilterLoading,
|
||||||
|
persistanceKey,
|
||||||
|
forageInitialised,
|
||||||
|
options.persistState,
|
||||||
|
]);
|
||||||
|
|
||||||
const onChangePage = useCallback(
|
const onChangePage = useCallback(
|
||||||
(page: number) => {
|
(page: number) => {
|
||||||
const newFilter = _.cloneDeep(filter);
|
const newFilter = _.cloneDeep(filter);
|
||||||
|
|||||||
@@ -58,6 +58,7 @@
|
|||||||
"reshuffle": "Reshuffle",
|
"reshuffle": "Reshuffle",
|
||||||
"running": "running",
|
"running": "running",
|
||||||
"save": "Save",
|
"save": "Save",
|
||||||
|
"save_filter": "Save filter",
|
||||||
"scan": "Scan",
|
"scan": "Scan",
|
||||||
"scrape_with": "Scrape with…",
|
"scrape_with": "Scrape with…",
|
||||||
"search": "Search",
|
"search": "Search",
|
||||||
@@ -65,6 +66,7 @@
|
|||||||
"select_none": "Select None",
|
"select_none": "Select None",
|
||||||
"selective_auto_tag": "Selective Auto Tag",
|
"selective_auto_tag": "Selective Auto Tag",
|
||||||
"selective_scan": "Selective Scan",
|
"selective_scan": "Selective Scan",
|
||||||
|
"set_as_default": "Set as default",
|
||||||
"set_back_image": "Back image…",
|
"set_back_image": "Back image…",
|
||||||
"set_front_image": "Front image…",
|
"set_front_image": "Front image…",
|
||||||
"set_image": "Set image…",
|
"set_image": "Set image…",
|
||||||
@@ -400,6 +402,7 @@
|
|||||||
"destination": "Destination",
|
"destination": "Destination",
|
||||||
"source": "Source"
|
"source": "Source"
|
||||||
},
|
},
|
||||||
|
"overwrite_filter_confirm": "Are you sure you want to overwrite existing saved query {entityName}?",
|
||||||
"scene_gen": {
|
"scene_gen": {
|
||||||
"image_previews": "Image Previews (animated WebP previews, only required if Preview Type is set to Animated Image)",
|
"image_previews": "Image Previews (animated WebP previews, only required if Preview Type is set to Animated Image)",
|
||||||
"markers": "Markers (20 second videos which begin at the given timecode)",
|
"markers": "Markers (20 second videos which begin at the given timecode)",
|
||||||
@@ -477,6 +480,9 @@
|
|||||||
"file_info": "File Info",
|
"file_info": "File Info",
|
||||||
"file_mod_time": "File Modification Time",
|
"file_mod_time": "File Modification Time",
|
||||||
"filesize": "File Size",
|
"filesize": "File Size",
|
||||||
|
"filter": "Filter",
|
||||||
|
"filter_name": "Filter name",
|
||||||
|
"filters": "Filters",
|
||||||
"framerate": "Frame Rate",
|
"framerate": "Frame Rate",
|
||||||
"galleries": "Galleries",
|
"galleries": "Galleries",
|
||||||
"gallery": "Gallery",
|
"gallery": "Gallery",
|
||||||
@@ -490,6 +496,7 @@
|
|||||||
"image_count": "Image Count",
|
"image_count": "Image Count",
|
||||||
"images": "Images",
|
"images": "Images",
|
||||||
"images-size": "Images size",
|
"images-size": "Images size",
|
||||||
|
"include_child_studios": "Include child studios",
|
||||||
"instagram": "Instagram",
|
"instagram": "Instagram",
|
||||||
"interactive": "Interactive",
|
"interactive": "Interactive",
|
||||||
"isMissing": "Is Missing",
|
"isMissing": "Is Missing",
|
||||||
@@ -552,6 +559,7 @@
|
|||||||
"search_filter": {
|
"search_filter": {
|
||||||
"add_filter": "Add Filter",
|
"add_filter": "Add Filter",
|
||||||
"name": "Filter",
|
"name": "Filter",
|
||||||
|
"saved_filters": "Saved filters",
|
||||||
"update_filter": "Update Filter"
|
"update_filter": "Update Filter"
|
||||||
},
|
},
|
||||||
"seconds": "Seconds",
|
"seconds": "Seconds",
|
||||||
@@ -570,12 +578,15 @@
|
|||||||
"toast": {
|
"toast": {
|
||||||
"added_entity": "Added {entity}",
|
"added_entity": "Added {entity}",
|
||||||
"added_generation_job_to_queue": "Added generation job to queue",
|
"added_generation_job_to_queue": "Added generation job to queue",
|
||||||
|
"create_entity": "Created {entity}",
|
||||||
|
"default_filter_set": "Default filter set",
|
||||||
"delete_entity": "Delete {count, plural, one {{singularEntity}} other {{pluralEntity}}}",
|
"delete_entity": "Delete {count, plural, one {{singularEntity}} other {{pluralEntity}}}",
|
||||||
"delete_past_tense": "Deleted {count, plural, one {{singularEntity}} other {{pluralEntity}}}",
|
"delete_past_tense": "Deleted {count, plural, one {{singularEntity}} other {{pluralEntity}}}",
|
||||||
"generating_screenshot": "Generating screenshot…",
|
"generating_screenshot": "Generating screenshot…",
|
||||||
"merged_tags": "Merged tags",
|
"merged_tags": "Merged tags",
|
||||||
"rescanning_entity": "Rescanning {count, plural, one {{singularEntity}} other {{pluralEntity}}}…",
|
"rescanning_entity": "Rescanning {count, plural, one {{singularEntity}} other {{pluralEntity}}}…",
|
||||||
"started_auto_tagging": "Started auto tagging",
|
"started_auto_tagging": "Started auto tagging",
|
||||||
|
"saved_entity": "Saved {entity}",
|
||||||
"updated_entity": "Updated {entity}"
|
"updated_entity": "Updated {entity}"
|
||||||
},
|
},
|
||||||
"total": "Total",
|
"total": "Total",
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import { CriterionOption, StringCriterion } from "./criterion";
|
import { StringCriterion, StringCriterionOption } from "./criterion";
|
||||||
|
|
||||||
const countryCriterionOption = new CriterionOption("country", "country");
|
const countryCriterionOption = new StringCriterionOption(
|
||||||
|
"country",
|
||||||
|
"country",
|
||||||
|
"country"
|
||||||
|
);
|
||||||
|
|
||||||
export class CountryCriterion extends StringCriterion {
|
export class CountryCriterion extends StringCriterion {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|||||||
@@ -16,106 +16,58 @@ import {
|
|||||||
IHierarchicalLabelValue,
|
IHierarchicalLabelValue,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
|
|
||||||
type Option = string | number | IOptionType;
|
export type Option = string | number | IOptionType;
|
||||||
export type CriterionValue =
|
export type CriterionValue =
|
||||||
| string
|
| string
|
||||||
| number
|
| number
|
||||||
| ILabeledId[]
|
| ILabeledId[]
|
||||||
| IHierarchicalLabelValue;
|
| IHierarchicalLabelValue;
|
||||||
|
|
||||||
|
const modifierMessageIDs = {
|
||||||
|
[CriterionModifier.Equals]: "criterion_modifier.equals",
|
||||||
|
[CriterionModifier.NotEquals]: "criterion_modifier.not_equals",
|
||||||
|
[CriterionModifier.GreaterThan]: "criterion_modifier.greater_than",
|
||||||
|
[CriterionModifier.LessThan]: "criterion_modifier.less_than",
|
||||||
|
[CriterionModifier.IsNull]: "criterion_modifier.is_null",
|
||||||
|
[CriterionModifier.NotNull]: "criterion_modifier.not_null",
|
||||||
|
[CriterionModifier.Includes]: "criterion_modifier.includes",
|
||||||
|
[CriterionModifier.IncludesAll]: "criterion_modifier.includes_all",
|
||||||
|
[CriterionModifier.Excludes]: "criterion_modifier.excludes",
|
||||||
|
[CriterionModifier.MatchesRegex]: "criterion_modifier.matches_regex",
|
||||||
|
[CriterionModifier.NotMatchesRegex]: "criterion_modifier.not_matches_regex",
|
||||||
|
};
|
||||||
|
|
||||||
// V = criterion value type
|
// V = criterion value type
|
||||||
export abstract class Criterion<V extends CriterionValue> {
|
export abstract class Criterion<V extends CriterionValue> {
|
||||||
public static getModifierOption(
|
public static getModifierOption(
|
||||||
modifier: CriterionModifier = CriterionModifier.Equals
|
modifier: CriterionModifier = CriterionModifier.Equals
|
||||||
): ILabeledValue {
|
): ILabeledValue {
|
||||||
switch (modifier) {
|
const messageID = modifierMessageIDs[modifier];
|
||||||
case CriterionModifier.Equals:
|
return { value: modifier, label: messageID };
|
||||||
return { value: CriterionModifier.Equals, label: "Equals" };
|
|
||||||
case CriterionModifier.NotEquals:
|
|
||||||
return { value: CriterionModifier.NotEquals, label: "Not Equals" };
|
|
||||||
case CriterionModifier.GreaterThan:
|
|
||||||
return { value: CriterionModifier.GreaterThan, label: "Greater Than" };
|
|
||||||
case CriterionModifier.LessThan:
|
|
||||||
return { value: CriterionModifier.LessThan, label: "Less Than" };
|
|
||||||
case CriterionModifier.IsNull:
|
|
||||||
return { value: CriterionModifier.IsNull, label: "Is NULL" };
|
|
||||||
case CriterionModifier.NotNull:
|
|
||||||
return { value: CriterionModifier.NotNull, label: "Not NULL" };
|
|
||||||
case CriterionModifier.IncludesAll:
|
|
||||||
return { value: CriterionModifier.IncludesAll, label: "Includes All" };
|
|
||||||
case CriterionModifier.Includes:
|
|
||||||
return { value: CriterionModifier.Includes, label: "Includes" };
|
|
||||||
case CriterionModifier.Excludes:
|
|
||||||
return { value: CriterionModifier.Excludes, label: "Excludes" };
|
|
||||||
case CriterionModifier.MatchesRegex:
|
|
||||||
return {
|
|
||||||
value: CriterionModifier.MatchesRegex,
|
|
||||||
label: "Matches Regex",
|
|
||||||
};
|
|
||||||
case CriterionModifier.NotMatchesRegex:
|
|
||||||
return {
|
|
||||||
value: CriterionModifier.NotMatchesRegex,
|
|
||||||
label: "Not Matches Regex",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public criterionOption: CriterionOption;
|
public criterionOption: CriterionOption;
|
||||||
public abstract modifier: CriterionModifier;
|
public modifier: CriterionModifier;
|
||||||
public abstract modifierOptions: ILabeledValue[];
|
public value: V;
|
||||||
public abstract options: Option[] | undefined;
|
|
||||||
public abstract value: V;
|
|
||||||
public inputType: "number" | "text" | undefined;
|
|
||||||
|
|
||||||
public abstract getLabelValue(): string;
|
public abstract getLabelValue(): string;
|
||||||
|
|
||||||
constructor(type: CriterionOption) {
|
constructor(type: CriterionOption, value: V) {
|
||||||
this.criterionOption = type;
|
this.criterionOption = type;
|
||||||
|
this.modifier = type.defaultModifier;
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static getModifierLabel(intl: IntlShape, modifier: CriterionModifier) {
|
||||||
|
const modifierMessageID = modifierMessageIDs[modifier];
|
||||||
|
|
||||||
|
return modifierMessageID
|
||||||
|
? intl.formatMessage({ id: modifierMessageID })
|
||||||
|
: "";
|
||||||
}
|
}
|
||||||
|
|
||||||
public getLabel(intl: IntlShape): string {
|
public getLabel(intl: IntlShape): string {
|
||||||
let modifierMessageID: string;
|
const modifierString = Criterion.getModifierLabel(intl, this.modifier);
|
||||||
switch (this.modifier) {
|
|
||||||
case CriterionModifier.Equals:
|
|
||||||
modifierMessageID = "criterion_modifier.equals";
|
|
||||||
break;
|
|
||||||
case CriterionModifier.NotEquals:
|
|
||||||
modifierMessageID = "criterion_modifier.not_equals";
|
|
||||||
break;
|
|
||||||
case CriterionModifier.GreaterThan:
|
|
||||||
modifierMessageID = "criterion_modifier.greater_than";
|
|
||||||
break;
|
|
||||||
case CriterionModifier.LessThan:
|
|
||||||
modifierMessageID = "criterion_modifier.less_than";
|
|
||||||
break;
|
|
||||||
case CriterionModifier.IsNull:
|
|
||||||
modifierMessageID = "criterion_modifier.is_null";
|
|
||||||
break;
|
|
||||||
case CriterionModifier.NotNull:
|
|
||||||
modifierMessageID = "criterion_modifier.not_null";
|
|
||||||
break;
|
|
||||||
case CriterionModifier.Includes:
|
|
||||||
modifierMessageID = "criterion_modifier.includes";
|
|
||||||
break;
|
|
||||||
case CriterionModifier.IncludesAll:
|
|
||||||
modifierMessageID = "criterion_modifier.includes_all";
|
|
||||||
break;
|
|
||||||
case CriterionModifier.Excludes:
|
|
||||||
modifierMessageID = "criterion_modifier.excludes";
|
|
||||||
break;
|
|
||||||
case CriterionModifier.MatchesRegex:
|
|
||||||
modifierMessageID = "criterion_modifier.matches_regex";
|
|
||||||
break;
|
|
||||||
case CriterionModifier.NotMatchesRegex:
|
|
||||||
modifierMessageID = "criterion_modifier.not_matches_regex";
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
modifierMessageID = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
const modifierString = modifierMessageID
|
|
||||||
? intl.formatMessage({ id: modifierMessageID })
|
|
||||||
: "";
|
|
||||||
let valueString = "";
|
let valueString = "";
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -145,7 +97,7 @@ export abstract class Criterion<V extends CriterionValue> {
|
|||||||
|
|
||||||
public toJSON() {
|
public toJSON() {
|
||||||
const encodedCriterion = {
|
const encodedCriterion = {
|
||||||
type: this.criterionOption.value,
|
type: this.criterionOption.type,
|
||||||
// #394 - the presence of a # symbol results in the query URL being
|
// #394 - the presence of a # symbol results in the query URL being
|
||||||
// malformed. We could set encode: true in the queryString.stringify
|
// malformed. We could set encode: true in the queryString.stringify
|
||||||
// call below, but this results in a URL that gets pretty long and ugly.
|
// call below, but this results in a URL that gets pretty long and ugly.
|
||||||
@@ -171,37 +123,72 @@ export abstract class Criterion<V extends CriterionValue> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type InputType = "number" | "text" | undefined;
|
||||||
|
|
||||||
|
interface ICriterionOptionsParams {
|
||||||
|
messageID: string;
|
||||||
|
type: CriterionType;
|
||||||
|
inputType?: InputType;
|
||||||
|
parameterName?: string;
|
||||||
|
modifierOptions?: CriterionModifier[];
|
||||||
|
defaultModifier?: CriterionModifier;
|
||||||
|
options?: Option[];
|
||||||
|
}
|
||||||
export class CriterionOption {
|
export class CriterionOption {
|
||||||
public readonly messageID: string;
|
public readonly messageID: string;
|
||||||
public readonly value: CriterionType;
|
public readonly type: CriterionType;
|
||||||
public readonly parameterName: string;
|
public readonly parameterName: string;
|
||||||
|
public readonly modifierOptions: ILabeledValue[];
|
||||||
|
public readonly defaultModifier: CriterionModifier;
|
||||||
|
public readonly options: Option[] | undefined;
|
||||||
|
public readonly inputType: InputType;
|
||||||
|
|
||||||
constructor(messageID: string, value: CriterionType, parameterName?: string) {
|
constructor(options: ICriterionOptionsParams) {
|
||||||
this.messageID = messageID;
|
this.messageID = options.messageID;
|
||||||
this.value = value;
|
this.type = options.type;
|
||||||
this.parameterName = parameterName ?? value;
|
this.parameterName = options.parameterName ?? options.type;
|
||||||
|
this.modifierOptions = (options.modifierOptions ?? []).map((o) =>
|
||||||
|
Criterion.getModifierOption(o)
|
||||||
|
);
|
||||||
|
this.defaultModifier = options.defaultModifier ?? CriterionModifier.Equals;
|
||||||
|
this.options = options.options;
|
||||||
|
this.inputType = options.inputType;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createCriterionOption(value: CriterionType) {
|
export class StringCriterionOption extends CriterionOption {
|
||||||
return new CriterionOption(value, value);
|
constructor(
|
||||||
|
messageID: string,
|
||||||
|
value: CriterionType,
|
||||||
|
parameterName?: string,
|
||||||
|
options?: Option[]
|
||||||
|
) {
|
||||||
|
super({
|
||||||
|
messageID,
|
||||||
|
type: value,
|
||||||
|
parameterName,
|
||||||
|
modifierOptions: [
|
||||||
|
CriterionModifier.Equals,
|
||||||
|
CriterionModifier.NotEquals,
|
||||||
|
CriterionModifier.Includes,
|
||||||
|
CriterionModifier.Excludes,
|
||||||
|
CriterionModifier.IsNull,
|
||||||
|
CriterionModifier.NotNull,
|
||||||
|
CriterionModifier.MatchesRegex,
|
||||||
|
CriterionModifier.NotMatchesRegex,
|
||||||
|
],
|
||||||
|
defaultModifier: CriterionModifier.Equals,
|
||||||
|
options,
|
||||||
|
inputType: "text",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createStringCriterionOption(value: CriterionType) {
|
||||||
|
return new StringCriterionOption(value, value, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
export class StringCriterion extends Criterion<string> {
|
export class StringCriterion extends Criterion<string> {
|
||||||
public modifier = CriterionModifier.Equals;
|
|
||||||
public modifierOptions = [
|
|
||||||
StringCriterion.getModifierOption(CriterionModifier.Equals),
|
|
||||||
StringCriterion.getModifierOption(CriterionModifier.NotEquals),
|
|
||||||
StringCriterion.getModifierOption(CriterionModifier.Includes),
|
|
||||||
StringCriterion.getModifierOption(CriterionModifier.Excludes),
|
|
||||||
StringCriterion.getModifierOption(CriterionModifier.IsNull),
|
|
||||||
StringCriterion.getModifierOption(CriterionModifier.NotNull),
|
|
||||||
StringCriterion.getModifierOption(CriterionModifier.MatchesRegex),
|
|
||||||
StringCriterion.getModifierOption(CriterionModifier.NotMatchesRegex),
|
|
||||||
];
|
|
||||||
public options: string[] | undefined;
|
|
||||||
public value: string = "";
|
|
||||||
|
|
||||||
public getLabelValue() {
|
public getLabelValue() {
|
||||||
return this.value;
|
return this.value;
|
||||||
}
|
}
|
||||||
@@ -218,74 +205,125 @@ export class StringCriterion extends Criterion<string> {
|
|||||||
return str.replaceAll(c, encodeURIComponent(c));
|
return str.replaceAll(c, encodeURIComponent(c));
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(type: CriterionOption, options?: string[]) {
|
constructor(type: CriterionOption) {
|
||||||
super(type);
|
super(type, "");
|
||||||
|
|
||||||
this.options = options;
|
|
||||||
this.inputType = "text";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MandatoryStringCriterion extends StringCriterion {
|
export class MandatoryStringCriterionOption extends CriterionOption {
|
||||||
public modifierOptions = [
|
constructor(
|
||||||
StringCriterion.getModifierOption(CriterionModifier.Equals),
|
messageID: string,
|
||||||
StringCriterion.getModifierOption(CriterionModifier.NotEquals),
|
value: CriterionType,
|
||||||
StringCriterion.getModifierOption(CriterionModifier.Includes),
|
parameterName?: string,
|
||||||
StringCriterion.getModifierOption(CriterionModifier.Excludes),
|
options?: Option[]
|
||||||
StringCriterion.getModifierOption(CriterionModifier.MatchesRegex),
|
) {
|
||||||
StringCriterion.getModifierOption(CriterionModifier.NotMatchesRegex),
|
super({
|
||||||
];
|
messageID,
|
||||||
|
type: value,
|
||||||
|
parameterName,
|
||||||
|
modifierOptions: [
|
||||||
|
CriterionModifier.Equals,
|
||||||
|
CriterionModifier.NotEquals,
|
||||||
|
CriterionModifier.Includes,
|
||||||
|
CriterionModifier.Excludes,
|
||||||
|
CriterionModifier.MatchesRegex,
|
||||||
|
CriterionModifier.NotMatchesRegex,
|
||||||
|
],
|
||||||
|
defaultModifier: CriterionModifier.Equals,
|
||||||
|
options,
|
||||||
|
inputType: "text",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class BooleanCriterionOption extends CriterionOption {
|
||||||
|
constructor(messageID: string, value: CriterionType, parameterName?: string) {
|
||||||
|
super({
|
||||||
|
messageID,
|
||||||
|
type: value,
|
||||||
|
parameterName,
|
||||||
|
modifierOptions: [],
|
||||||
|
defaultModifier: CriterionModifier.Equals,
|
||||||
|
options: [true.toString(), false.toString()],
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class BooleanCriterion extends StringCriterion {
|
export class BooleanCriterion extends StringCriterion {
|
||||||
public modifier = CriterionModifier.Equals;
|
|
||||||
public modifierOptions = [];
|
|
||||||
|
|
||||||
constructor(type: CriterionOption) {
|
|
||||||
super(type, [true.toString(), false.toString()]);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected toCriterionInput(): boolean {
|
protected toCriterionInput(): boolean {
|
||||||
return this.value === "true";
|
return this.value === "true";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class NumberCriterion extends Criterion<number> {
|
export class NumberCriterionOption extends CriterionOption {
|
||||||
public modifier = CriterionModifier.Equals;
|
constructor(
|
||||||
public modifierOptions = [
|
messageID: string,
|
||||||
Criterion.getModifierOption(CriterionModifier.Equals),
|
value: CriterionType,
|
||||||
Criterion.getModifierOption(CriterionModifier.NotEquals),
|
parameterName?: string,
|
||||||
Criterion.getModifierOption(CriterionModifier.GreaterThan),
|
options?: Option[]
|
||||||
Criterion.getModifierOption(CriterionModifier.LessThan),
|
) {
|
||||||
Criterion.getModifierOption(CriterionModifier.IsNull),
|
super({
|
||||||
Criterion.getModifierOption(CriterionModifier.NotNull),
|
messageID,
|
||||||
];
|
type: value,
|
||||||
public options: number[] | undefined;
|
parameterName,
|
||||||
public value: number = 0;
|
modifierOptions: [
|
||||||
|
CriterionModifier.Equals,
|
||||||
|
CriterionModifier.NotEquals,
|
||||||
|
CriterionModifier.GreaterThan,
|
||||||
|
CriterionModifier.LessThan,
|
||||||
|
CriterionModifier.IsNull,
|
||||||
|
CriterionModifier.NotNull,
|
||||||
|
],
|
||||||
|
defaultModifier: CriterionModifier.Equals,
|
||||||
|
options,
|
||||||
|
inputType: "number",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createNumberCriterionOption(value: CriterionType) {
|
||||||
|
return new NumberCriterionOption(value, value, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
export class NumberCriterion extends Criterion<number> {
|
||||||
public getLabelValue() {
|
public getLabelValue() {
|
||||||
return this.value.toString();
|
return this.value.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(type: CriterionOption, options?: number[]) {
|
constructor(type: CriterionOption) {
|
||||||
super(type);
|
super(type, 0);
|
||||||
|
|
||||||
this.options = options;
|
|
||||||
this.inputType = "number";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export abstract class ILabeledIdCriterion extends Criterion<ILabeledId[]> {
|
export class ILabeledIdCriterionOption extends CriterionOption {
|
||||||
public modifier = CriterionModifier.IncludesAll;
|
constructor(
|
||||||
public modifierOptions = [
|
messageID: string,
|
||||||
Criterion.getModifierOption(CriterionModifier.IncludesAll),
|
value: CriterionType,
|
||||||
Criterion.getModifierOption(CriterionModifier.Includes),
|
parameterName: string,
|
||||||
Criterion.getModifierOption(CriterionModifier.Excludes),
|
includeAll: boolean
|
||||||
];
|
) {
|
||||||
|
const modifierOptions = [
|
||||||
|
CriterionModifier.Includes,
|
||||||
|
CriterionModifier.Excludes,
|
||||||
|
];
|
||||||
|
|
||||||
public options: IOptionType[] = [];
|
let defaultModifier = CriterionModifier.Includes;
|
||||||
public value: ILabeledId[] = [];
|
if (includeAll) {
|
||||||
|
modifierOptions.unshift(CriterionModifier.IncludesAll);
|
||||||
|
defaultModifier = CriterionModifier.IncludesAll;
|
||||||
|
}
|
||||||
|
|
||||||
|
super({
|
||||||
|
messageID,
|
||||||
|
type: value,
|
||||||
|
parameterName,
|
||||||
|
modifierOptions,
|
||||||
|
defaultModifier,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ILabeledIdCriterion extends Criterion<ILabeledId[]> {
|
||||||
public getLabelValue(): string {
|
public getLabelValue(): string {
|
||||||
return this.value.map((v) => v.label).join(", ");
|
return this.value.map((v) => v.label).join(", ");
|
||||||
}
|
}
|
||||||
@@ -303,33 +341,12 @@ export abstract class ILabeledIdCriterion extends Criterion<ILabeledId[]> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(type: CriterionOption, includeAll: boolean) {
|
constructor(type: CriterionOption) {
|
||||||
super(type);
|
super(type, []);
|
||||||
|
|
||||||
if (!includeAll) {
|
|
||||||
this.modifier = CriterionModifier.Includes;
|
|
||||||
this.modifierOptions = [
|
|
||||||
Criterion.getModifierOption(CriterionModifier.Includes),
|
|
||||||
Criterion.getModifierOption(CriterionModifier.Excludes),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export abstract class IHierarchicalLabeledIdCriterion extends Criterion<IHierarchicalLabelValue> {
|
export class IHierarchicalLabeledIdCriterion extends Criterion<IHierarchicalLabelValue> {
|
||||||
public modifier = CriterionModifier.IncludesAll;
|
|
||||||
public modifierOptions = [
|
|
||||||
Criterion.getModifierOption(CriterionModifier.IncludesAll),
|
|
||||||
Criterion.getModifierOption(CriterionModifier.Includes),
|
|
||||||
Criterion.getModifierOption(CriterionModifier.Excludes),
|
|
||||||
];
|
|
||||||
|
|
||||||
public options: IOptionType[] = [];
|
|
||||||
public value: IHierarchicalLabelValue = {
|
|
||||||
items: [],
|
|
||||||
depth: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
public encodeValue() {
|
public encodeValue() {
|
||||||
return {
|
return {
|
||||||
items: this.value.items.map((o) => {
|
items: this.value.items.map((o) => {
|
||||||
@@ -357,52 +374,41 @@ export abstract class IHierarchicalLabeledIdCriterion extends Criterion<IHierarc
|
|||||||
return `${labels} (+${this.value.depth > 0 ? this.value.depth : "all"})`;
|
return `${labels} (+${this.value.depth > 0 ? this.value.depth : "all"})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
public toJSON() {
|
constructor(type: CriterionOption) {
|
||||||
const encodedCriterion = {
|
const value: IHierarchicalLabelValue = {
|
||||||
type: this.criterionOption.value,
|
items: [],
|
||||||
value: this.encodeValue(),
|
depth: 0,
|
||||||
modifier: this.modifier,
|
|
||||||
};
|
};
|
||||||
return JSON.stringify(encodedCriterion);
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(type: CriterionOption, includeAll: boolean) {
|
super(type, value);
|
||||||
super(type);
|
|
||||||
|
|
||||||
if (!includeAll) {
|
|
||||||
this.modifier = CriterionModifier.Includes;
|
|
||||||
this.modifierOptions = [
|
|
||||||
Criterion.getModifierOption(CriterionModifier.Includes),
|
|
||||||
Criterion.getModifierOption(CriterionModifier.Excludes),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MandatoryNumberCriterion extends NumberCriterion {
|
export class MandatoryNumberCriterionOption extends CriterionOption {
|
||||||
public modifierOptions = [
|
constructor(messageID: string, value: CriterionType, parameterName?: string) {
|
||||||
Criterion.getModifierOption(CriterionModifier.Equals),
|
super({
|
||||||
Criterion.getModifierOption(CriterionModifier.NotEquals),
|
messageID,
|
||||||
Criterion.getModifierOption(CriterionModifier.GreaterThan),
|
type: value,
|
||||||
Criterion.getModifierOption(CriterionModifier.LessThan),
|
parameterName,
|
||||||
];
|
modifierOptions: [
|
||||||
|
CriterionModifier.Equals,
|
||||||
|
CriterionModifier.NotEquals,
|
||||||
|
CriterionModifier.GreaterThan,
|
||||||
|
CriterionModifier.LessThan,
|
||||||
|
],
|
||||||
|
defaultModifier: CriterionModifier.Equals,
|
||||||
|
inputType: "number",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createMandatoryNumberCriterionOption(value: CriterionType) {
|
||||||
|
return new MandatoryNumberCriterionOption(value, value, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DurationCriterion extends Criterion<number> {
|
export class DurationCriterion extends Criterion<number> {
|
||||||
public modifier = CriterionModifier.Equals;
|
constructor(type: CriterionOption) {
|
||||||
public modifierOptions = [
|
super(type, 0);
|
||||||
Criterion.getModifierOption(CriterionModifier.Equals),
|
|
||||||
Criterion.getModifierOption(CriterionModifier.NotEquals),
|
|
||||||
Criterion.getModifierOption(CriterionModifier.GreaterThan),
|
|
||||||
Criterion.getModifierOption(CriterionModifier.LessThan),
|
|
||||||
];
|
|
||||||
public options: number[] | undefined;
|
|
||||||
public value: number = 0;
|
|
||||||
|
|
||||||
constructor(type: CriterionOption, options?: number[]) {
|
|
||||||
super(type);
|
|
||||||
|
|
||||||
this.options = options;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public getLabelValue() {
|
public getLabelValue() {
|
||||||
|
|||||||
@@ -3,25 +3,27 @@ import {
|
|||||||
StringCriterion,
|
StringCriterion,
|
||||||
NumberCriterion,
|
NumberCriterion,
|
||||||
DurationCriterion,
|
DurationCriterion,
|
||||||
MandatoryStringCriterion,
|
NumberCriterionOption,
|
||||||
MandatoryNumberCriterion,
|
MandatoryStringCriterionOption,
|
||||||
CriterionOption,
|
MandatoryNumberCriterionOption,
|
||||||
|
StringCriterionOption,
|
||||||
|
ILabeledIdCriterion,
|
||||||
} from "./criterion";
|
} from "./criterion";
|
||||||
import { OrganizedCriterion } from "./organized";
|
import { OrganizedCriterion } from "./organized";
|
||||||
import { FavoriteCriterion } from "./favorite";
|
import { FavoriteCriterion } from "./favorite";
|
||||||
import { HasMarkersCriterion } from "./has-markers";
|
import { HasMarkersCriterion } from "./has-markers";
|
||||||
import {
|
import {
|
||||||
PerformerIsMissingCriterion,
|
PerformerIsMissingCriterionOption,
|
||||||
SceneIsMissingCriterion,
|
ImageIsMissingCriterionOption,
|
||||||
GalleryIsMissingCriterion,
|
TagIsMissingCriterionOption,
|
||||||
TagIsMissingCriterion,
|
SceneIsMissingCriterionOption,
|
||||||
StudioIsMissingCriterion,
|
IsMissingCriterion,
|
||||||
MovieIsMissingCriterion,
|
GalleryIsMissingCriterionOption,
|
||||||
ImageIsMissingCriterion,
|
StudioIsMissingCriterionOption,
|
||||||
|
MovieIsMissingCriterionOption,
|
||||||
} from "./is-missing";
|
} from "./is-missing";
|
||||||
import { NoneCriterion } from "./none";
|
import { NoneCriterion } from "./none";
|
||||||
import { PerformersCriterion } from "./performers";
|
import { PerformersCriterion } from "./performers";
|
||||||
import { RatingCriterion } from "./rating";
|
|
||||||
import { AverageResolutionCriterion, ResolutionCriterion } from "./resolution";
|
import { AverageResolutionCriterion, ResolutionCriterion } from "./resolution";
|
||||||
import { StudiosCriterion, ParentStudiosCriterion } from "./studios";
|
import { StudiosCriterion, ParentStudiosCriterion } from "./studios";
|
||||||
import {
|
import {
|
||||||
@@ -31,19 +33,22 @@ import {
|
|||||||
TagsCriterionOption,
|
TagsCriterionOption,
|
||||||
} from "./tags";
|
} from "./tags";
|
||||||
import { GenderCriterion } from "./gender";
|
import { GenderCriterion } from "./gender";
|
||||||
import { MoviesCriterion } from "./movies";
|
import { MoviesCriterionOption } from "./movies";
|
||||||
import { GalleriesCriterion } from "./galleries";
|
import { GalleriesCriterion } from "./galleries";
|
||||||
import { CriterionType } from "../types";
|
import { CriterionType } from "../types";
|
||||||
import { InteractiveCriterion } from "./interactive";
|
import { InteractiveCriterion } from "./interactive";
|
||||||
|
import { RatingCriterionOption } from "./rating";
|
||||||
|
|
||||||
export function makeCriteria(type: CriterionType = "none") {
|
export function makeCriteria(type: CriterionType = "none") {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "none":
|
case "none":
|
||||||
return new NoneCriterion();
|
return new NoneCriterion();
|
||||||
case "path":
|
case "path":
|
||||||
return new MandatoryStringCriterion(new CriterionOption(type, type));
|
return new StringCriterion(
|
||||||
|
new MandatoryStringCriterionOption(type, type)
|
||||||
|
);
|
||||||
case "rating":
|
case "rating":
|
||||||
return new RatingCriterion();
|
return new NumberCriterion(RatingCriterionOption);
|
||||||
case "organized":
|
case "organized":
|
||||||
return new OrganizedCriterion();
|
return new OrganizedCriterion();
|
||||||
case "o_counter":
|
case "o_counter":
|
||||||
@@ -53,31 +58,33 @@ export function makeCriteria(type: CriterionType = "none") {
|
|||||||
case "gallery_count":
|
case "gallery_count":
|
||||||
case "performer_count":
|
case "performer_count":
|
||||||
case "tag_count":
|
case "tag_count":
|
||||||
return new MandatoryNumberCriterion(new CriterionOption(type, type));
|
return new NumberCriterion(
|
||||||
|
new MandatoryNumberCriterionOption(type, type)
|
||||||
|
);
|
||||||
case "resolution":
|
case "resolution":
|
||||||
return new ResolutionCriterion();
|
return new ResolutionCriterion();
|
||||||
case "average_resolution":
|
case "average_resolution":
|
||||||
return new AverageResolutionCriterion();
|
return new AverageResolutionCriterion();
|
||||||
case "duration":
|
case "duration":
|
||||||
return new DurationCriterion(new CriterionOption(type, type));
|
return new DurationCriterion(new NumberCriterionOption(type, type));
|
||||||
case "favorite":
|
case "favorite":
|
||||||
return new FavoriteCriterion();
|
return new FavoriteCriterion();
|
||||||
case "hasMarkers":
|
case "hasMarkers":
|
||||||
return new HasMarkersCriterion();
|
return new HasMarkersCriterion();
|
||||||
case "sceneIsMissing":
|
case "sceneIsMissing":
|
||||||
return new SceneIsMissingCriterion();
|
return new IsMissingCriterion(SceneIsMissingCriterionOption);
|
||||||
case "imageIsMissing":
|
case "imageIsMissing":
|
||||||
return new ImageIsMissingCriterion();
|
return new IsMissingCriterion(ImageIsMissingCriterionOption);
|
||||||
case "performerIsMissing":
|
case "performerIsMissing":
|
||||||
return new PerformerIsMissingCriterion();
|
return new IsMissingCriterion(PerformerIsMissingCriterionOption);
|
||||||
case "galleryIsMissing":
|
case "galleryIsMissing":
|
||||||
return new GalleryIsMissingCriterion();
|
return new IsMissingCriterion(GalleryIsMissingCriterionOption);
|
||||||
case "tagIsMissing":
|
case "tagIsMissing":
|
||||||
return new TagIsMissingCriterion();
|
return new IsMissingCriterion(TagIsMissingCriterionOption);
|
||||||
case "studioIsMissing":
|
case "studioIsMissing":
|
||||||
return new StudioIsMissingCriterion();
|
return new IsMissingCriterion(StudioIsMissingCriterionOption);
|
||||||
case "movieIsMissing":
|
case "movieIsMissing":
|
||||||
return new MovieIsMissingCriterion();
|
return new IsMissingCriterion(MovieIsMissingCriterionOption);
|
||||||
case "tags":
|
case "tags":
|
||||||
return new TagsCriterion(TagsCriterionOption);
|
return new TagsCriterion(TagsCriterionOption);
|
||||||
case "sceneTags":
|
case "sceneTags":
|
||||||
@@ -91,15 +98,17 @@ export function makeCriteria(type: CriterionType = "none") {
|
|||||||
case "parent_studios":
|
case "parent_studios":
|
||||||
return new ParentStudiosCriterion();
|
return new ParentStudiosCriterion();
|
||||||
case "movies":
|
case "movies":
|
||||||
return new MoviesCriterion();
|
return new ILabeledIdCriterion(MoviesCriterionOption);
|
||||||
case "galleries":
|
case "galleries":
|
||||||
return new GalleriesCriterion();
|
return new GalleriesCriterion();
|
||||||
case "birth_year":
|
case "birth_year":
|
||||||
case "death_year":
|
case "death_year":
|
||||||
case "weight":
|
case "weight":
|
||||||
return new NumberCriterion(new CriterionOption(type, type));
|
return new NumberCriterion(new NumberCriterionOption(type, type));
|
||||||
case "age":
|
case "age":
|
||||||
return new MandatoryNumberCriterion(new CriterionOption(type, type));
|
return new NumberCriterion(
|
||||||
|
new MandatoryNumberCriterionOption(type, type)
|
||||||
|
);
|
||||||
case "gender":
|
case "gender":
|
||||||
return new GenderCriterion();
|
return new GenderCriterion();
|
||||||
case "ethnicity":
|
case "ethnicity":
|
||||||
@@ -115,7 +124,7 @@ export function makeCriteria(type: CriterionType = "none") {
|
|||||||
case "aliases":
|
case "aliases":
|
||||||
case "url":
|
case "url":
|
||||||
case "stash_id":
|
case "stash_id":
|
||||||
return new StringCriterion(new CriterionOption(type, type));
|
return new StringCriterion(new StringCriterionOption(type, type));
|
||||||
case "interactive":
|
case "interactive":
|
||||||
return new InteractiveCriterion();
|
return new InteractiveCriterion();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { BooleanCriterion, CriterionOption } from "./criterion";
|
import { BooleanCriterion, BooleanCriterionOption } from "./criterion";
|
||||||
|
|
||||||
export const FavoriteCriterionOption = new CriterionOption(
|
export const FavoriteCriterionOption = new BooleanCriterionOption(
|
||||||
"favourite",
|
"favourite",
|
||||||
"favorite",
|
"favorite",
|
||||||
"filter_favorites"
|
"filter_favorites"
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
import { CriterionOption, ILabeledIdCriterion } from "./criterion";
|
import { ILabeledIdCriterion, ILabeledIdCriterionOption } from "./criterion";
|
||||||
|
|
||||||
const galleriesCriterionOption = new CriterionOption("galleries", "galleries");
|
const galleriesCriterionOption = new ILabeledIdCriterionOption(
|
||||||
|
"galleries",
|
||||||
|
"galleries",
|
||||||
|
"galleries",
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
export class GalleriesCriterion extends ILabeledIdCriterion {
|
export class GalleriesCriterion extends ILabeledIdCriterion {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(galleriesCriterionOption, true);
|
super(galleriesCriterionOption);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,16 @@
|
|||||||
import {
|
import { GenderCriterionInput } from "src/core/generated-graphql";
|
||||||
CriterionModifier,
|
import { genderStrings, stringToGender } from "src/utils/gender";
|
||||||
GenderCriterionInput,
|
|
||||||
} from "src/core/generated-graphql";
|
|
||||||
import { getGenderStrings, stringToGender } from "src/core/StashService";
|
|
||||||
import { CriterionOption, StringCriterion } from "./criterion";
|
import { CriterionOption, StringCriterion } from "./criterion";
|
||||||
|
|
||||||
export const GenderCriterionOption = new CriterionOption("gender", "gender");
|
export const GenderCriterionOption = new CriterionOption({
|
||||||
|
messageID: "gender",
|
||||||
|
type: "gender",
|
||||||
|
options: genderStrings,
|
||||||
|
});
|
||||||
|
|
||||||
export class GenderCriterion extends StringCriterion {
|
export class GenderCriterion extends StringCriterion {
|
||||||
public modifier = CriterionModifier.Equals;
|
|
||||||
public modifierOptions = [];
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super(GenderCriterionOption, getGenderStrings());
|
super(GenderCriterionOption);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected toCriterionInput(): GenderCriterionInput {
|
protected toCriterionInput(): GenderCriterionInput {
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
import { CriterionOption, StringCriterion } from "./criterion";
|
import { CriterionOption, StringCriterion } from "./criterion";
|
||||||
|
|
||||||
export const HasMarkersCriterionOption = new CriterionOption(
|
export const HasMarkersCriterionOption = new CriterionOption({
|
||||||
"hasMarkers",
|
messageID: "hasMarkers",
|
||||||
"hasMarkers",
|
type: "hasMarkers",
|
||||||
"has_markers"
|
parameterName: "has_markers",
|
||||||
);
|
options: [true.toString(), false.toString()],
|
||||||
|
});
|
||||||
|
|
||||||
export class HasMarkersCriterion extends StringCriterion {
|
export class HasMarkersCriterion extends StringCriterion {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(HasMarkersCriterionOption, [true.toString(), false.toString()]);
|
super(HasMarkersCriterionOption);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected toCriterionInput(): string {
|
protected toCriterionInput(): string {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { BooleanCriterion, CriterionOption } from "./criterion";
|
import { BooleanCriterion, BooleanCriterionOption } from "./criterion";
|
||||||
|
|
||||||
export const InteractiveCriterionOption = new CriterionOption(
|
export const InteractiveCriterionOption = new BooleanCriterionOption(
|
||||||
"interactive",
|
"interactive",
|
||||||
"interactive"
|
"interactive"
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { CriterionModifier } from "src/core/generated-graphql";
|
import { CriterionModifier } from "src/core/generated-graphql";
|
||||||
import { CriterionOption, StringCriterion } from "./criterion";
|
import { CriterionType } from "../types";
|
||||||
|
import { CriterionOption, StringCriterion, Option } from "./criterion";
|
||||||
|
|
||||||
export abstract class IsMissingCriterion extends StringCriterion {
|
export class IsMissingCriterion extends StringCriterion {
|
||||||
public modifierOptions = [];
|
public modifierOptions = [];
|
||||||
public modifier = CriterionModifier.Equals;
|
public modifier = CriterionModifier.Equals;
|
||||||
|
|
||||||
@@ -10,136 +11,98 @@ export abstract class IsMissingCriterion extends StringCriterion {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SceneIsMissingCriterionOption = new CriterionOption(
|
class IsMissingCriterionOptionClass extends CriterionOption {
|
||||||
|
constructor(
|
||||||
|
messageID: string,
|
||||||
|
value: CriterionType,
|
||||||
|
parameterName: string,
|
||||||
|
options: Option[]
|
||||||
|
) {
|
||||||
|
super({
|
||||||
|
messageID,
|
||||||
|
type: value,
|
||||||
|
parameterName,
|
||||||
|
options,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SceneIsMissingCriterionOption = new IsMissingCriterionOptionClass(
|
||||||
"isMissing",
|
"isMissing",
|
||||||
"sceneIsMissing",
|
"sceneIsMissing",
|
||||||
"is_missing"
|
"is_missing",
|
||||||
|
[
|
||||||
|
"title",
|
||||||
|
"details",
|
||||||
|
"url",
|
||||||
|
"date",
|
||||||
|
"galleries",
|
||||||
|
"studio",
|
||||||
|
"movie",
|
||||||
|
"performers",
|
||||||
|
"tags",
|
||||||
|
"stash_id",
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
export class SceneIsMissingCriterion extends IsMissingCriterion {
|
export const ImageIsMissingCriterionOption = new IsMissingCriterionOptionClass(
|
||||||
constructor() {
|
|
||||||
super(SceneIsMissingCriterionOption, [
|
|
||||||
"title",
|
|
||||||
"details",
|
|
||||||
"url",
|
|
||||||
"date",
|
|
||||||
"galleries",
|
|
||||||
"studio",
|
|
||||||
"movie",
|
|
||||||
"performers",
|
|
||||||
"tags",
|
|
||||||
"stash_id",
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ImageIsMissingCriterionOption = new CriterionOption(
|
|
||||||
"isMissing",
|
"isMissing",
|
||||||
"imageIsMissing",
|
"imageIsMissing",
|
||||||
"is_missing"
|
"is_missing",
|
||||||
|
["title", "galleries", "studio", "performers", "tags"]
|
||||||
);
|
);
|
||||||
|
|
||||||
export class ImageIsMissingCriterion extends IsMissingCriterion {
|
export const PerformerIsMissingCriterionOption = new IsMissingCriterionOptionClass(
|
||||||
constructor() {
|
|
||||||
super(ImageIsMissingCriterionOption, [
|
|
||||||
"title",
|
|
||||||
"galleries",
|
|
||||||
"studio",
|
|
||||||
"performers",
|
|
||||||
"tags",
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const PerformerIsMissingCriterionOption = new CriterionOption(
|
|
||||||
"isMissing",
|
"isMissing",
|
||||||
"performerIsMissing",
|
"performerIsMissing",
|
||||||
"is_missing"
|
"is_missing",
|
||||||
|
[
|
||||||
|
"url",
|
||||||
|
"twitter",
|
||||||
|
"instagram",
|
||||||
|
"ethnicity",
|
||||||
|
"country",
|
||||||
|
"hair_color",
|
||||||
|
"eye_color",
|
||||||
|
"height",
|
||||||
|
"weight",
|
||||||
|
"measurements",
|
||||||
|
"fake_tits",
|
||||||
|
"career_length",
|
||||||
|
"tattoos",
|
||||||
|
"piercings",
|
||||||
|
"aliases",
|
||||||
|
"gender",
|
||||||
|
"image",
|
||||||
|
"details",
|
||||||
|
"stash_id",
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
export class PerformerIsMissingCriterion extends IsMissingCriterion {
|
export const GalleryIsMissingCriterionOption = new IsMissingCriterionOptionClass(
|
||||||
constructor() {
|
|
||||||
super(PerformerIsMissingCriterionOption, [
|
|
||||||
"url",
|
|
||||||
"twitter",
|
|
||||||
"instagram",
|
|
||||||
"ethnicity",
|
|
||||||
"country",
|
|
||||||
"hair_color",
|
|
||||||
"eye_color",
|
|
||||||
"height",
|
|
||||||
"weight",
|
|
||||||
"measurements",
|
|
||||||
"fake_tits",
|
|
||||||
"career_length",
|
|
||||||
"tattoos",
|
|
||||||
"piercings",
|
|
||||||
"aliases",
|
|
||||||
"gender",
|
|
||||||
"image",
|
|
||||||
"details",
|
|
||||||
"stash_id",
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const GalleryIsMissingCriterionOption = new CriterionOption(
|
|
||||||
"isMissing",
|
"isMissing",
|
||||||
"galleryIsMissing",
|
"galleryIsMissing",
|
||||||
"is_missing"
|
"is_missing",
|
||||||
|
["title", "details", "url", "date", "studio", "performers", "tags", "scenes"]
|
||||||
);
|
);
|
||||||
|
|
||||||
export class GalleryIsMissingCriterion extends IsMissingCriterion {
|
export const TagIsMissingCriterionOption = new IsMissingCriterionOptionClass(
|
||||||
constructor() {
|
|
||||||
super(GalleryIsMissingCriterionOption, [
|
|
||||||
"title",
|
|
||||||
"details",
|
|
||||||
"url",
|
|
||||||
"date",
|
|
||||||
"studio",
|
|
||||||
"performers",
|
|
||||||
"tags",
|
|
||||||
"scenes",
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const TagIsMissingCriterionOption = new CriterionOption(
|
|
||||||
"isMissing",
|
"isMissing",
|
||||||
"tagIsMissing",
|
"tagIsMissing",
|
||||||
"is_missing"
|
"is_missing",
|
||||||
|
["image"]
|
||||||
);
|
);
|
||||||
|
|
||||||
export class TagIsMissingCriterion extends IsMissingCriterion {
|
export const StudioIsMissingCriterionOption = new IsMissingCriterionOptionClass(
|
||||||
constructor() {
|
|
||||||
super(TagIsMissingCriterionOption, ["image"]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const StudioIsMissingCriterionOption = new CriterionOption(
|
|
||||||
"isMissing",
|
"isMissing",
|
||||||
"studioIsMissing",
|
"studioIsMissing",
|
||||||
"is_missing"
|
"is_missing",
|
||||||
|
["image", "stash_id", "details"]
|
||||||
);
|
);
|
||||||
|
|
||||||
export class StudioIsMissingCriterion extends IsMissingCriterion {
|
export const MovieIsMissingCriterionOption = new IsMissingCriterionOptionClass(
|
||||||
constructor() {
|
|
||||||
super(StudioIsMissingCriterionOption, ["image", "stash_id", "details"]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const MovieIsMissingCriterionOption = new CriterionOption(
|
|
||||||
"isMissing",
|
"isMissing",
|
||||||
"movieIsMissing",
|
"movieIsMissing",
|
||||||
"is_missing"
|
"is_missing",
|
||||||
|
["front_image", "back_image", "scenes"]
|
||||||
);
|
);
|
||||||
|
|
||||||
export class MovieIsMissingCriterion extends IsMissingCriterion {
|
|
||||||
constructor() {
|
|
||||||
super(MovieIsMissingCriterionOption, [
|
|
||||||
"front_image",
|
|
||||||
"back_image",
|
|
||||||
"scenes",
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
import { CriterionOption, ILabeledIdCriterion } from "./criterion";
|
import { ILabeledIdCriterion, ILabeledIdCriterionOption } from "./criterion";
|
||||||
|
|
||||||
export const MoviesCriterionOption = new CriterionOption("movies", "movies");
|
export const MoviesCriterionOption = new ILabeledIdCriterionOption(
|
||||||
|
"movies",
|
||||||
|
"movies",
|
||||||
|
"movies",
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
export class MoviesCriterion extends ILabeledIdCriterion {
|
export class MoviesCriterion extends ILabeledIdCriterion {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(MoviesCriterionOption, false);
|
super(MoviesCriterionOption);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
import { CriterionModifier } from "src/core/generated-graphql";
|
import { Criterion, StringCriterionOption } from "./criterion";
|
||||||
import { Criterion, CriterionOption } from "./criterion";
|
|
||||||
|
|
||||||
export const NoneCriterionOption = new CriterionOption("none", "none");
|
export const NoneCriterionOption = new StringCriterionOption(
|
||||||
|
"none",
|
||||||
|
"none",
|
||||||
|
"none"
|
||||||
|
);
|
||||||
export class NoneCriterion extends Criterion<string> {
|
export class NoneCriterion extends Criterion<string> {
|
||||||
public modifier = CriterionModifier.Equals;
|
|
||||||
public modifierOptions = [];
|
|
||||||
public options: undefined;
|
|
||||||
public value: string = "none";
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super(NoneCriterionOption);
|
super(NoneCriterionOption, "none");
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line class-methods-use-this
|
// eslint-disable-next-line class-methods-use-this
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { BooleanCriterion, CriterionOption } from "./criterion";
|
import { BooleanCriterion, BooleanCriterionOption } from "./criterion";
|
||||||
|
|
||||||
export const OrganizedCriterionOption = new CriterionOption(
|
export const OrganizedCriterionOption = new BooleanCriterionOption(
|
||||||
|
"organized",
|
||||||
"organized",
|
"organized",
|
||||||
"organized"
|
"organized"
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import { CriterionOption, ILabeledIdCriterion } from "./criterion";
|
import { ILabeledIdCriterion, ILabeledIdCriterionOption } from "./criterion";
|
||||||
|
|
||||||
export const PerformersCriterionOption = new CriterionOption(
|
export const PerformersCriterionOption = new ILabeledIdCriterionOption(
|
||||||
"performers",
|
"performers",
|
||||||
"performers"
|
"performers",
|
||||||
|
"performers",
|
||||||
|
true
|
||||||
);
|
);
|
||||||
|
|
||||||
export class PerformersCriterion extends ILabeledIdCriterion {
|
export class PerformersCriterion extends ILabeledIdCriterion {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(PerformersCriterionOption, true);
|
super(PerformersCriterionOption);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,8 @@
|
|||||||
import { CriterionModifier } from "src/core/generated-graphql";
|
import { NumberCriterionOption } from "./criterion";
|
||||||
import { Criterion, CriterionOption, NumberCriterion } from "./criterion";
|
|
||||||
|
|
||||||
export const RatingCriterionOption = new CriterionOption("rating", "rating");
|
export const RatingCriterionOption = new NumberCriterionOption(
|
||||||
|
"rating",
|
||||||
export class RatingCriterion extends NumberCriterion {
|
"rating",
|
||||||
public modifier = CriterionModifier.Equals;
|
"rating",
|
||||||
public modifierOptions = [
|
[1, 2, 3, 4, 5]
|
||||||
Criterion.getModifierOption(CriterionModifier.Equals),
|
);
|
||||||
Criterion.getModifierOption(CriterionModifier.NotEquals),
|
|
||||||
Criterion.getModifierOption(CriterionModifier.GreaterThan),
|
|
||||||
Criterion.getModifierOption(CriterionModifier.LessThan),
|
|
||||||
Criterion.getModifierOption(CriterionModifier.IsNull),
|
|
||||||
Criterion.getModifierOption(CriterionModifier.NotNull),
|
|
||||||
];
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super(RatingCriterionOption, [1, 2, 3, 4, 5]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,27 +1,8 @@
|
|||||||
import { CriterionModifier, ResolutionEnum } from "src/core/generated-graphql";
|
import { ResolutionEnum } from "src/core/generated-graphql";
|
||||||
|
import { CriterionType } from "../types";
|
||||||
import { CriterionOption, StringCriterion } from "./criterion";
|
import { CriterionOption, StringCriterion } from "./criterion";
|
||||||
|
|
||||||
abstract class AbstractResolutionCriterion extends StringCriterion {
|
abstract class AbstractResolutionCriterion extends StringCriterion {
|
||||||
public modifier = CriterionModifier.Equals;
|
|
||||||
public modifierOptions = [];
|
|
||||||
|
|
||||||
constructor(type: CriterionOption) {
|
|
||||||
super(type, [
|
|
||||||
"144p",
|
|
||||||
"240p",
|
|
||||||
"360p",
|
|
||||||
"480p",
|
|
||||||
"540p",
|
|
||||||
"720p",
|
|
||||||
"1080p",
|
|
||||||
"1440p",
|
|
||||||
"4k",
|
|
||||||
"5k",
|
|
||||||
"6k",
|
|
||||||
"8k",
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected toCriterionInput(): ResolutionEnum | undefined {
|
protected toCriterionInput(): ResolutionEnum | undefined {
|
||||||
switch (this.value) {
|
switch (this.value) {
|
||||||
case "144p":
|
case "144p":
|
||||||
@@ -55,21 +36,40 @@ abstract class AbstractResolutionCriterion extends StringCriterion {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ResolutionCriterionOption = new CriterionOption(
|
class ResolutionCriterionOptionType extends CriterionOption {
|
||||||
"resolution",
|
constructor(value: CriterionType) {
|
||||||
|
super({
|
||||||
|
messageID: value,
|
||||||
|
type: value,
|
||||||
|
parameterName: value,
|
||||||
|
options: [
|
||||||
|
"144p",
|
||||||
|
"240p",
|
||||||
|
"360p",
|
||||||
|
"480p",
|
||||||
|
"540p",
|
||||||
|
"720p",
|
||||||
|
"1080p",
|
||||||
|
"1440p",
|
||||||
|
"4k",
|
||||||
|
"5k",
|
||||||
|
"6k",
|
||||||
|
"8k",
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ResolutionCriterionOption = new ResolutionCriterionOptionType(
|
||||||
"resolution"
|
"resolution"
|
||||||
);
|
);
|
||||||
export class ResolutionCriterion extends AbstractResolutionCriterion {
|
export class ResolutionCriterion extends AbstractResolutionCriterion {
|
||||||
public modifier = CriterionModifier.Equals;
|
|
||||||
public modifierOptions = [];
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super(ResolutionCriterionOption);
|
super(ResolutionCriterionOption);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AverageResolutionCriterionOption = new CriterionOption(
|
export const AverageResolutionCriterionOption = new ResolutionCriterionOptionType(
|
||||||
"average_resolution",
|
|
||||||
"average_resolution"
|
"average_resolution"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,23 +1,30 @@
|
|||||||
import {
|
import {
|
||||||
CriterionOption,
|
|
||||||
IHierarchicalLabeledIdCriterion,
|
IHierarchicalLabeledIdCriterion,
|
||||||
ILabeledIdCriterion,
|
ILabeledIdCriterion,
|
||||||
|
ILabeledIdCriterionOption,
|
||||||
} from "./criterion";
|
} from "./criterion";
|
||||||
|
|
||||||
export const StudiosCriterionOption = new CriterionOption("studios", "studios");
|
export const StudiosCriterionOption = new ILabeledIdCriterionOption(
|
||||||
|
"studios",
|
||||||
|
"studios",
|
||||||
|
"studios",
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
export class StudiosCriterion extends IHierarchicalLabeledIdCriterion {
|
export class StudiosCriterion extends IHierarchicalLabeledIdCriterion {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(StudiosCriterionOption, false);
|
super(StudiosCriterionOption);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ParentStudiosCriterionOption = new CriterionOption(
|
export const ParentStudiosCriterionOption = new ILabeledIdCriterionOption(
|
||||||
"parent_studios",
|
"parent_studios",
|
||||||
"parent_studios",
|
"parent_studios",
|
||||||
"parents"
|
"parents",
|
||||||
|
false
|
||||||
);
|
);
|
||||||
export class ParentStudiosCriterion extends ILabeledIdCriterion {
|
export class ParentStudiosCriterion extends ILabeledIdCriterion {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(ParentStudiosCriterionOption, false);
|
super(ParentStudiosCriterionOption);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,22 @@
|
|||||||
import { CriterionOption, ILabeledIdCriterion } from "./criterion";
|
import { ILabeledIdCriterion, ILabeledIdCriterionOption } from "./criterion";
|
||||||
|
|
||||||
export class TagsCriterion extends ILabeledIdCriterion {
|
export class TagsCriterion extends ILabeledIdCriterion {}
|
||||||
constructor(type: CriterionOption) {
|
|
||||||
super(type, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const TagsCriterionOption = new CriterionOption("tags", "tags");
|
export const TagsCriterionOption = new ILabeledIdCriterionOption(
|
||||||
export const SceneTagsCriterionOption = new CriterionOption(
|
"tags",
|
||||||
|
"tags",
|
||||||
|
"tags",
|
||||||
|
true
|
||||||
|
);
|
||||||
|
export const SceneTagsCriterionOption = new ILabeledIdCriterionOption(
|
||||||
"sceneTags",
|
"sceneTags",
|
||||||
"sceneTags",
|
"sceneTags",
|
||||||
"scene_tags"
|
"scene_tags",
|
||||||
|
true
|
||||||
);
|
);
|
||||||
export const PerformerTagsCriterionOption = new CriterionOption(
|
export const PerformerTagsCriterionOption = new ILabeledIdCriterionOption(
|
||||||
"performerTags",
|
"performerTags",
|
||||||
"performerTags",
|
"performerTags",
|
||||||
"performer_tags"
|
"performer_tags",
|
||||||
|
true
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { FilterMode } from "src/core/generated-graphql";
|
||||||
import { ListFilterOptions } from "./filter-options";
|
import { ListFilterOptions } from "./filter-options";
|
||||||
import { GalleryListFilterOptions } from "./galleries";
|
import { GalleryListFilterOptions } from "./galleries";
|
||||||
import { ImageListFilterOptions } from "./images";
|
import { ImageListFilterOptions } from "./images";
|
||||||
@@ -7,7 +8,6 @@ import { SceneMarkerListFilterOptions } from "./scene-markers";
|
|||||||
import { SceneListFilterOptions } from "./scenes";
|
import { SceneListFilterOptions } from "./scenes";
|
||||||
import { StudioListFilterOptions } from "./studios";
|
import { StudioListFilterOptions } from "./studios";
|
||||||
import { TagListFilterOptions } from "./tags";
|
import { TagListFilterOptions } from "./tags";
|
||||||
import { FilterMode } from "./types";
|
|
||||||
|
|
||||||
export function getFilterOptions(mode: FilterMode): ListFilterOptions {
|
export function getFilterOptions(mode: FilterMode): ListFilterOptions {
|
||||||
switch (mode) {
|
switch (mode) {
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import queryString, { ParsedQuery } from "query-string";
|
import queryString, { ParsedQuery } from "query-string";
|
||||||
import { FindFilterType, SortDirectionEnum } from "src/core/generated-graphql";
|
import {
|
||||||
|
FilterMode,
|
||||||
|
FindFilterType,
|
||||||
|
SortDirectionEnum,
|
||||||
|
} from "src/core/generated-graphql";
|
||||||
import { Criterion, CriterionValue } from "./criteria/criterion";
|
import { Criterion, CriterionValue } from "./criteria/criterion";
|
||||||
import { makeCriteria } from "./criteria/factory";
|
import { makeCriteria } from "./criteria/factory";
|
||||||
import { DisplayMode } from "./types";
|
import { DisplayMode } from "./types";
|
||||||
@@ -23,6 +27,7 @@ const DEFAULT_PARAMS = {
|
|||||||
|
|
||||||
// TODO: handle customCriteria
|
// TODO: handle customCriteria
|
||||||
export class ListFilterModel {
|
export class ListFilterModel {
|
||||||
|
public mode: FilterMode;
|
||||||
public searchTerm?: string;
|
public searchTerm?: string;
|
||||||
public currentPage = DEFAULT_PARAMS.currentPage;
|
public currentPage = DEFAULT_PARAMS.currentPage;
|
||||||
public itemsPerPage = DEFAULT_PARAMS.itemsPerPage;
|
public itemsPerPage = DEFAULT_PARAMS.itemsPerPage;
|
||||||
@@ -33,16 +38,22 @@ export class ListFilterModel {
|
|||||||
public randomSeed = -1;
|
public randomSeed = -1;
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
|
mode: FilterMode,
|
||||||
rawParms?: ParsedQuery<string>,
|
rawParms?: ParsedQuery<string>,
|
||||||
defaultSort?: string,
|
defaultSort?: string,
|
||||||
defaultDisplayMode?: DisplayMode
|
defaultDisplayMode?: DisplayMode
|
||||||
) {
|
) {
|
||||||
|
this.mode = mode;
|
||||||
const params = rawParms as IQueryParameters;
|
const params = rawParms as IQueryParameters;
|
||||||
this.sortBy = defaultSort;
|
this.sortBy = defaultSort;
|
||||||
if (defaultDisplayMode !== undefined) this.displayMode = defaultDisplayMode;
|
if (defaultDisplayMode !== undefined) this.displayMode = defaultDisplayMode;
|
||||||
if (params) this.configureFromQueryParameters(params);
|
if (params) this.configureFromQueryParameters(params);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public clone() {
|
||||||
|
return Object.assign(new ListFilterModel(this.mode), this);
|
||||||
|
}
|
||||||
|
|
||||||
public configureFromQueryParameters(params: IQueryParameters) {
|
public configureFromQueryParameters(params: IQueryParameters) {
|
||||||
if (params.sortby !== undefined) {
|
if (params.sortby !== undefined) {
|
||||||
this.sortBy = params.sortby;
|
this.sortBy = params.sortby;
|
||||||
@@ -64,7 +75,7 @@ export class ListFilterModel {
|
|||||||
params.sortdir === "desc"
|
params.sortdir === "desc"
|
||||||
? SortDirectionEnum.Desc
|
? SortDirectionEnum.Desc
|
||||||
: SortDirectionEnum.Asc;
|
: SortDirectionEnum.Asc;
|
||||||
if (params.disp) {
|
if (params.disp !== undefined) {
|
||||||
this.displayMode = Number.parseInt(params.disp, 10);
|
this.displayMode = Number.parseInt(params.disp, 10);
|
||||||
}
|
}
|
||||||
if (params.q) {
|
if (params.q) {
|
||||||
@@ -153,6 +164,24 @@ export class ListFilterModel {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getSavedQueryParameters() {
|
||||||
|
const encodedCriteria: string[] = this.criteria.map((criterion) =>
|
||||||
|
criterion.toJSON()
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
perPage: this.itemsPerPage,
|
||||||
|
sortby: this.getSortBy() ?? undefined,
|
||||||
|
sortdir:
|
||||||
|
this.sortDirection === SortDirectionEnum.Desc ? "desc" : undefined,
|
||||||
|
disp: this.displayMode,
|
||||||
|
q: this.searchTerm,
|
||||||
|
c: encodedCriteria,
|
||||||
|
};
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
public makeQueryParameters(): string {
|
public makeQueryParameters(): string {
|
||||||
return queryString.stringify(this.getQueryParameters(), { encode: false });
|
return queryString.stringify(this.getQueryParameters(), { encode: false });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { createCriterionOption } from "./criteria/criterion";
|
import { createStringCriterionOption } from "./criteria/criterion";
|
||||||
import { GalleryIsMissingCriterionOption } from "./criteria/is-missing";
|
import { GalleryIsMissingCriterionOption } from "./criteria/is-missing";
|
||||||
import { NoneCriterionOption } from "./criteria/none";
|
|
||||||
import { OrganizedCriterionOption } from "./criteria/organized";
|
import { OrganizedCriterionOption } from "./criteria/organized";
|
||||||
import { PerformersCriterionOption } from "./criteria/performers";
|
import { PerformersCriterionOption } from "./criteria/performers";
|
||||||
import { RatingCriterionOption } from "./criteria/rating";
|
import { RatingCriterionOption } from "./criteria/rating";
|
||||||
@@ -39,20 +38,19 @@ const displayModeOptions = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const criterionOptions = [
|
const criterionOptions = [
|
||||||
NoneCriterionOption,
|
createStringCriterionOption("path"),
|
||||||
createCriterionOption("path"),
|
|
||||||
RatingCriterionOption,
|
RatingCriterionOption,
|
||||||
OrganizedCriterionOption,
|
OrganizedCriterionOption,
|
||||||
AverageResolutionCriterionOption,
|
AverageResolutionCriterionOption,
|
||||||
GalleryIsMissingCriterionOption,
|
GalleryIsMissingCriterionOption,
|
||||||
TagsCriterionOption,
|
TagsCriterionOption,
|
||||||
createCriterionOption("tag_count"),
|
createStringCriterionOption("tag_count"),
|
||||||
PerformerTagsCriterionOption,
|
PerformerTagsCriterionOption,
|
||||||
PerformersCriterionOption,
|
PerformersCriterionOption,
|
||||||
createCriterionOption("performer_count"),
|
createStringCriterionOption("performer_count"),
|
||||||
createCriterionOption("image_count"),
|
createStringCriterionOption("image_count"),
|
||||||
StudiosCriterionOption,
|
StudiosCriterionOption,
|
||||||
createCriterionOption("url"),
|
createStringCriterionOption("url"),
|
||||||
];
|
];
|
||||||
|
|
||||||
export const GalleryListFilterOptions = new ListFilterOptions(
|
export const GalleryListFilterOptions = new ListFilterOptions(
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { createCriterionOption } from "./criteria/criterion";
|
import {
|
||||||
|
createMandatoryNumberCriterionOption,
|
||||||
|
createStringCriterionOption,
|
||||||
|
} from "./criteria/criterion";
|
||||||
import { ImageIsMissingCriterionOption } from "./criteria/is-missing";
|
import { ImageIsMissingCriterionOption } from "./criteria/is-missing";
|
||||||
import { NoneCriterionOption } from "./criteria/none";
|
|
||||||
import { OrganizedCriterionOption } from "./criteria/organized";
|
import { OrganizedCriterionOption } from "./criteria/organized";
|
||||||
import { PerformersCriterionOption } from "./criteria/performers";
|
import { PerformersCriterionOption } from "./criteria/performers";
|
||||||
import { RatingCriterionOption } from "./criteria/rating";
|
import { RatingCriterionOption } from "./criteria/rating";
|
||||||
@@ -29,18 +31,17 @@ const sortByOptions = [
|
|||||||
|
|
||||||
const displayModeOptions = [DisplayMode.Grid, DisplayMode.Wall];
|
const displayModeOptions = [DisplayMode.Grid, DisplayMode.Wall];
|
||||||
const criterionOptions = [
|
const criterionOptions = [
|
||||||
NoneCriterionOption,
|
createStringCriterionOption("path"),
|
||||||
createCriterionOption("path"),
|
|
||||||
RatingCriterionOption,
|
RatingCriterionOption,
|
||||||
OrganizedCriterionOption,
|
OrganizedCriterionOption,
|
||||||
createCriterionOption("o_counter"),
|
createMandatoryNumberCriterionOption("o_counter"),
|
||||||
ResolutionCriterionOption,
|
ResolutionCriterionOption,
|
||||||
ImageIsMissingCriterionOption,
|
ImageIsMissingCriterionOption,
|
||||||
TagsCriterionOption,
|
TagsCriterionOption,
|
||||||
createCriterionOption("tag_count"),
|
createMandatoryNumberCriterionOption("tag_count"),
|
||||||
PerformerTagsCriterionOption,
|
PerformerTagsCriterionOption,
|
||||||
PerformersCriterionOption,
|
PerformersCriterionOption,
|
||||||
createCriterionOption("performer_count"),
|
createMandatoryNumberCriterionOption("performer_count"),
|
||||||
StudiosCriterionOption,
|
StudiosCriterionOption,
|
||||||
];
|
];
|
||||||
export const ImageListFilterOptions = new ListFilterOptions(
|
export const ImageListFilterOptions = new ListFilterOptions(
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { createCriterionOption } from "./criteria/criterion";
|
import { createStringCriterionOption } from "./criteria/criterion";
|
||||||
import { MovieIsMissingCriterionOption } from "./criteria/is-missing";
|
import { MovieIsMissingCriterionOption } from "./criteria/is-missing";
|
||||||
import { NoneCriterionOption } from "./criteria/none";
|
|
||||||
import { StudiosCriterionOption } from "./criteria/studios";
|
import { StudiosCriterionOption } from "./criteria/studios";
|
||||||
import { ListFilterOptions } from "./filter-options";
|
import { ListFilterOptions } from "./filter-options";
|
||||||
import { DisplayMode } from "./types";
|
import { DisplayMode } from "./types";
|
||||||
@@ -17,10 +16,9 @@ const sortByOptions = ["name", "random"]
|
|||||||
]);
|
]);
|
||||||
const displayModeOptions = [DisplayMode.Grid];
|
const displayModeOptions = [DisplayMode.Grid];
|
||||||
const criterionOptions = [
|
const criterionOptions = [
|
||||||
NoneCriterionOption,
|
|
||||||
StudiosCriterionOption,
|
StudiosCriterionOption,
|
||||||
MovieIsMissingCriterionOption,
|
MovieIsMissingCriterionOption,
|
||||||
createCriterionOption("url"),
|
createStringCriterionOption("url"),
|
||||||
];
|
];
|
||||||
|
|
||||||
export const MovieListFilterOptions = new ListFilterOptions(
|
export const MovieListFilterOptions = new ListFilterOptions(
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
import { createCriterionOption } from "./criteria/criterion";
|
import {
|
||||||
|
createNumberCriterionOption,
|
||||||
|
createMandatoryNumberCriterionOption,
|
||||||
|
createStringCriterionOption,
|
||||||
|
} from "./criteria/criterion";
|
||||||
import { FavoriteCriterionOption } from "./criteria/favorite";
|
import { FavoriteCriterionOption } from "./criteria/favorite";
|
||||||
import { GenderCriterionOption } from "./criteria/gender";
|
import { GenderCriterionOption } from "./criteria/gender";
|
||||||
import { PerformerIsMissingCriterionOption } from "./criteria/is-missing";
|
import { PerformerIsMissingCriterionOption } from "./criteria/is-missing";
|
||||||
import { NoneCriterionOption } from "./criteria/none";
|
|
||||||
import { RatingCriterionOption } from "./criteria/rating";
|
import { RatingCriterionOption } from "./criteria/rating";
|
||||||
import { StudiosCriterionOption } from "./criteria/studios";
|
import { StudiosCriterionOption } from "./criteria/studios";
|
||||||
import { TagsCriterionOption } from "./criteria/tags";
|
import { TagsCriterionOption } from "./criteria/tags";
|
||||||
@@ -55,19 +58,19 @@ const stringCriteria: CriterionType[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const criterionOptions = [
|
const criterionOptions = [
|
||||||
NoneCriterionOption,
|
|
||||||
FavoriteCriterionOption,
|
FavoriteCriterionOption,
|
||||||
GenderCriterionOption,
|
GenderCriterionOption,
|
||||||
PerformerIsMissingCriterionOption,
|
PerformerIsMissingCriterionOption,
|
||||||
TagsCriterionOption,
|
TagsCriterionOption,
|
||||||
RatingCriterionOption,
|
RatingCriterionOption,
|
||||||
StudiosCriterionOption,
|
StudiosCriterionOption,
|
||||||
createCriterionOption("url"),
|
createStringCriterionOption("url"),
|
||||||
createCriterionOption("tag_count"),
|
createMandatoryNumberCriterionOption("tag_count"),
|
||||||
createCriterionOption("scene_count"),
|
createMandatoryNumberCriterionOption("scene_count"),
|
||||||
createCriterionOption("image_count"),
|
createMandatoryNumberCriterionOption("image_count"),
|
||||||
createCriterionOption("gallery_count"),
|
createMandatoryNumberCriterionOption("gallery_count"),
|
||||||
...numberCriteria.concat(stringCriteria).map((c) => createCriterionOption(c)),
|
...numberCriteria.map((c) => createNumberCriterionOption(c)),
|
||||||
|
...stringCriteria.map((c) => createStringCriterionOption(c)),
|
||||||
];
|
];
|
||||||
export const PerformerListFilterOptions = new ListFilterOptions(
|
export const PerformerListFilterOptions = new ListFilterOptions(
|
||||||
defaultSortBy,
|
defaultSortBy,
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { NoneCriterionOption } from "./criteria/none";
|
|
||||||
import { PerformersCriterionOption } from "./criteria/performers";
|
import { PerformersCriterionOption } from "./criteria/performers";
|
||||||
import { SceneTagsCriterionOption, TagsCriterionOption } from "./criteria/tags";
|
import { SceneTagsCriterionOption, TagsCriterionOption } from "./criteria/tags";
|
||||||
import { ListFilterOptions } from "./filter-options";
|
import { ListFilterOptions } from "./filter-options";
|
||||||
@@ -14,7 +13,6 @@ const sortByOptions = [
|
|||||||
].map(ListFilterOptions.createSortBy);
|
].map(ListFilterOptions.createSortBy);
|
||||||
const displayModeOptions = [DisplayMode.Wall];
|
const displayModeOptions = [DisplayMode.Wall];
|
||||||
const criterionOptions = [
|
const criterionOptions = [
|
||||||
NoneCriterionOption,
|
|
||||||
TagsCriterionOption,
|
TagsCriterionOption,
|
||||||
SceneTagsCriterionOption,
|
SceneTagsCriterionOption,
|
||||||
PerformersCriterionOption,
|
PerformersCriterionOption,
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { createCriterionOption } from "./criteria/criterion";
|
import {
|
||||||
|
createMandatoryNumberCriterionOption,
|
||||||
|
createStringCriterionOption,
|
||||||
|
} from "./criteria/criterion";
|
||||||
import { HasMarkersCriterionOption } from "./criteria/has-markers";
|
import { HasMarkersCriterionOption } from "./criteria/has-markers";
|
||||||
import { SceneIsMissingCriterionOption } from "./criteria/is-missing";
|
import { SceneIsMissingCriterionOption } from "./criteria/is-missing";
|
||||||
import { MoviesCriterionOption } from "./criteria/movies";
|
import { MoviesCriterionOption } from "./criteria/movies";
|
||||||
import { NoneCriterionOption } from "./criteria/none";
|
|
||||||
import { OrganizedCriterionOption } from "./criteria/organized";
|
import { OrganizedCriterionOption } from "./criteria/organized";
|
||||||
import { PerformersCriterionOption } from "./criteria/performers";
|
import { PerformersCriterionOption } from "./criteria/performers";
|
||||||
import { RatingCriterionOption } from "./criteria/rating";
|
import { RatingCriterionOption } from "./criteria/rating";
|
||||||
@@ -44,24 +46,23 @@ const displayModeOptions = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const criterionOptions = [
|
const criterionOptions = [
|
||||||
NoneCriterionOption,
|
createStringCriterionOption("path"),
|
||||||
createCriterionOption("path"),
|
|
||||||
RatingCriterionOption,
|
RatingCriterionOption,
|
||||||
OrganizedCriterionOption,
|
OrganizedCriterionOption,
|
||||||
createCriterionOption("o_counter"),
|
createMandatoryNumberCriterionOption("o_counter"),
|
||||||
ResolutionCriterionOption,
|
ResolutionCriterionOption,
|
||||||
createCriterionOption("duration"),
|
createMandatoryNumberCriterionOption("duration"),
|
||||||
HasMarkersCriterionOption,
|
HasMarkersCriterionOption,
|
||||||
SceneIsMissingCriterionOption,
|
SceneIsMissingCriterionOption,
|
||||||
TagsCriterionOption,
|
TagsCriterionOption,
|
||||||
createCriterionOption("tag_count"),
|
createMandatoryNumberCriterionOption("tag_count"),
|
||||||
PerformerTagsCriterionOption,
|
PerformerTagsCriterionOption,
|
||||||
PerformersCriterionOption,
|
PerformersCriterionOption,
|
||||||
createCriterionOption("performer_count"),
|
createMandatoryNumberCriterionOption("performer_count"),
|
||||||
StudiosCriterionOption,
|
StudiosCriterionOption,
|
||||||
MoviesCriterionOption,
|
MoviesCriterionOption,
|
||||||
createCriterionOption("url"),
|
createStringCriterionOption("url"),
|
||||||
createCriterionOption("stash_id"),
|
createStringCriterionOption("stash_id"),
|
||||||
InteractiveCriterionOption,
|
InteractiveCriterionOption,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { createCriterionOption } from "./criteria/criterion";
|
import {
|
||||||
|
createMandatoryNumberCriterionOption,
|
||||||
|
createStringCriterionOption,
|
||||||
|
} from "./criteria/criterion";
|
||||||
import { StudioIsMissingCriterionOption } from "./criteria/is-missing";
|
import { StudioIsMissingCriterionOption } from "./criteria/is-missing";
|
||||||
import { NoneCriterionOption } from "./criteria/none";
|
|
||||||
import { RatingCriterionOption } from "./criteria/rating";
|
import { RatingCriterionOption } from "./criteria/rating";
|
||||||
import { ParentStudiosCriterionOption } from "./criteria/studios";
|
import { ParentStudiosCriterionOption } from "./criteria/studios";
|
||||||
import { ListFilterOptions } from "./filter-options";
|
import { ListFilterOptions } from "./filter-options";
|
||||||
@@ -26,15 +28,14 @@ const sortByOptions = ["name", "random", "rating"]
|
|||||||
|
|
||||||
const displayModeOptions = [DisplayMode.Grid];
|
const displayModeOptions = [DisplayMode.Grid];
|
||||||
const criterionOptions = [
|
const criterionOptions = [
|
||||||
NoneCriterionOption,
|
|
||||||
ParentStudiosCriterionOption,
|
ParentStudiosCriterionOption,
|
||||||
StudioIsMissingCriterionOption,
|
StudioIsMissingCriterionOption,
|
||||||
RatingCriterionOption,
|
RatingCriterionOption,
|
||||||
createCriterionOption("scene_count"),
|
createMandatoryNumberCriterionOption("scene_count"),
|
||||||
createCriterionOption("image_count"),
|
createMandatoryNumberCriterionOption("image_count"),
|
||||||
createCriterionOption("gallery_count"),
|
createMandatoryNumberCriterionOption("gallery_count"),
|
||||||
createCriterionOption("url"),
|
createStringCriterionOption("url"),
|
||||||
createCriterionOption("stash_id"),
|
createStringCriterionOption("stash_id"),
|
||||||
];
|
];
|
||||||
|
|
||||||
export const StudioListFilterOptions = new ListFilterOptions(
|
export const StudioListFilterOptions = new ListFilterOptions(
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { createCriterionOption } from "./criteria/criterion";
|
import { createMandatoryNumberCriterionOption } from "./criteria/criterion";
|
||||||
import { TagIsMissingCriterionOption } from "./criteria/is-missing";
|
import { TagIsMissingCriterionOption } from "./criteria/is-missing";
|
||||||
import { NoneCriterionOption } from "./criteria/none";
|
|
||||||
import { ListFilterOptions } from "./filter-options";
|
import { ListFilterOptions } from "./filter-options";
|
||||||
import { DisplayMode } from "./types";
|
import { DisplayMode } from "./types";
|
||||||
|
|
||||||
@@ -34,12 +33,11 @@ const sortByOptions = [
|
|||||||
|
|
||||||
const displayModeOptions = [DisplayMode.Grid, DisplayMode.List];
|
const displayModeOptions = [DisplayMode.Grid, DisplayMode.List];
|
||||||
const criterionOptions = [
|
const criterionOptions = [
|
||||||
NoneCriterionOption,
|
|
||||||
TagIsMissingCriterionOption,
|
TagIsMissingCriterionOption,
|
||||||
createCriterionOption("scene_count"),
|
createMandatoryNumberCriterionOption("scene_count"),
|
||||||
createCriterionOption("image_count"),
|
createMandatoryNumberCriterionOption("image_count"),
|
||||||
createCriterionOption("gallery_count"),
|
createMandatoryNumberCriterionOption("gallery_count"),
|
||||||
createCriterionOption("performer_count"),
|
createMandatoryNumberCriterionOption("performer_count"),
|
||||||
// marker count has been disabled for now due to performance issues
|
// marker count has been disabled for now due to performance issues
|
||||||
// ListFilterModel.createCriterionOption("marker_count"),
|
// ListFilterModel.createCriterionOption("marker_count"),
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -8,17 +8,6 @@ export enum DisplayMode {
|
|||||||
Tagger,
|
Tagger,
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum FilterMode {
|
|
||||||
Scenes,
|
|
||||||
Performers,
|
|
||||||
Studios,
|
|
||||||
Galleries,
|
|
||||||
SceneMarkers,
|
|
||||||
Movies,
|
|
||||||
Tags,
|
|
||||||
Images,
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ILabeledId {
|
export interface ILabeledId {
|
||||||
id: string;
|
id: string;
|
||||||
label: string;
|
label: string;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import queryString from "query-string";
|
import queryString from "query-string";
|
||||||
import { RouteComponentProps } from "react-router-dom";
|
import { RouteComponentProps } from "react-router-dom";
|
||||||
|
import { FilterMode } from "src/core/generated-graphql";
|
||||||
import { ListFilterModel } from "./list-filter/filter";
|
import { ListFilterModel } from "./list-filter/filter";
|
||||||
import { SceneListFilterOptions } from "./list-filter/scenes";
|
import { SceneListFilterOptions } from "./list-filter/scenes";
|
||||||
|
|
||||||
@@ -27,7 +28,7 @@ export class SceneQueue {
|
|||||||
public static fromListFilterModel(filter: ListFilterModel) {
|
public static fromListFilterModel(filter: ListFilterModel) {
|
||||||
const ret = new SceneQueue();
|
const ret = new SceneQueue();
|
||||||
|
|
||||||
const filterCopy = Object.assign(new ListFilterModel(), filter);
|
const filterCopy = filter.clone();
|
||||||
filterCopy.itemsPerPage = 40;
|
filterCopy.itemsPerPage = 40;
|
||||||
|
|
||||||
ret.originalQueryPage = filter.currentPage;
|
ret.originalQueryPage = filter.currentPage;
|
||||||
@@ -95,6 +96,7 @@ export class SceneQueue {
|
|||||||
|
|
||||||
if (parsed.qfp) {
|
if (parsed.qfp) {
|
||||||
const query = new ListFilterModel(
|
const query = new ListFilterModel(
|
||||||
|
FilterMode.Scenes,
|
||||||
translated as queryString.ParsedQuery,
|
translated as queryString.ParsedQuery,
|
||||||
SceneListFilterOptions.defaultSortBy
|
SceneListFilterOptions.defaultSortBy
|
||||||
);
|
);
|
||||||
|
|||||||
49
ui/v2.5/src/utils/gender.ts
Normal file
49
ui/v2.5/src/utils/gender.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import * as GQL from "../core/generated-graphql";
|
||||||
|
|
||||||
|
export const stringGenderMap = new Map<string, GQL.GenderEnum>([
|
||||||
|
["Male", GQL.GenderEnum.Male],
|
||||||
|
["Female", GQL.GenderEnum.Female],
|
||||||
|
["Transgender Male", GQL.GenderEnum.TransgenderMale],
|
||||||
|
["Transgender Female", GQL.GenderEnum.TransgenderFemale],
|
||||||
|
["Intersex", GQL.GenderEnum.Intersex],
|
||||||
|
["Non-Binary", GQL.GenderEnum.NonBinary],
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const genderToString = (value?: GQL.GenderEnum | string) => {
|
||||||
|
if (!value) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const foundEntry = Array.from(stringGenderMap.entries()).find((e) => {
|
||||||
|
return e[1] === value;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (foundEntry) {
|
||||||
|
return foundEntry[0];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const stringToGender = (
|
||||||
|
value?: string | null,
|
||||||
|
caseInsensitive?: boolean
|
||||||
|
) => {
|
||||||
|
if (!value) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ret = stringGenderMap.get(value);
|
||||||
|
if (ret || !caseInsensitive) {
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
const asUpper = value.toUpperCase();
|
||||||
|
const foundEntry = Array.from(stringGenderMap.entries()).find((e) => {
|
||||||
|
return e[0].toUpperCase() === asUpper;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (foundEntry) {
|
||||||
|
return foundEntry[1];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const genderStrings = Array.from(stringGenderMap.keys());
|
||||||
@@ -30,7 +30,7 @@ const makePerformerScenesUrl = (
|
|||||||
extraCriteria?: Criterion<CriterionValue>[]
|
extraCriteria?: Criterion<CriterionValue>[]
|
||||||
) => {
|
) => {
|
||||||
if (!performer.id) return "#";
|
if (!performer.id) return "#";
|
||||||
const filter = new ListFilterModel();
|
const filter = new ListFilterModel(GQL.FilterMode.Scenes);
|
||||||
const criterion = new PerformersCriterion();
|
const criterion = new PerformersCriterion();
|
||||||
criterion.value = [
|
criterion.value = [
|
||||||
{ id: performer.id, label: performer.name || `Performer ${performer.id}` },
|
{ id: performer.id, label: performer.name || `Performer ${performer.id}` },
|
||||||
@@ -45,7 +45,7 @@ const makePerformerImagesUrl = (
|
|||||||
extraCriteria?: Criterion<CriterionValue>[]
|
extraCriteria?: Criterion<CriterionValue>[]
|
||||||
) => {
|
) => {
|
||||||
if (!performer.id) return "#";
|
if (!performer.id) return "#";
|
||||||
const filter = new ListFilterModel();
|
const filter = new ListFilterModel(GQL.FilterMode.Images);
|
||||||
const criterion = new PerformersCriterion();
|
const criterion = new PerformersCriterion();
|
||||||
criterion.value = [
|
criterion.value = [
|
||||||
{ id: performer.id, label: performer.name || `Performer ${performer.id}` },
|
{ id: performer.id, label: performer.name || `Performer ${performer.id}` },
|
||||||
@@ -60,7 +60,7 @@ const makePerformerGalleriesUrl = (
|
|||||||
extraCriteria?: Criterion<CriterionValue>[]
|
extraCriteria?: Criterion<CriterionValue>[]
|
||||||
) => {
|
) => {
|
||||||
if (!performer.id) return "#";
|
if (!performer.id) return "#";
|
||||||
const filter = new ListFilterModel();
|
const filter = new ListFilterModel(GQL.FilterMode.Galleries);
|
||||||
const criterion = new PerformersCriterion();
|
const criterion = new PerformersCriterion();
|
||||||
criterion.value = [
|
criterion.value = [
|
||||||
{ id: performer.id, label: performer.name || `Performer ${performer.id}` },
|
{ id: performer.id, label: performer.name || `Performer ${performer.id}` },
|
||||||
@@ -74,7 +74,7 @@ const makePerformersCountryUrl = (
|
|||||||
performer: Partial<GQL.PerformerDataFragment>
|
performer: Partial<GQL.PerformerDataFragment>
|
||||||
) => {
|
) => {
|
||||||
if (!performer.id) return "#";
|
if (!performer.id) return "#";
|
||||||
const filter = new ListFilterModel();
|
const filter = new ListFilterModel(GQL.FilterMode.Performers);
|
||||||
const criterion = new CountryCriterion();
|
const criterion = new CountryCriterion();
|
||||||
criterion.value = `${performer.country}`;
|
criterion.value = `${performer.country}`;
|
||||||
filter.criteria.push(criterion);
|
filter.criteria.push(criterion);
|
||||||
@@ -83,7 +83,7 @@ const makePerformersCountryUrl = (
|
|||||||
|
|
||||||
const makeStudioScenesUrl = (studio: Partial<GQL.StudioDataFragment>) => {
|
const makeStudioScenesUrl = (studio: Partial<GQL.StudioDataFragment>) => {
|
||||||
if (!studio.id) return "#";
|
if (!studio.id) return "#";
|
||||||
const filter = new ListFilterModel();
|
const filter = new ListFilterModel(GQL.FilterMode.Scenes);
|
||||||
const criterion = new StudiosCriterion();
|
const criterion = new StudiosCriterion();
|
||||||
criterion.value = {
|
criterion.value = {
|
||||||
items: [{ id: studio.id, label: studio.name || `Studio ${studio.id}` }],
|
items: [{ id: studio.id, label: studio.name || `Studio ${studio.id}` }],
|
||||||
@@ -95,7 +95,7 @@ const makeStudioScenesUrl = (studio: Partial<GQL.StudioDataFragment>) => {
|
|||||||
|
|
||||||
const makeStudioImagesUrl = (studio: Partial<GQL.StudioDataFragment>) => {
|
const makeStudioImagesUrl = (studio: Partial<GQL.StudioDataFragment>) => {
|
||||||
if (!studio.id) return "#";
|
if (!studio.id) return "#";
|
||||||
const filter = new ListFilterModel();
|
const filter = new ListFilterModel(GQL.FilterMode.Images);
|
||||||
const criterion = new StudiosCriterion();
|
const criterion = new StudiosCriterion();
|
||||||
criterion.value = {
|
criterion.value = {
|
||||||
items: [{ id: studio.id, label: studio.name || `Studio ${studio.id}` }],
|
items: [{ id: studio.id, label: studio.name || `Studio ${studio.id}` }],
|
||||||
@@ -107,7 +107,7 @@ const makeStudioImagesUrl = (studio: Partial<GQL.StudioDataFragment>) => {
|
|||||||
|
|
||||||
const makeStudioGalleriesUrl = (studio: Partial<GQL.StudioDataFragment>) => {
|
const makeStudioGalleriesUrl = (studio: Partial<GQL.StudioDataFragment>) => {
|
||||||
if (!studio.id) return "#";
|
if (!studio.id) return "#";
|
||||||
const filter = new ListFilterModel();
|
const filter = new ListFilterModel(GQL.FilterMode.Galleries);
|
||||||
const criterion = new StudiosCriterion();
|
const criterion = new StudiosCriterion();
|
||||||
criterion.value = {
|
criterion.value = {
|
||||||
items: [{ id: studio.id, label: studio.name || `Studio ${studio.id}` }],
|
items: [{ id: studio.id, label: studio.name || `Studio ${studio.id}` }],
|
||||||
@@ -119,7 +119,7 @@ const makeStudioGalleriesUrl = (studio: Partial<GQL.StudioDataFragment>) => {
|
|||||||
|
|
||||||
const makeChildStudiosUrl = (studio: Partial<GQL.StudioDataFragment>) => {
|
const makeChildStudiosUrl = (studio: Partial<GQL.StudioDataFragment>) => {
|
||||||
if (!studio.id) return "#";
|
if (!studio.id) return "#";
|
||||||
const filter = new ListFilterModel();
|
const filter = new ListFilterModel(GQL.FilterMode.Studios);
|
||||||
const criterion = new ParentStudiosCriterion();
|
const criterion = new ParentStudiosCriterion();
|
||||||
criterion.value = [
|
criterion.value = [
|
||||||
{ id: studio.id, label: studio.name || `Studio ${studio.id}` },
|
{ id: studio.id, label: studio.name || `Studio ${studio.id}` },
|
||||||
@@ -130,7 +130,7 @@ const makeChildStudiosUrl = (studio: Partial<GQL.StudioDataFragment>) => {
|
|||||||
|
|
||||||
const makeMovieScenesUrl = (movie: Partial<GQL.MovieDataFragment>) => {
|
const makeMovieScenesUrl = (movie: Partial<GQL.MovieDataFragment>) => {
|
||||||
if (!movie.id) return "#";
|
if (!movie.id) return "#";
|
||||||
const filter = new ListFilterModel();
|
const filter = new ListFilterModel(GQL.FilterMode.Scenes);
|
||||||
const criterion = new MoviesCriterion();
|
const criterion = new MoviesCriterion();
|
||||||
criterion.value = [
|
criterion.value = [
|
||||||
{ id: movie.id, label: movie.name || `Movie ${movie.id}` },
|
{ id: movie.id, label: movie.name || `Movie ${movie.id}` },
|
||||||
@@ -141,7 +141,7 @@ const makeMovieScenesUrl = (movie: Partial<GQL.MovieDataFragment>) => {
|
|||||||
|
|
||||||
const makeTagScenesUrl = (tag: Partial<GQL.TagDataFragment>) => {
|
const makeTagScenesUrl = (tag: Partial<GQL.TagDataFragment>) => {
|
||||||
if (!tag.id) return "#";
|
if (!tag.id) return "#";
|
||||||
const filter = new ListFilterModel();
|
const filter = new ListFilterModel(GQL.FilterMode.Scenes);
|
||||||
const criterion = new TagsCriterion(TagsCriterionOption);
|
const criterion = new TagsCriterion(TagsCriterionOption);
|
||||||
criterion.value = [{ id: tag.id, label: tag.name || `Tag ${tag.id}` }];
|
criterion.value = [{ id: tag.id, label: tag.name || `Tag ${tag.id}` }];
|
||||||
filter.criteria.push(criterion);
|
filter.criteria.push(criterion);
|
||||||
@@ -150,7 +150,7 @@ const makeTagScenesUrl = (tag: Partial<GQL.TagDataFragment>) => {
|
|||||||
|
|
||||||
const makeTagPerformersUrl = (tag: Partial<GQL.TagDataFragment>) => {
|
const makeTagPerformersUrl = (tag: Partial<GQL.TagDataFragment>) => {
|
||||||
if (!tag.id) return "#";
|
if (!tag.id) return "#";
|
||||||
const filter = new ListFilterModel();
|
const filter = new ListFilterModel(GQL.FilterMode.Performers);
|
||||||
const criterion = new TagsCriterion(TagsCriterionOption);
|
const criterion = new TagsCriterion(TagsCriterionOption);
|
||||||
criterion.value = [{ id: tag.id, label: tag.name || `Tag ${tag.id}` }];
|
criterion.value = [{ id: tag.id, label: tag.name || `Tag ${tag.id}` }];
|
||||||
filter.criteria.push(criterion);
|
filter.criteria.push(criterion);
|
||||||
@@ -159,7 +159,7 @@ const makeTagPerformersUrl = (tag: Partial<GQL.TagDataFragment>) => {
|
|||||||
|
|
||||||
const makeTagSceneMarkersUrl = (tag: Partial<GQL.TagDataFragment>) => {
|
const makeTagSceneMarkersUrl = (tag: Partial<GQL.TagDataFragment>) => {
|
||||||
if (!tag.id) return "#";
|
if (!tag.id) return "#";
|
||||||
const filter = new ListFilterModel();
|
const filter = new ListFilterModel(GQL.FilterMode.SceneMarkers);
|
||||||
const criterion = new TagsCriterion(TagsCriterionOption);
|
const criterion = new TagsCriterion(TagsCriterionOption);
|
||||||
criterion.value = [{ id: tag.id, label: tag.name || `Tag ${tag.id}` }];
|
criterion.value = [{ id: tag.id, label: tag.name || `Tag ${tag.id}` }];
|
||||||
filter.criteria.push(criterion);
|
filter.criteria.push(criterion);
|
||||||
@@ -168,7 +168,7 @@ const makeTagSceneMarkersUrl = (tag: Partial<GQL.TagDataFragment>) => {
|
|||||||
|
|
||||||
const makeTagGalleriesUrl = (tag: Partial<GQL.TagDataFragment>) => {
|
const makeTagGalleriesUrl = (tag: Partial<GQL.TagDataFragment>) => {
|
||||||
if (!tag.id) return "#";
|
if (!tag.id) return "#";
|
||||||
const filter = new ListFilterModel();
|
const filter = new ListFilterModel(GQL.FilterMode.Galleries);
|
||||||
const criterion = new TagsCriterion(TagsCriterionOption);
|
const criterion = new TagsCriterion(TagsCriterionOption);
|
||||||
criterion.value = [{ id: tag.id, label: tag.name || `Tag ${tag.id}` }];
|
criterion.value = [{ id: tag.id, label: tag.name || `Tag ${tag.id}` }];
|
||||||
filter.criteria.push(criterion);
|
filter.criteria.push(criterion);
|
||||||
@@ -177,7 +177,7 @@ const makeTagGalleriesUrl = (tag: Partial<GQL.TagDataFragment>) => {
|
|||||||
|
|
||||||
const makeTagImagesUrl = (tag: Partial<GQL.TagDataFragment>) => {
|
const makeTagImagesUrl = (tag: Partial<GQL.TagDataFragment>) => {
|
||||||
if (!tag.id) return "#";
|
if (!tag.id) return "#";
|
||||||
const filter = new ListFilterModel();
|
const filter = new ListFilterModel(GQL.FilterMode.Images);
|
||||||
const criterion = new TagsCriterion(TagsCriterionOption);
|
const criterion = new TagsCriterion(TagsCriterionOption);
|
||||||
criterion.value = [{ id: tag.id, label: tag.name || `Tag ${tag.id}` }];
|
criterion.value = [{ id: tag.id, label: tag.name || `Tag ${tag.id}` }];
|
||||||
filter.criteria.push(criterion);
|
filter.criteria.push(criterion);
|
||||||
|
|||||||
Reference in New Issue
Block a user