mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 12:24:38 +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
|
||||
ScrapedMovieStudio:
|
||||
model: github.com/stashapp/stash/pkg/models.ScrapedMovieStudio
|
||||
SavedFilter:
|
||||
model: github.com/stashapp/stash/pkg/models.SavedFilter
|
||||
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"""
|
||||
type Query {
|
||||
# Filters
|
||||
findSavedFilters(mode: FilterMode!): [SavedFilter!]!
|
||||
findDefaultFilter(mode: FilterMode!): SavedFilter
|
||||
|
||||
"""Find a scene by ID or Checksum"""
|
||||
findScene(id: ID, checksum: String): Scene
|
||||
findSceneByHash(input: SceneHashInput!): Scene
|
||||
@@ -199,6 +203,11 @@ type Mutation {
|
||||
tagsDestroy(ids: [ID!]!): Boolean!
|
||||
tagsMerge(input: TagsMergeInput!): Tag
|
||||
|
||||
# Saved filters
|
||||
saveFilter(input: SaveFilterInput!): SavedFilter!
|
||||
destroySavedFilter(input: DestroyFilterInput!): Boolean!
|
||||
setDefaultFilter(input: SetDefaultFilterInput!): Boolean!
|
||||
|
||||
"""Change general configuration options"""
|
||||
configureGeneral(input: ConfigGeneralInput!): ConfigGeneralResult!
|
||||
configureInterface(input: ConfigInterfaceInput!): ConfigInterfaceResult!
|
||||
|
||||
@@ -317,3 +317,41 @@ input HierarchicalMultiCriterionInput {
|
||||
modifier: CriterionModifier!
|
||||
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 WriteMu *sync.Mutex
|
||||
var dbPath string
|
||||
var appSchemaVersion uint = 24
|
||||
var appSchemaVersion uint = 25
|
||||
var databaseSchemaVersion uint
|
||||
|
||||
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
|
||||
studio models.StudioReaderWriter
|
||||
tag models.TagReaderWriter
|
||||
savedFilter models.SavedFilterReaderWriter
|
||||
}
|
||||
|
||||
func NewTransactionManager() *TransactionManager {
|
||||
@@ -29,6 +30,7 @@ func NewTransactionManager() *TransactionManager {
|
||||
scrapedItem: &ScrapedItemReaderWriter{},
|
||||
studio: &StudioReaderWriter{},
|
||||
tag: &TagReaderWriter{},
|
||||
savedFilter: &SavedFilterReaderWriter{},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,6 +74,10 @@ func (t *TransactionManager) Tag() models.TagReaderWriter {
|
||||
return t.tag
|
||||
}
|
||||
|
||||
func (t *TransactionManager) SavedFilter() models.SavedFilterReaderWriter {
|
||||
return t.savedFilter
|
||||
}
|
||||
|
||||
type ReadTransaction struct {
|
||||
t *TransactionManager
|
||||
}
|
||||
@@ -115,3 +121,7 @@ func (r *ReadTransaction) Studio() models.StudioReader {
|
||||
func (r *ReadTransaction) Tag() models.TagReader {
|
||||
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
|
||||
Studio() StudioReaderWriter
|
||||
Tag() TagReaderWriter
|
||||
SavedFilter() SavedFilterReaderWriter
|
||||
}
|
||||
|
||||
type ReaderRepository interface {
|
||||
@@ -22,4 +23,5 @@ type ReaderRepository interface {
|
||||
ScrapedItem() ScrapedItemReader
|
||||
Studio() StudioReader
|
||||
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
|
||||
@@ -188,12 +188,23 @@ const (
|
||||
markerIdxWithScene = iota
|
||||
)
|
||||
|
||||
const (
|
||||
savedFilterIdxDefaultScene = iota
|
||||
savedFilterIdxDefaultImage
|
||||
savedFilterIdxScene
|
||||
savedFilterIdxImage
|
||||
|
||||
// new indexes above
|
||||
totalSavedFilters
|
||||
)
|
||||
|
||||
const (
|
||||
pathField = "Path"
|
||||
checksumField = "Checksum"
|
||||
titleField = "Title"
|
||||
urlField = "URL"
|
||||
zipPath = "zipPath.zip"
|
||||
firstSavedFilterName = "firstSavedFilterName"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -205,6 +216,7 @@ var (
|
||||
tagIDs []int
|
||||
studioIDs []int
|
||||
markerIDs []int
|
||||
savedFilterIDs []int
|
||||
|
||||
tagNames []string
|
||||
studioNames []string
|
||||
@@ -423,6 +435,10 @@ func populateDB() 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 {
|
||||
return fmt.Errorf("error linking performer tags: %s", err.Error())
|
||||
}
|
||||
@@ -979,6 +995,51 @@ func createMarker(mqb models.SceneMarkerReaderWriter, sceneIdx, primaryTagIdx in
|
||||
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 {
|
||||
for _, l := range links {
|
||||
if err := fn(l[0], l[1]); err != nil {
|
||||
|
||||
@@ -125,6 +125,11 @@ func (t *transaction) Tag() models.TagReaderWriter {
|
||||
return NewTagReaderWriter(t.tx)
|
||||
}
|
||||
|
||||
func (t *transaction) SavedFilter() models.SavedFilterReaderWriter {
|
||||
t.ensureTx()
|
||||
return NewSavedFilterReaderWriter(t.tx)
|
||||
}
|
||||
|
||||
type ReadTransaction struct{}
|
||||
|
||||
func (t *ReadTransaction) Begin() error {
|
||||
@@ -183,6 +188,10 @@ func (t *ReadTransaction) Tag() models.TagReader {
|
||||
return NewTagReaderWriter(database.DB)
|
||||
}
|
||||
|
||||
func (t *ReadTransaction) SavedFilter() models.SavedFilterReader {
|
||||
return NewSavedFilterReaderWriter(database.DB)
|
||||
}
|
||||
|
||||
type TransactionManager struct {
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
### ✨ 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 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))
|
||||
|
||||
@@ -24,7 +24,7 @@ export const GalleryAddPanel: React.FC<IGalleryAddProps> = ({ gallery }) => {
|
||||
};
|
||||
// if galleries is already present, then we modify it, otherwise add
|
||||
let galleryCriterion = filter.criteria.find((c) => {
|
||||
return c.criterionOption.value === "galleries";
|
||||
return c.criterionOption.type === "galleries";
|
||||
}) as GalleriesCriterion;
|
||||
|
||||
if (
|
||||
|
||||
@@ -26,7 +26,7 @@ export const GalleryImagesPanel: React.FC<IGalleryDetailsProps> = ({
|
||||
};
|
||||
// if galleries is already present, then we modify it, otherwise add
|
||||
let galleryCriterion = filter.criteria.find((c) => {
|
||||
return c.criterionOption.value === "galleries";
|
||||
return c.criterionOption.type === "galleries";
|
||||
}) as GalleriesCriterion;
|
||||
|
||||
if (
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import _ from "lodash";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { Button, Form, Modal, OverlayTrigger, Tooltip } from "react-bootstrap";
|
||||
import Mousetrap from "mousetrap";
|
||||
import { Icon, FilterSelect, DurationInput } from "src/components/Shared";
|
||||
import { Button, Form, Modal } from "react-bootstrap";
|
||||
import { FilterSelect, DurationInput } from "src/components/Shared";
|
||||
import { CriterionModifier } from "src/core/generated-graphql";
|
||||
import {
|
||||
DurationCriterion,
|
||||
@@ -10,7 +9,10 @@ import {
|
||||
Criterion,
|
||||
IHierarchicalLabeledIdCriterion,
|
||||
} 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 { ListFilterOptions } from "src/models/list-filter/filter-options";
|
||||
import { defineMessages, FormattedMessage, useIntl } from "react-intl";
|
||||
@@ -29,15 +31,18 @@ interface IAddFilterProps {
|
||||
editingCriterion?: Criterion<CriterionValue>;
|
||||
}
|
||||
|
||||
export const AddFilter: React.FC<IAddFilterProps> = (
|
||||
props: IAddFilterProps
|
||||
) => {
|
||||
export const AddFilterDialog: React.FC<IAddFilterProps> = ({
|
||||
onAddCriterion,
|
||||
onCancel,
|
||||
filterOptions,
|
||||
editingCriterion,
|
||||
}) => {
|
||||
const defaultValue = useRef<string | number | undefined>();
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [criterion, setCriterion] = useState<Criterion<CriterionValue>>(
|
||||
new NoneCriterion()
|
||||
);
|
||||
const { options, modifierOptions } = criterion.criterionOption;
|
||||
|
||||
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
|
||||
useEffect(() => {
|
||||
if (!props.editingCriterion) {
|
||||
return;
|
||||
if (!editingCriterion) {
|
||||
setCriterion(makeCriteria());
|
||||
} else {
|
||||
setCriterion(editingCriterion);
|
||||
}
|
||||
setIsOpen(true);
|
||||
setCriterion(props.editingCriterion);
|
||||
}, [props.editingCriterion]);
|
||||
}, [editingCriterion]);
|
||||
|
||||
function onChangedCriteriaType(event: React.ChangeEvent<HTMLSelectElement>) {
|
||||
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) {
|
||||
const value = defaultValue.current;
|
||||
if (
|
||||
criterion.options &&
|
||||
options &&
|
||||
(value === undefined || value === "" || typeof value === "number")
|
||||
) {
|
||||
criterion.value = criterion.options[0].toString();
|
||||
criterion.value = options[0].toString();
|
||||
} else if (typeof value === "number" && value === undefined) {
|
||||
criterion.value = 0;
|
||||
} else if (value === undefined) {
|
||||
criterion.value = "";
|
||||
}
|
||||
}
|
||||
const oldId = props.editingCriterion
|
||||
? props.editingCriterion.getId()
|
||||
: undefined;
|
||||
props.onAddCriterion(criterion, oldId);
|
||||
onToggle();
|
||||
}
|
||||
|
||||
function onToggle() {
|
||||
if (isOpen) {
|
||||
props.onCancel();
|
||||
}
|
||||
setIsOpen(!isOpen);
|
||||
setCriterion(makeCriteria());
|
||||
const oldId = editingCriterion ? editingCriterion.getId() : undefined;
|
||||
onAddCriterion(criterion, oldId);
|
||||
}
|
||||
|
||||
const maybeRenderFilterPopoverContents = () => {
|
||||
if (criterion.criterionOption.value === "none") {
|
||||
if (criterion.criterionOption.type === "none") {
|
||||
return;
|
||||
}
|
||||
|
||||
function renderModifier() {
|
||||
if (criterion.modifierOptions.length === 0) {
|
||||
if (modifierOptions.length === 0) {
|
||||
return;
|
||||
}
|
||||
return (
|
||||
@@ -148,9 +133,9 @@ export const AddFilter: React.FC<IAddFilterProps> = (
|
||||
value={criterion.modifier}
|
||||
className="btn-secondary"
|
||||
>
|
||||
{criterion.modifierOptions.map((c) => (
|
||||
{modifierOptions.map((c) => (
|
||||
<option key={c.value} value={c.value}>
|
||||
{c.label}
|
||||
{c.label ? intl.formatMessage({ id: c.label }) : ""}
|
||||
</option>
|
||||
))}
|
||||
</Form.Control>
|
||||
@@ -168,19 +153,19 @@ export const AddFilter: React.FC<IAddFilterProps> = (
|
||||
|
||||
if (Array.isArray(criterion.value)) {
|
||||
if (
|
||||
criterion.criterionOption.value !== "performers" &&
|
||||
criterion.criterionOption.value !== "studios" &&
|
||||
criterion.criterionOption.value !== "parent_studios" &&
|
||||
criterion.criterionOption.value !== "tags" &&
|
||||
criterion.criterionOption.value !== "sceneTags" &&
|
||||
criterion.criterionOption.value !== "performerTags" &&
|
||||
criterion.criterionOption.value !== "movies"
|
||||
criterion.criterionOption.type !== "performers" &&
|
||||
criterion.criterionOption.type !== "studios" &&
|
||||
criterion.criterionOption.type !== "parent_studios" &&
|
||||
criterion.criterionOption.type !== "tags" &&
|
||||
criterion.criterionOption.type !== "sceneTags" &&
|
||||
criterion.criterionOption.type !== "performerTags" &&
|
||||
criterion.criterionOption.type !== "movies"
|
||||
)
|
||||
return;
|
||||
|
||||
return (
|
||||
<FilterSelect
|
||||
type={criterion.criterionOption.value}
|
||||
type={criterion.criterionOption.type}
|
||||
isMulti
|
||||
onSelect={(items) => {
|
||||
const newCriterion = _.cloneDeep(criterion);
|
||||
@@ -195,11 +180,11 @@ export const AddFilter: React.FC<IAddFilterProps> = (
|
||||
);
|
||||
}
|
||||
if (criterion instanceof IHierarchicalLabeledIdCriterion) {
|
||||
if (criterion.criterionOption.value !== "studios") return;
|
||||
if (criterion.criterionOption.type !== "studios") return;
|
||||
|
||||
return (
|
||||
<FilterSelect
|
||||
type={criterion.criterionOption.value}
|
||||
type={criterion.criterionOption.type}
|
||||
isMulti
|
||||
onSelect={(items) => {
|
||||
const newCriterion = _.cloneDeep(criterion);
|
||||
@@ -213,10 +198,7 @@ export const AddFilter: React.FC<IAddFilterProps> = (
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (
|
||||
criterion.options &&
|
||||
!criterionIsHierarchicalLabelValue(criterion.value)
|
||||
) {
|
||||
if (options && !criterionIsHierarchicalLabelValue(criterion.value)) {
|
||||
defaultValue.current = criterion.value;
|
||||
return (
|
||||
<Form.Control
|
||||
@@ -225,7 +207,7 @@ export const AddFilter: React.FC<IAddFilterProps> = (
|
||||
value={criterion.value.toString()}
|
||||
className="btn-secondary"
|
||||
>
|
||||
{criterion.options.map((c) => (
|
||||
{options.map((c) => (
|
||||
<option key={c.toString()} value={c.toString()}>
|
||||
{c}
|
||||
</option>
|
||||
@@ -245,7 +227,7 @@ export const AddFilter: React.FC<IAddFilterProps> = (
|
||||
return (
|
||||
<Form.Control
|
||||
className="btn-secondary"
|
||||
type={criterion.inputType}
|
||||
type={criterion.criterionOption.inputType}
|
||||
onChange={onChangedInput}
|
||||
onBlur={onBlurInput}
|
||||
defaultValue={criterion.value ? criterion.value.toString() : ""}
|
||||
@@ -259,7 +241,7 @@ export const AddFilter: React.FC<IAddFilterProps> = (
|
||||
<Form.Group>
|
||||
<Form.Check
|
||||
checked={criterion.value.depth !== 0}
|
||||
label="Include child studios"
|
||||
label={intl.formatMessage({ id: "include_child_studios" })}
|
||||
onChange={() => {
|
||||
const newCriterion = _.cloneDeep(criterion);
|
||||
newCriterion.value.depth =
|
||||
@@ -304,7 +286,7 @@ export const AddFilter: React.FC<IAddFilterProps> = (
|
||||
};
|
||||
|
||||
function maybeRenderFilterCriterion() {
|
||||
if (!props.editingCriterion) {
|
||||
if (!editingCriterion) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -312,7 +294,7 @@ export const AddFilter: React.FC<IAddFilterProps> = (
|
||||
<Form.Group>
|
||||
<strong>
|
||||
{intl.formatMessage({
|
||||
id: props.editingCriterion.criterionOption.messageID,
|
||||
id: editingCriterion.criterionOption.messageID,
|
||||
})}
|
||||
</strong>
|
||||
</Form.Group>
|
||||
@@ -320,14 +302,15 @@ export const AddFilter: React.FC<IAddFilterProps> = (
|
||||
}
|
||||
|
||||
function maybeRenderFilterSelect() {
|
||||
if (props.editingCriterion) {
|
||||
if (editingCriterion) {
|
||||
return;
|
||||
}
|
||||
|
||||
const options = props.filterOptions.criterionOptions
|
||||
const thisOptions = [NoneCriterionOption]
|
||||
.concat(filterOptions.criterionOptions)
|
||||
.map((c) => {
|
||||
return {
|
||||
value: c.value,
|
||||
value: c.type,
|
||||
text: intl.formatMessage({ id: c.messageID }),
|
||||
};
|
||||
})
|
||||
@@ -345,10 +328,10 @@ export const AddFilter: React.FC<IAddFilterProps> = (
|
||||
<Form.Control
|
||||
as="select"
|
||||
onChange={onChangedCriteriaType}
|
||||
value={criterion.criterionOption.value}
|
||||
value={criterion.criterionOption.type}
|
||||
className="btn-secondary"
|
||||
>
|
||||
{options.map((c) => (
|
||||
{thisOptions.map((c) => (
|
||||
<option key={c.value} value={c.value} disabled={c.value === "none"}>
|
||||
{c.text}
|
||||
</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.update_filter" });
|
||||
return (
|
||||
<>
|
||||
<OverlayTrigger
|
||||
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 show onHide={() => onCancel()}>
|
||||
<Modal.Header>{title}</Modal.Header>
|
||||
<Modal.Body>
|
||||
<div className="dialog-content">
|
||||
@@ -388,7 +358,7 @@ export const AddFilter: React.FC<IAddFilterProps> = (
|
||||
<Modal.Footer>
|
||||
<Button
|
||||
onClick={onAddFilter}
|
||||
disabled={criterion.criterionOption.value === "none"}
|
||||
disabled={criterion.criterionOption.type === "none"}
|
||||
>
|
||||
{title}
|
||||
</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 React, { useState, useEffect } from "react";
|
||||
import React, { HTMLAttributes, useEffect } from "react";
|
||||
import Mousetrap from "mousetrap";
|
||||
import { SortDirectionEnum } from "src/core/generated-graphql";
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
ButtonGroup,
|
||||
Dropdown,
|
||||
@@ -12,61 +11,44 @@ import {
|
||||
Tooltip,
|
||||
InputGroup,
|
||||
FormControl,
|
||||
ButtonToolbar,
|
||||
} from "react-bootstrap";
|
||||
|
||||
import { Icon } from "src/components/Shared";
|
||||
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||
import { DisplayMode } from "src/models/list-filter/types";
|
||||
import { useFocus } from "src/utils";
|
||||
import { ListFilterOptions } from "src/models/list-filter/filter-options";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import {
|
||||
Criterion,
|
||||
CriterionValue,
|
||||
} from "src/models/list-filter/criteria/criterion";
|
||||
import { AddFilter } from "./AddFilter";
|
||||
|
||||
interface IListFilterOperation {
|
||||
text: string;
|
||||
onClick: () => void;
|
||||
isDisplayed?: () => boolean;
|
||||
}
|
||||
import { PersistanceLevel } from "src/hooks/ListHook";
|
||||
import { SavedFilterList } from "./SavedFilterList";
|
||||
|
||||
interface IListFilterProps {
|
||||
onFilterUpdate: (newFilter: ListFilterModel) => void;
|
||||
zoomIndex?: number;
|
||||
onChangeZoom?: (zoomIndex: number) => void;
|
||||
onSelectAll?: () => void;
|
||||
onSelectNone?: () => void;
|
||||
onEdit?: () => void;
|
||||
onDelete?: () => void;
|
||||
otherOperations?: IListFilterOperation[];
|
||||
filter: ListFilterModel;
|
||||
filterOptions: ListFilterOptions;
|
||||
itemsSelected?: boolean;
|
||||
filterDialogOpen?: boolean;
|
||||
persistState?: PersistanceLevel;
|
||||
openFilterDialog: () => void;
|
||||
}
|
||||
|
||||
const PAGE_SIZE_OPTIONS = ["20", "40", "60", "120", "250", "500", "1000"];
|
||||
const minZoom = 0;
|
||||
const maxZoom = 3;
|
||||
|
||||
export const ListFilter: React.FC<IListFilterProps> = (
|
||||
props: IListFilterProps
|
||||
) => {
|
||||
export const ListFilter: React.FC<IListFilterProps> = ({
|
||||
onFilterUpdate,
|
||||
filter,
|
||||
filterOptions,
|
||||
filterDialogOpen,
|
||||
openFilterDialog,
|
||||
persistState,
|
||||
}) => {
|
||||
const [queryRef, setQueryFocus] = useFocus();
|
||||
|
||||
const searchCallback = debounce((value: string) => {
|
||||
const newFilter = _.cloneDeep(props.filter);
|
||||
const newFilter = _.cloneDeep(filter);
|
||||
newFilter.searchTerm = value;
|
||||
newFilter.currentPage = 1;
|
||||
props.onFilterUpdate(newFilter);
|
||||
onFilterUpdate(newFilter);
|
||||
}, 500);
|
||||
|
||||
const [editingCriterion, setEditingCriterion] = useState<
|
||||
Criterion<CriterionValue> | undefined
|
||||
>(undefined);
|
||||
|
||||
const intl = useIntl();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -76,81 +58,20 @@ export const ListFilter: React.FC<IListFilterProps> = (
|
||||
});
|
||||
|
||||
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 () => {
|
||||
Mousetrap.unbind("/");
|
||||
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>) {
|
||||
const val = event.currentTarget.value;
|
||||
|
||||
const newFilter = _.cloneDeep(props.filter);
|
||||
const newFilter = _.cloneDeep(filter);
|
||||
newFilter.itemsPerPage = parseInt(val, 10);
|
||||
newFilter.currentPage = 1;
|
||||
props.onFilterUpdate(newFilter);
|
||||
onFilterUpdate(newFilter);
|
||||
}
|
||||
|
||||
function onChangeQuery(event: React.FormEvent<HTMLInputElement>) {
|
||||
@@ -158,95 +79,32 @@ export const ListFilter: React.FC<IListFilterProps> = (
|
||||
}
|
||||
|
||||
function onChangeSortDirection() {
|
||||
const newFilter = _.cloneDeep(props.filter);
|
||||
if (props.filter.sortDirection === SortDirectionEnum.Asc) {
|
||||
const newFilter = _.cloneDeep(filter);
|
||||
if (filter.sortDirection === SortDirectionEnum.Asc) {
|
||||
newFilter.sortDirection = SortDirectionEnum.Desc;
|
||||
} else {
|
||||
newFilter.sortDirection = SortDirectionEnum.Asc;
|
||||
}
|
||||
|
||||
props.onFilterUpdate(newFilter);
|
||||
onFilterUpdate(newFilter);
|
||||
}
|
||||
|
||||
function onChangeSortBy(eventKey: string | null) {
|
||||
const newFilter = _.cloneDeep(props.filter);
|
||||
const newFilter = _.cloneDeep(filter);
|
||||
newFilter.sortBy = eventKey ?? undefined;
|
||||
newFilter.currentPage = 1;
|
||||
props.onFilterUpdate(newFilter);
|
||||
onFilterUpdate(newFilter);
|
||||
}
|
||||
|
||||
function onReshuffleRandomSort() {
|
||||
const newFilter = _.cloneDeep(props.filter);
|
||||
const newFilter = _.cloneDeep(filter);
|
||||
newFilter.currentPage = 1;
|
||||
newFilter.randomSeed = -1;
|
||||
props.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);
|
||||
onFilterUpdate(newFilter);
|
||||
}
|
||||
|
||||
function renderSortByOptions() {
|
||||
return props.filterOptions.sortByOptions
|
||||
return filterOptions.sortByOptions
|
||||
.map((o) => {
|
||||
return {
|
||||
message: intl.formatMessage({ id: o.messageID }),
|
||||
@@ -266,257 +124,81 @@ export const ListFilter: React.FC<IListFilterProps> = (
|
||||
));
|
||||
}
|
||||
|
||||
function renderDisplayModeOptions() {
|
||||
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}` });
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
const SavedFilterDropdown = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
HTMLAttributes<HTMLDivElement>
|
||||
>(({ style, className }, ref) => (
|
||||
<div ref={ref} style={style} className={className}>
|
||||
<SavedFilterList
|
||||
filter={filter}
|
||||
onSetFilter={(f) => {
|
||||
onFilterUpdate(f);
|
||||
}}
|
||||
persistState={persistState}
|
||||
/>
|
||||
</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() {
|
||||
const currentSortBy = props.filterOptions.sortByOptions.find(
|
||||
(o) => o.value === props.filter.sortBy
|
||||
const currentSortBy = filterOptions.sortByOptions.find(
|
||||
(o) => o.value === filter.sortBy
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ButtonToolbar className="align-items-center justify-content-center mb-2">
|
||||
<div className="d-flex">
|
||||
<div className="d-flex mb-1">
|
||||
<InputGroup className="mr-2 flex-grow-1">
|
||||
<InputGroup.Prepend>
|
||||
<Dropdown>
|
||||
<OverlayTrigger
|
||||
placement="top"
|
||||
overlay={
|
||||
<Tooltip id="filter-tooltip">
|
||||
<FormattedMessage id="search_filter.saved_filters" />
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
<Dropdown.Toggle variant="secondary">
|
||||
<Icon icon="bookmark" />
|
||||
</Dropdown.Toggle>
|
||||
</OverlayTrigger>
|
||||
<Dropdown.Menu
|
||||
as={SavedFilterDropdown}
|
||||
className="saved-filter-list-menu"
|
||||
/>
|
||||
</Dropdown>
|
||||
</InputGroup.Prepend>
|
||||
<FormControl
|
||||
ref={queryRef}
|
||||
placeholder={`${intl.formatMessage({ id: "actions.search" })}…`}
|
||||
defaultValue={props.filter.searchTerm}
|
||||
defaultValue={filter.searchTerm}
|
||||
onInput={onChangeQuery}
|
||||
className="bg-secondary text-white border-secondary w-50"
|
||||
className="query-text-field bg-secondary text-white border-secondary"
|
||||
/>
|
||||
|
||||
<InputGroup.Append>
|
||||
<AddFilter
|
||||
filterOptions={props.filterOptions}
|
||||
onAddCriterion={onAddCriterion}
|
||||
onCancel={onCancelAddCriterion}
|
||||
editingCriterion={editingCriterion}
|
||||
/>
|
||||
<OverlayTrigger
|
||||
placement="top"
|
||||
overlay={
|
||||
<Tooltip id="filter-tooltip">
|
||||
<FormattedMessage id="search_filter.name" />
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => openFilterDialog()}
|
||||
active={filterDialogOpen}
|
||||
>
|
||||
<Icon icon="filter" />
|
||||
</Button>
|
||||
</OverlayTrigger>
|
||||
</InputGroup.Append>
|
||||
</InputGroup>
|
||||
</div>
|
||||
|
||||
<Dropdown as={ButtonGroup} className="mr-2">
|
||||
<Dropdown.Toggle split variant="secondary" id="more-menu">
|
||||
<Dropdown as={ButtonGroup} className="mr-2 mb-1">
|
||||
<Dropdown.Toggle variant="secondary">
|
||||
{currentSortBy
|
||||
? intl.formatMessage({ id: currentSortBy.messageID })
|
||||
: ""}
|
||||
@@ -527,7 +209,7 @@ export const ListFilter: React.FC<IListFilterProps> = (
|
||||
<OverlayTrigger
|
||||
overlay={
|
||||
<Tooltip id="sort-direction-tooltip">
|
||||
{props.filter.sortDirection === SortDirectionEnum.Asc
|
||||
{filter.sortDirection === SortDirectionEnum.Asc
|
||||
? intl.formatMessage({ id: "ascending" })
|
||||
: intl.formatMessage({ id: "descending" })}
|
||||
</Tooltip>
|
||||
@@ -536,14 +218,14 @@ export const ListFilter: React.FC<IListFilterProps> = (
|
||||
<Button variant="secondary" onClick={onChangeSortDirection}>
|
||||
<Icon
|
||||
icon={
|
||||
props.filter.sortDirection === SortDirectionEnum.Asc
|
||||
filter.sortDirection === SortDirectionEnum.Asc
|
||||
? "caret-up"
|
||||
: "caret-down"
|
||||
}
|
||||
/>
|
||||
</Button>
|
||||
</OverlayTrigger>
|
||||
{props.filter.sortBy === "random" && (
|
||||
{filter.sortBy === "random" && (
|
||||
<OverlayTrigger
|
||||
overlay={
|
||||
<Tooltip id="sort-reshuffle-tooltip">
|
||||
@@ -557,13 +239,12 @@ export const ListFilter: React.FC<IListFilterProps> = (
|
||||
</OverlayTrigger>
|
||||
)}
|
||||
</Dropdown>
|
||||
</div>
|
||||
|
||||
<Form.Control
|
||||
as="select"
|
||||
onChange={onChangePageSize}
|
||||
value={props.filter.itemsPerPage.toString()}
|
||||
className="btn-secondary mx-1"
|
||||
value={filter.itemsPerPage.toString()}
|
||||
className="btn-secondary mx-1 mb-1"
|
||||
>
|
||||
{PAGE_SIZE_OPTIONS.map((s) => (
|
||||
<option value={s} key={s}>
|
||||
@@ -571,18 +252,6 @@ export const ListFilter: React.FC<IListFilterProps> = (
|
||||
</option>
|
||||
))}
|
||||
</Form.Control>
|
||||
|
||||
{maybeRenderSelectedButtons()}
|
||||
|
||||
<div className="mx-2">{renderMore()}</div>
|
||||
|
||||
<ButtonGroup>{renderDisplayModeOptions()}</ButtonGroup>
|
||||
{maybeRenderZoom()}
|
||||
</ButtonToolbar>
|
||||
|
||||
<div className="d-flex justify-content-center">
|
||||
{renderFilterTags()}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
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-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! };
|
||||
// if movie is already present, then we modify it, otherwise add
|
||||
let movieCriterion = filter.criteria.find((c) => {
|
||||
return c.criterionOption.value === "movies";
|
||||
return c.criterionOption.type === "movies";
|
||||
}) as MoviesCriterion;
|
||||
|
||||
if (
|
||||
|
||||
@@ -2,10 +2,10 @@ import React from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { TagLink } from "src/components/Shared";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { genderToString } from "src/core/StashService";
|
||||
import { TextUtils } from "src/utils";
|
||||
import { TextField, URLField } from "src/utils/field";
|
||||
import { RatingStars } from "src/components/Scenes/SceneDetails/RatingStars";
|
||||
import { genderToString } from "src/utils/gender";
|
||||
|
||||
interface IPerformerDetails {
|
||||
performer: Partial<GQL.PerformerDataFragment>;
|
||||
|
||||
@@ -14,10 +14,7 @@ import Mousetrap from "mousetrap";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import * as yup from "yup";
|
||||
import {
|
||||
getGenderStrings,
|
||||
useListPerformerScrapers,
|
||||
genderToString,
|
||||
stringToGender,
|
||||
queryScrapePerformer,
|
||||
mutateReloadScrapers,
|
||||
usePerformerUpdate,
|
||||
@@ -40,6 +37,11 @@ import { useToast } from "src/hooks";
|
||||
import { Prompt, useHistory } from "react-router-dom";
|
||||
import { useFormik } from "formik";
|
||||
import { RatingStars } from "src/components/Scenes/SceneDetails/RatingStars";
|
||||
import {
|
||||
genderStrings,
|
||||
genderToString,
|
||||
stringToGender,
|
||||
} from "src/utils/gender";
|
||||
import { PerformerScrapeDialog } from "./PerformerScrapeDialog";
|
||||
import PerformerScrapeModal from "./PerformerScrapeModal";
|
||||
import PerformerStashBoxModal, { IStashBox } from "./PerformerStashBoxModal";
|
||||
@@ -92,7 +94,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
||||
|
||||
const [createTag] = useTagCreate();
|
||||
|
||||
const genderOptions = [""].concat(getGenderStrings());
|
||||
const genderOptions = [""].concat(genderStrings);
|
||||
|
||||
const labelXS = 3;
|
||||
const labelXL = 2;
|
||||
|
||||
@@ -9,23 +9,23 @@ import {
|
||||
ScrapeDialogRow,
|
||||
ScrapedTextAreaRow,
|
||||
} from "src/components/Shared/ScrapeDialog";
|
||||
import {
|
||||
getGenderStrings,
|
||||
genderToString,
|
||||
stringToGender,
|
||||
useTagCreate,
|
||||
} from "src/core/StashService";
|
||||
import { useTagCreate } from "src/core/StashService";
|
||||
import { Form } from "react-bootstrap";
|
||||
import { TagSelect } from "src/components/Shared";
|
||||
import { useToast } from "src/hooks";
|
||||
import _ from "lodash";
|
||||
import {
|
||||
genderStrings,
|
||||
genderToString,
|
||||
stringToGender,
|
||||
} from "src/utils/gender";
|
||||
|
||||
function renderScrapedGender(
|
||||
result: ScrapeResult<string>,
|
||||
isNew?: boolean,
|
||||
onChange?: (value: string) => void
|
||||
) {
|
||||
const selectOptions = [""].concat(getGenderStrings());
|
||||
const selectOptions = [""].concat(genderStrings);
|
||||
|
||||
return (
|
||||
<Form.Control
|
||||
|
||||
@@ -233,7 +233,7 @@ export const Scene: React.FC = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const filterCopy = Object.assign(new ListFilterModel(), sceneQueue.query);
|
||||
const filterCopy = sceneQueue.query.clone();
|
||||
const newStart = queueStart - filterCopy.itemsPerPage;
|
||||
filterCopy.currentPage = Math.ceil(newStart / filterCopy.itemsPerPage);
|
||||
const query = await queryFindScenes(filterCopy);
|
||||
@@ -254,7 +254,7 @@ export const Scene: React.FC = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const filterCopy = Object.assign(new ListFilterModel(), sceneQueue.query);
|
||||
const filterCopy = sceneQueue.query.clone();
|
||||
const newStart = queueStart + queueScenes.length;
|
||||
filterCopy.currentPage = Math.ceil(newStart / filterCopy.itemsPerPage);
|
||||
const query = await queryFindScenes(filterCopy);
|
||||
@@ -291,7 +291,7 @@ export const Scene: React.FC = () => {
|
||||
const pages = Math.ceil(queueTotal / query.itemsPerPage);
|
||||
const page = Math.floor(Math.random() * pages) + 1;
|
||||
const index = Math.floor(Math.random() * query.itemsPerPage);
|
||||
const filterCopy = Object.assign(new ListFilterModel(), sceneQueue.query);
|
||||
const filterCopy = sceneQueue.query.clone();
|
||||
filterCopy.currentPage = page;
|
||||
const queryResults = await queryFindScenes(filterCopy);
|
||||
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! };
|
||||
// if studio is already present, then we modify it, otherwise add
|
||||
let parentStudioCriterion = filter.criteria.find((c) => {
|
||||
return c.criterionOption.value === "parent_studios";
|
||||
return c.criterionOption.type === "parent_studios";
|
||||
}) as ParentStudiosCriterion;
|
||||
|
||||
if (
|
||||
|
||||
@@ -11,8 +11,8 @@ import {
|
||||
TruncatedText,
|
||||
} from "src/components/Shared";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { genderToString } from "src/core/StashService";
|
||||
import { TextUtils } from "src/utils";
|
||||
import { genderToString } from "src/utils/gender";
|
||||
import { IStashBoxPerformer } from "./utils";
|
||||
|
||||
interface IPerformerModalProps {
|
||||
|
||||
@@ -16,7 +16,7 @@ export const TagMarkersPanel: React.FC<ITagMarkersPanel> = ({ tag }) => {
|
||||
const tagValue = { id: tag.id!, label: tag.name! };
|
||||
// if tag is already present, then we modify it, otherwise add
|
||||
let tagCriterion = filter.criteria.find((c) => {
|
||||
return c.criterionOption.value === "tags";
|
||||
return c.criterionOption.type === "tags";
|
||||
}) as TagsCriterion;
|
||||
|
||||
if (
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
getQueryDefinition,
|
||||
getOperationName,
|
||||
} from "@apollo/client/utilities";
|
||||
import { stringToGender } from "src/utils/gender";
|
||||
import { filterData } from "../utils";
|
||||
import { ListFilterModel } from "../models/list-filter/filter";
|
||||
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) =>
|
||||
GQL.useFindGalleriesQuery({
|
||||
variables: {
|
||||
@@ -680,6 +695,29 @@ export const useTagsDestroy = (input: GQL.TagsDestroyMutationVariables) =>
|
||||
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 = () =>
|
||||
GQL.useTagsMergeMutation({
|
||||
update: deleteCache(tagMutationImpactedQueries),
|
||||
@@ -973,54 +1011,6 @@ export const queryParseSceneFilenames = (
|
||||
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 = (
|
||||
toCreate: GQL.ScrapedScenePerformer
|
||||
) => {
|
||||
|
||||
@@ -9,7 +9,7 @@ export const performerFilterHook = (
|
||||
const performerValue = { id: performer.id!, label: performer.name! };
|
||||
// if performers is already present, then we modify it, otherwise add
|
||||
let performerCriterion = filter.criteria.find((c) => {
|
||||
return c.criterionOption.value === "performers";
|
||||
return c.criterionOption.type === "performers";
|
||||
}) as PerformersCriterion;
|
||||
|
||||
if (
|
||||
|
||||
@@ -7,7 +7,7 @@ export const studioFilterHook = (studio: Partial<GQL.StudioDataFragment>) => {
|
||||
const studioValue = { id: studio.id!, label: studio.name! };
|
||||
// if studio is already present, then we modify it, otherwise add
|
||||
let studioCriterion = filter.criteria.find((c) => {
|
||||
return c.criterionOption.value === "studios";
|
||||
return c.criterionOption.type === "studios";
|
||||
}) as StudiosCriterion;
|
||||
|
||||
if (
|
||||
|
||||
@@ -10,7 +10,7 @@ export const tagFilterHook = (tag: GQL.TagDataFragment) => {
|
||||
const tagValue = { id: tag.id, label: tag.name };
|
||||
// if tag is already present, then we modify it, otherwise add
|
||||
let tagCriterion = filter.criteria.find((c) => {
|
||||
return c.criterionOption.value === "tags";
|
||||
return c.criterionOption.type === "tags";
|
||||
}) as TagsCriterion;
|
||||
|
||||
if (
|
||||
|
||||
@@ -27,12 +27,15 @@ import {
|
||||
TagDataFragment,
|
||||
FindImagesQueryResult,
|
||||
SlimImageDataFragment,
|
||||
FilterMode,
|
||||
} from "src/core/generated-graphql";
|
||||
import { useInterfaceLocalForage } from "src/hooks/LocalForage";
|
||||
import { LoadingIndicator } from "src/components/Shared";
|
||||
import { ListFilter } from "src/components/List/ListFilter";
|
||||
import { FilterTags } from "src/components/List/FilterTags";
|
||||
import { Pagination, PaginationIndex } from "src/components/List/Pagination";
|
||||
import {
|
||||
useFindDefaultFilter,
|
||||
useFindScenes,
|
||||
useFindSceneMarkers,
|
||||
useFindImages,
|
||||
@@ -43,9 +46,17 @@ import {
|
||||
useFindTags,
|
||||
} from "src/core/StashService";
|
||||
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 { 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>(
|
||||
result: I[],
|
||||
@@ -88,8 +99,11 @@ export interface IListHookOperation<T> {
|
||||
}
|
||||
|
||||
export enum PersistanceLevel {
|
||||
// do not load default query or persist display mode
|
||||
NONE,
|
||||
// load default query, don't load or persist display mode
|
||||
ALL,
|
||||
// load and persist display mode only
|
||||
VIEW,
|
||||
}
|
||||
|
||||
@@ -98,6 +112,10 @@ interface IListHookOptions<T, E> {
|
||||
persistanceKey?: string;
|
||||
defaultSort?: string;
|
||||
filterHook?: (filter: ListFilterModel) => ListFilterModel;
|
||||
filterDialog?: (
|
||||
criteria: Criterion<CriterionValue>[],
|
||||
setCriteria: (v: Criterion<CriterionValue>[]) => void
|
||||
) => React.ReactNode;
|
||||
zoomable?: boolean;
|
||||
selectable?: boolean;
|
||||
defaultZoomIndex?: number;
|
||||
@@ -167,6 +185,8 @@ const RenderList = <
|
||||
renderEditDialog,
|
||||
renderDeleteDialog,
|
||||
updateQueryParams,
|
||||
filterDialog,
|
||||
persistState,
|
||||
}: IListHookOptions<QueryResult, QueryData> &
|
||||
IQuery<QueryResult, QueryData> &
|
||||
IRenderListProps) => {
|
||||
@@ -176,6 +196,11 @@ const RenderList = <
|
||||
const [lastClickedId, setLastClickedId] = useState<string | undefined>();
|
||||
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 totalCount = getCount(result);
|
||||
const items = getData(result);
|
||||
@@ -189,6 +214,7 @@ const RenderList = <
|
||||
}, [pages, filter.currentPage, onChangePage]);
|
||||
|
||||
useEffect(() => {
|
||||
Mousetrap.bind("f", () => setNewCriterion(true));
|
||||
Mousetrap.bind("right", () => {
|
||||
const maxPage = totalCount / filter.itemsPerPage;
|
||||
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 = (
|
||||
<div>
|
||||
<ButtonToolbar className="align-items-center justify-content-center mb-2">
|
||||
<ListFilter
|
||||
onFilterUpdate={updateQueryParams}
|
||||
filter={filter}
|
||||
filterOptions={filterOptions}
|
||||
openFilterDialog={() => setNewCriterion(true)}
|
||||
filterDialogOpen={newCriterion ?? editingCriterion}
|
||||
persistState={persistState}
|
||||
/>
|
||||
<ListOperationButtons
|
||||
onSelectAll={selectable ? onSelectAll : undefined}
|
||||
onSelectNone={selectable ? onSelectNone : undefined}
|
||||
zoomIndex={zoomable ? zoomIndex : undefined}
|
||||
onChangeZoom={zoomable ? onChangeZoom : undefined}
|
||||
otherOperations={operations}
|
||||
itemsSelected={selectedIds.size > 0}
|
||||
onEdit={renderEditDialog ? onEdit : undefined}
|
||||
onDelete={renderDeleteDialog ? onDelete : undefined}
|
||||
filter={filter}
|
||||
filterOptions={filterOptions}
|
||||
/>
|
||||
<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 &&
|
||||
renderEditDialog &&
|
||||
renderEditDialog(
|
||||
@@ -454,6 +563,7 @@ const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
|
||||
const defaultDisplayMode = filterOptions.displayModeOptions[0];
|
||||
const [filter, setFilter] = useState<ListFilterModel>(
|
||||
new ListFilterModel(
|
||||
options.filterMode,
|
||||
queryString.parse(location.search),
|
||||
defaultSort,
|
||||
defaultDisplayMode
|
||||
@@ -462,8 +572,8 @@ const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
|
||||
|
||||
const updateInterfaceConfig = useCallback(
|
||||
(updatedFilter: ListFilterModel, level: PersistanceLevel) => {
|
||||
setInterfaceState((prevState) => {
|
||||
if (level === PersistanceLevel.VIEW) {
|
||||
setInterfaceState((prevState) => {
|
||||
return {
|
||||
[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]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
interfaceState.loading ||
|
||||
// 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 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 {
|
||||
data: defaultFilter,
|
||||
loading: defaultFilterLoading,
|
||||
} = useFindDefaultFilter(options.filterMode);
|
||||
|
||||
const updateQueryParams = useCallback(
|
||||
(listFilter: ListFilterModel) => {
|
||||
@@ -565,6 +607,67 @@ const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
|
||||
[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(
|
||||
(page: number) => {
|
||||
const newFilter = _.cloneDeep(filter);
|
||||
|
||||
@@ -58,6 +58,7 @@
|
||||
"reshuffle": "Reshuffle",
|
||||
"running": "running",
|
||||
"save": "Save",
|
||||
"save_filter": "Save filter",
|
||||
"scan": "Scan",
|
||||
"scrape_with": "Scrape with…",
|
||||
"search": "Search",
|
||||
@@ -65,6 +66,7 @@
|
||||
"select_none": "Select None",
|
||||
"selective_auto_tag": "Selective Auto Tag",
|
||||
"selective_scan": "Selective Scan",
|
||||
"set_as_default": "Set as default",
|
||||
"set_back_image": "Back image…",
|
||||
"set_front_image": "Front image…",
|
||||
"set_image": "Set image…",
|
||||
@@ -400,6 +402,7 @@
|
||||
"destination": "Destination",
|
||||
"source": "Source"
|
||||
},
|
||||
"overwrite_filter_confirm": "Are you sure you want to overwrite existing saved query {entityName}?",
|
||||
"scene_gen": {
|
||||
"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)",
|
||||
@@ -477,6 +480,9 @@
|
||||
"file_info": "File Info",
|
||||
"file_mod_time": "File Modification Time",
|
||||
"filesize": "File Size",
|
||||
"filter": "Filter",
|
||||
"filter_name": "Filter name",
|
||||
"filters": "Filters",
|
||||
"framerate": "Frame Rate",
|
||||
"galleries": "Galleries",
|
||||
"gallery": "Gallery",
|
||||
@@ -490,6 +496,7 @@
|
||||
"image_count": "Image Count",
|
||||
"images": "Images",
|
||||
"images-size": "Images size",
|
||||
"include_child_studios": "Include child studios",
|
||||
"instagram": "Instagram",
|
||||
"interactive": "Interactive",
|
||||
"isMissing": "Is Missing",
|
||||
@@ -552,6 +559,7 @@
|
||||
"search_filter": {
|
||||
"add_filter": "Add Filter",
|
||||
"name": "Filter",
|
||||
"saved_filters": "Saved filters",
|
||||
"update_filter": "Update Filter"
|
||||
},
|
||||
"seconds": "Seconds",
|
||||
@@ -570,12 +578,15 @@
|
||||
"toast": {
|
||||
"added_entity": "Added {entity}",
|
||||
"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_past_tense": "Deleted {count, plural, one {{singularEntity}} other {{pluralEntity}}}",
|
||||
"generating_screenshot": "Generating screenshot…",
|
||||
"merged_tags": "Merged tags",
|
||||
"rescanning_entity": "Rescanning {count, plural, one {{singularEntity}} other {{pluralEntity}}}…",
|
||||
"started_auto_tagging": "Started auto tagging",
|
||||
"saved_entity": "Saved {entity}",
|
||||
"updated_entity": "Updated {entity}"
|
||||
},
|
||||
"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 {
|
||||
constructor() {
|
||||
|
||||
@@ -16,106 +16,58 @@ import {
|
||||
IHierarchicalLabelValue,
|
||||
} from "../types";
|
||||
|
||||
type Option = string | number | IOptionType;
|
||||
export type Option = string | number | IOptionType;
|
||||
export type CriterionValue =
|
||||
| string
|
||||
| number
|
||||
| ILabeledId[]
|
||||
| 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
|
||||
export abstract class Criterion<V extends CriterionValue> {
|
||||
public static getModifierOption(
|
||||
modifier: CriterionModifier = CriterionModifier.Equals
|
||||
): ILabeledValue {
|
||||
switch (modifier) {
|
||||
case CriterionModifier.Equals:
|
||||
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",
|
||||
};
|
||||
}
|
||||
const messageID = modifierMessageIDs[modifier];
|
||||
return { value: modifier, label: messageID };
|
||||
}
|
||||
|
||||
public criterionOption: CriterionOption;
|
||||
public abstract modifier: CriterionModifier;
|
||||
public abstract modifierOptions: ILabeledValue[];
|
||||
public abstract options: Option[] | undefined;
|
||||
public abstract value: V;
|
||||
public inputType: "number" | "text" | undefined;
|
||||
public modifier: CriterionModifier;
|
||||
public value: V;
|
||||
|
||||
public abstract getLabelValue(): string;
|
||||
|
||||
constructor(type: CriterionOption) {
|
||||
constructor(type: CriterionOption, value: V) {
|
||||
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 {
|
||||
let modifierMessageID: string;
|
||||
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 })
|
||||
: "";
|
||||
const modifierString = Criterion.getModifierLabel(intl, this.modifier);
|
||||
let valueString = "";
|
||||
|
||||
if (
|
||||
@@ -145,7 +97,7 @@ export abstract class Criterion<V extends CriterionValue> {
|
||||
|
||||
public toJSON() {
|
||||
const encodedCriterion = {
|
||||
type: this.criterionOption.value,
|
||||
type: this.criterionOption.type,
|
||||
// #394 - the presence of a # symbol results in the query URL being
|
||||
// malformed. We could set encode: true in the queryString.stringify
|
||||
// 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 {
|
||||
public readonly messageID: string;
|
||||
public readonly value: CriterionType;
|
||||
public readonly type: CriterionType;
|
||||
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) {
|
||||
this.messageID = messageID;
|
||||
this.value = value;
|
||||
this.parameterName = parameterName ?? value;
|
||||
constructor(options: ICriterionOptionsParams) {
|
||||
this.messageID = options.messageID;
|
||||
this.type = options.type;
|
||||
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) {
|
||||
return new CriterionOption(value, value);
|
||||
export class StringCriterionOption extends CriterionOption {
|
||||
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> {
|
||||
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() {
|
||||
return this.value;
|
||||
}
|
||||
@@ -218,74 +205,125 @@ export class StringCriterion extends Criterion<string> {
|
||||
return str.replaceAll(c, encodeURIComponent(c));
|
||||
}
|
||||
|
||||
constructor(type: CriterionOption, options?: string[]) {
|
||||
super(type);
|
||||
|
||||
this.options = options;
|
||||
this.inputType = "text";
|
||||
constructor(type: CriterionOption) {
|
||||
super(type, "");
|
||||
}
|
||||
}
|
||||
|
||||
export class MandatoryStringCriterion extends StringCriterion {
|
||||
public modifierOptions = [
|
||||
StringCriterion.getModifierOption(CriterionModifier.Equals),
|
||||
StringCriterion.getModifierOption(CriterionModifier.NotEquals),
|
||||
StringCriterion.getModifierOption(CriterionModifier.Includes),
|
||||
StringCriterion.getModifierOption(CriterionModifier.Excludes),
|
||||
StringCriterion.getModifierOption(CriterionModifier.MatchesRegex),
|
||||
StringCriterion.getModifierOption(CriterionModifier.NotMatchesRegex),
|
||||
];
|
||||
export class MandatoryStringCriterionOption extends CriterionOption {
|
||||
constructor(
|
||||
messageID: string,
|
||||
value: CriterionType,
|
||||
parameterName?: string,
|
||||
options?: Option[]
|
||||
) {
|
||||
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 {
|
||||
public modifier = CriterionModifier.Equals;
|
||||
public modifierOptions = [];
|
||||
|
||||
constructor(type: CriterionOption) {
|
||||
super(type, [true.toString(), false.toString()]);
|
||||
}
|
||||
|
||||
protected toCriterionInput(): boolean {
|
||||
return this.value === "true";
|
||||
}
|
||||
}
|
||||
|
||||
export class NumberCriterion extends Criterion<number> {
|
||||
public modifier = CriterionModifier.Equals;
|
||||
public modifierOptions = [
|
||||
Criterion.getModifierOption(CriterionModifier.Equals),
|
||||
Criterion.getModifierOption(CriterionModifier.NotEquals),
|
||||
Criterion.getModifierOption(CriterionModifier.GreaterThan),
|
||||
Criterion.getModifierOption(CriterionModifier.LessThan),
|
||||
Criterion.getModifierOption(CriterionModifier.IsNull),
|
||||
Criterion.getModifierOption(CriterionModifier.NotNull),
|
||||
];
|
||||
public options: number[] | undefined;
|
||||
public value: number = 0;
|
||||
export class NumberCriterionOption extends CriterionOption {
|
||||
constructor(
|
||||
messageID: string,
|
||||
value: CriterionType,
|
||||
parameterName?: string,
|
||||
options?: Option[]
|
||||
) {
|
||||
super({
|
||||
messageID,
|
||||
type: value,
|
||||
parameterName,
|
||||
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() {
|
||||
return this.value.toString();
|
||||
}
|
||||
|
||||
constructor(type: CriterionOption, options?: number[]) {
|
||||
super(type);
|
||||
|
||||
this.options = options;
|
||||
this.inputType = "number";
|
||||
constructor(type: CriterionOption) {
|
||||
super(type, 0);
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class ILabeledIdCriterion extends Criterion<ILabeledId[]> {
|
||||
public modifier = CriterionModifier.IncludesAll;
|
||||
public modifierOptions = [
|
||||
Criterion.getModifierOption(CriterionModifier.IncludesAll),
|
||||
Criterion.getModifierOption(CriterionModifier.Includes),
|
||||
Criterion.getModifierOption(CriterionModifier.Excludes),
|
||||
export class ILabeledIdCriterionOption extends CriterionOption {
|
||||
constructor(
|
||||
messageID: string,
|
||||
value: CriterionType,
|
||||
parameterName: string,
|
||||
includeAll: boolean
|
||||
) {
|
||||
const modifierOptions = [
|
||||
CriterionModifier.Includes,
|
||||
CriterionModifier.Excludes,
|
||||
];
|
||||
|
||||
public options: IOptionType[] = [];
|
||||
public value: ILabeledId[] = [];
|
||||
let defaultModifier = CriterionModifier.Includes;
|
||||
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 {
|
||||
return this.value.map((v) => v.label).join(", ");
|
||||
}
|
||||
@@ -303,33 +341,12 @@ export abstract class ILabeledIdCriterion extends Criterion<ILabeledId[]> {
|
||||
});
|
||||
}
|
||||
|
||||
constructor(type: CriterionOption, includeAll: boolean) {
|
||||
super(type);
|
||||
|
||||
if (!includeAll) {
|
||||
this.modifier = CriterionModifier.Includes;
|
||||
this.modifierOptions = [
|
||||
Criterion.getModifierOption(CriterionModifier.Includes),
|
||||
Criterion.getModifierOption(CriterionModifier.Excludes),
|
||||
];
|
||||
}
|
||||
constructor(type: CriterionOption) {
|
||||
super(type, []);
|
||||
}
|
||||
}
|
||||
|
||||
export abstract 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,
|
||||
};
|
||||
|
||||
export class IHierarchicalLabeledIdCriterion extends Criterion<IHierarchicalLabelValue> {
|
||||
public encodeValue() {
|
||||
return {
|
||||
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"})`;
|
||||
}
|
||||
|
||||
public toJSON() {
|
||||
const encodedCriterion = {
|
||||
type: this.criterionOption.value,
|
||||
value: this.encodeValue(),
|
||||
modifier: this.modifier,
|
||||
constructor(type: CriterionOption) {
|
||||
const value: IHierarchicalLabelValue = {
|
||||
items: [],
|
||||
depth: 0,
|
||||
};
|
||||
return JSON.stringify(encodedCriterion);
|
||||
}
|
||||
|
||||
constructor(type: CriterionOption, includeAll: boolean) {
|
||||
super(type);
|
||||
|
||||
if (!includeAll) {
|
||||
this.modifier = CriterionModifier.Includes;
|
||||
this.modifierOptions = [
|
||||
Criterion.getModifierOption(CriterionModifier.Includes),
|
||||
Criterion.getModifierOption(CriterionModifier.Excludes),
|
||||
];
|
||||
}
|
||||
super(type, value);
|
||||
}
|
||||
}
|
||||
|
||||
export class MandatoryNumberCriterion extends NumberCriterion {
|
||||
public modifierOptions = [
|
||||
Criterion.getModifierOption(CriterionModifier.Equals),
|
||||
Criterion.getModifierOption(CriterionModifier.NotEquals),
|
||||
Criterion.getModifierOption(CriterionModifier.GreaterThan),
|
||||
Criterion.getModifierOption(CriterionModifier.LessThan),
|
||||
];
|
||||
export class MandatoryNumberCriterionOption extends CriterionOption {
|
||||
constructor(messageID: string, value: CriterionType, parameterName?: string) {
|
||||
super({
|
||||
messageID,
|
||||
type: value,
|
||||
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> {
|
||||
public modifier = CriterionModifier.Equals;
|
||||
public modifierOptions = [
|
||||
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;
|
||||
constructor(type: CriterionOption) {
|
||||
super(type, 0);
|
||||
}
|
||||
|
||||
public getLabelValue() {
|
||||
|
||||
@@ -3,25 +3,27 @@ import {
|
||||
StringCriterion,
|
||||
NumberCriterion,
|
||||
DurationCriterion,
|
||||
MandatoryStringCriterion,
|
||||
MandatoryNumberCriterion,
|
||||
CriterionOption,
|
||||
NumberCriterionOption,
|
||||
MandatoryStringCriterionOption,
|
||||
MandatoryNumberCriterionOption,
|
||||
StringCriterionOption,
|
||||
ILabeledIdCriterion,
|
||||
} from "./criterion";
|
||||
import { OrganizedCriterion } from "./organized";
|
||||
import { FavoriteCriterion } from "./favorite";
|
||||
import { HasMarkersCriterion } from "./has-markers";
|
||||
import {
|
||||
PerformerIsMissingCriterion,
|
||||
SceneIsMissingCriterion,
|
||||
GalleryIsMissingCriterion,
|
||||
TagIsMissingCriterion,
|
||||
StudioIsMissingCriterion,
|
||||
MovieIsMissingCriterion,
|
||||
ImageIsMissingCriterion,
|
||||
PerformerIsMissingCriterionOption,
|
||||
ImageIsMissingCriterionOption,
|
||||
TagIsMissingCriterionOption,
|
||||
SceneIsMissingCriterionOption,
|
||||
IsMissingCriterion,
|
||||
GalleryIsMissingCriterionOption,
|
||||
StudioIsMissingCriterionOption,
|
||||
MovieIsMissingCriterionOption,
|
||||
} from "./is-missing";
|
||||
import { NoneCriterion } from "./none";
|
||||
import { PerformersCriterion } from "./performers";
|
||||
import { RatingCriterion } from "./rating";
|
||||
import { AverageResolutionCriterion, ResolutionCriterion } from "./resolution";
|
||||
import { StudiosCriterion, ParentStudiosCriterion } from "./studios";
|
||||
import {
|
||||
@@ -31,19 +33,22 @@ import {
|
||||
TagsCriterionOption,
|
||||
} from "./tags";
|
||||
import { GenderCriterion } from "./gender";
|
||||
import { MoviesCriterion } from "./movies";
|
||||
import { MoviesCriterionOption } from "./movies";
|
||||
import { GalleriesCriterion } from "./galleries";
|
||||
import { CriterionType } from "../types";
|
||||
import { InteractiveCriterion } from "./interactive";
|
||||
import { RatingCriterionOption } from "./rating";
|
||||
|
||||
export function makeCriteria(type: CriterionType = "none") {
|
||||
switch (type) {
|
||||
case "none":
|
||||
return new NoneCriterion();
|
||||
case "path":
|
||||
return new MandatoryStringCriterion(new CriterionOption(type, type));
|
||||
return new StringCriterion(
|
||||
new MandatoryStringCriterionOption(type, type)
|
||||
);
|
||||
case "rating":
|
||||
return new RatingCriterion();
|
||||
return new NumberCriterion(RatingCriterionOption);
|
||||
case "organized":
|
||||
return new OrganizedCriterion();
|
||||
case "o_counter":
|
||||
@@ -53,31 +58,33 @@ export function makeCriteria(type: CriterionType = "none") {
|
||||
case "gallery_count":
|
||||
case "performer_count":
|
||||
case "tag_count":
|
||||
return new MandatoryNumberCriterion(new CriterionOption(type, type));
|
||||
return new NumberCriterion(
|
||||
new MandatoryNumberCriterionOption(type, type)
|
||||
);
|
||||
case "resolution":
|
||||
return new ResolutionCriterion();
|
||||
case "average_resolution":
|
||||
return new AverageResolutionCriterion();
|
||||
case "duration":
|
||||
return new DurationCriterion(new CriterionOption(type, type));
|
||||
return new DurationCriterion(new NumberCriterionOption(type, type));
|
||||
case "favorite":
|
||||
return new FavoriteCriterion();
|
||||
case "hasMarkers":
|
||||
return new HasMarkersCriterion();
|
||||
case "sceneIsMissing":
|
||||
return new SceneIsMissingCriterion();
|
||||
return new IsMissingCriterion(SceneIsMissingCriterionOption);
|
||||
case "imageIsMissing":
|
||||
return new ImageIsMissingCriterion();
|
||||
return new IsMissingCriterion(ImageIsMissingCriterionOption);
|
||||
case "performerIsMissing":
|
||||
return new PerformerIsMissingCriterion();
|
||||
return new IsMissingCriterion(PerformerIsMissingCriterionOption);
|
||||
case "galleryIsMissing":
|
||||
return new GalleryIsMissingCriterion();
|
||||
return new IsMissingCriterion(GalleryIsMissingCriterionOption);
|
||||
case "tagIsMissing":
|
||||
return new TagIsMissingCriterion();
|
||||
return new IsMissingCriterion(TagIsMissingCriterionOption);
|
||||
case "studioIsMissing":
|
||||
return new StudioIsMissingCriterion();
|
||||
return new IsMissingCriterion(StudioIsMissingCriterionOption);
|
||||
case "movieIsMissing":
|
||||
return new MovieIsMissingCriterion();
|
||||
return new IsMissingCriterion(MovieIsMissingCriterionOption);
|
||||
case "tags":
|
||||
return new TagsCriterion(TagsCriterionOption);
|
||||
case "sceneTags":
|
||||
@@ -91,15 +98,17 @@ export function makeCriteria(type: CriterionType = "none") {
|
||||
case "parent_studios":
|
||||
return new ParentStudiosCriterion();
|
||||
case "movies":
|
||||
return new MoviesCriterion();
|
||||
return new ILabeledIdCriterion(MoviesCriterionOption);
|
||||
case "galleries":
|
||||
return new GalleriesCriterion();
|
||||
case "birth_year":
|
||||
case "death_year":
|
||||
case "weight":
|
||||
return new NumberCriterion(new CriterionOption(type, type));
|
||||
return new NumberCriterion(new NumberCriterionOption(type, type));
|
||||
case "age":
|
||||
return new MandatoryNumberCriterion(new CriterionOption(type, type));
|
||||
return new NumberCriterion(
|
||||
new MandatoryNumberCriterionOption(type, type)
|
||||
);
|
||||
case "gender":
|
||||
return new GenderCriterion();
|
||||
case "ethnicity":
|
||||
@@ -115,7 +124,7 @@ export function makeCriteria(type: CriterionType = "none") {
|
||||
case "aliases":
|
||||
case "url":
|
||||
case "stash_id":
|
||||
return new StringCriterion(new CriterionOption(type, type));
|
||||
return new StringCriterion(new StringCriterionOption(type, type));
|
||||
case "interactive":
|
||||
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",
|
||||
"favorite",
|
||||
"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 {
|
||||
constructor() {
|
||||
super(galleriesCriterionOption, true);
|
||||
super(galleriesCriterionOption);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
import {
|
||||
CriterionModifier,
|
||||
GenderCriterionInput,
|
||||
} from "src/core/generated-graphql";
|
||||
import { getGenderStrings, stringToGender } from "src/core/StashService";
|
||||
import { GenderCriterionInput } from "src/core/generated-graphql";
|
||||
import { genderStrings, stringToGender } from "src/utils/gender";
|
||||
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 {
|
||||
public modifier = CriterionModifier.Equals;
|
||||
public modifierOptions = [];
|
||||
|
||||
constructor() {
|
||||
super(GenderCriterionOption, getGenderStrings());
|
||||
super(GenderCriterionOption);
|
||||
}
|
||||
|
||||
protected toCriterionInput(): GenderCriterionInput {
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { CriterionOption, StringCriterion } from "./criterion";
|
||||
|
||||
export const HasMarkersCriterionOption = new CriterionOption(
|
||||
"hasMarkers",
|
||||
"hasMarkers",
|
||||
"has_markers"
|
||||
);
|
||||
export const HasMarkersCriterionOption = new CriterionOption({
|
||||
messageID: "hasMarkers",
|
||||
type: "hasMarkers",
|
||||
parameterName: "has_markers",
|
||||
options: [true.toString(), false.toString()],
|
||||
});
|
||||
|
||||
export class HasMarkersCriterion extends StringCriterion {
|
||||
constructor() {
|
||||
super(HasMarkersCriterionOption, [true.toString(), false.toString()]);
|
||||
super(HasMarkersCriterionOption);
|
||||
}
|
||||
|
||||
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"
|
||||
);
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
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 modifier = CriterionModifier.Equals;
|
||||
|
||||
@@ -10,15 +11,27 @@ 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",
|
||||
"sceneIsMissing",
|
||||
"is_missing"
|
||||
);
|
||||
|
||||
export class SceneIsMissingCriterion extends IsMissingCriterion {
|
||||
constructor() {
|
||||
super(SceneIsMissingCriterionOption, [
|
||||
"is_missing",
|
||||
[
|
||||
"title",
|
||||
"details",
|
||||
"url",
|
||||
@@ -29,37 +42,21 @@ export class SceneIsMissingCriterion extends IsMissingCriterion {
|
||||
"performers",
|
||||
"tags",
|
||||
"stash_id",
|
||||
]);
|
||||
}
|
||||
}
|
||||
]
|
||||
);
|
||||
|
||||
export const ImageIsMissingCriterionOption = new CriterionOption(
|
||||
export const ImageIsMissingCriterionOption = new IsMissingCriterionOptionClass(
|
||||
"isMissing",
|
||||
"imageIsMissing",
|
||||
"is_missing"
|
||||
"is_missing",
|
||||
["title", "galleries", "studio", "performers", "tags"]
|
||||
);
|
||||
|
||||
export class ImageIsMissingCriterion extends IsMissingCriterion {
|
||||
constructor() {
|
||||
super(ImageIsMissingCriterionOption, [
|
||||
"title",
|
||||
"galleries",
|
||||
"studio",
|
||||
"performers",
|
||||
"tags",
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
export const PerformerIsMissingCriterionOption = new CriterionOption(
|
||||
export const PerformerIsMissingCriterionOption = new IsMissingCriterionOptionClass(
|
||||
"isMissing",
|
||||
"performerIsMissing",
|
||||
"is_missing"
|
||||
);
|
||||
|
||||
export class PerformerIsMissingCriterion extends IsMissingCriterion {
|
||||
constructor() {
|
||||
super(PerformerIsMissingCriterionOption, [
|
||||
"is_missing",
|
||||
[
|
||||
"url",
|
||||
"twitter",
|
||||
"instagram",
|
||||
@@ -79,67 +76,33 @@ export class PerformerIsMissingCriterion extends IsMissingCriterion {
|
||||
"image",
|
||||
"details",
|
||||
"stash_id",
|
||||
]);
|
||||
}
|
||||
}
|
||||
]
|
||||
);
|
||||
|
||||
export const GalleryIsMissingCriterionOption = new CriterionOption(
|
||||
export const GalleryIsMissingCriterionOption = new IsMissingCriterionOptionClass(
|
||||
"isMissing",
|
||||
"galleryIsMissing",
|
||||
"is_missing"
|
||||
"is_missing",
|
||||
["title", "details", "url", "date", "studio", "performers", "tags", "scenes"]
|
||||
);
|
||||
|
||||
export class GalleryIsMissingCriterion extends IsMissingCriterion {
|
||||
constructor() {
|
||||
super(GalleryIsMissingCriterionOption, [
|
||||
"title",
|
||||
"details",
|
||||
"url",
|
||||
"date",
|
||||
"studio",
|
||||
"performers",
|
||||
"tags",
|
||||
"scenes",
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
export const TagIsMissingCriterionOption = new CriterionOption(
|
||||
export const TagIsMissingCriterionOption = new IsMissingCriterionOptionClass(
|
||||
"isMissing",
|
||||
"tagIsMissing",
|
||||
"is_missing"
|
||||
"is_missing",
|
||||
["image"]
|
||||
);
|
||||
|
||||
export class TagIsMissingCriterion extends IsMissingCriterion {
|
||||
constructor() {
|
||||
super(TagIsMissingCriterionOption, ["image"]);
|
||||
}
|
||||
}
|
||||
|
||||
export const StudioIsMissingCriterionOption = new CriterionOption(
|
||||
export const StudioIsMissingCriterionOption = new IsMissingCriterionOptionClass(
|
||||
"isMissing",
|
||||
"studioIsMissing",
|
||||
"is_missing"
|
||||
"is_missing",
|
||||
["image", "stash_id", "details"]
|
||||
);
|
||||
|
||||
export class StudioIsMissingCriterion extends IsMissingCriterion {
|
||||
constructor() {
|
||||
super(StudioIsMissingCriterionOption, ["image", "stash_id", "details"]);
|
||||
}
|
||||
}
|
||||
|
||||
export const MovieIsMissingCriterionOption = new CriterionOption(
|
||||
export const MovieIsMissingCriterionOption = new IsMissingCriterionOptionClass(
|
||||
"isMissing",
|
||||
"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 {
|
||||
constructor() {
|
||||
super(MoviesCriterionOption, false);
|
||||
super(MoviesCriterionOption);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import { CriterionModifier } from "src/core/generated-graphql";
|
||||
import { Criterion, CriterionOption } from "./criterion";
|
||||
import { Criterion, StringCriterionOption } from "./criterion";
|
||||
|
||||
export const NoneCriterionOption = new CriterionOption("none", "none");
|
||||
export const NoneCriterionOption = new StringCriterionOption(
|
||||
"none",
|
||||
"none",
|
||||
"none"
|
||||
);
|
||||
export class NoneCriterion extends Criterion<string> {
|
||||
public modifier = CriterionModifier.Equals;
|
||||
public modifierOptions = [];
|
||||
public options: undefined;
|
||||
public value: string = "none";
|
||||
|
||||
constructor() {
|
||||
super(NoneCriterionOption);
|
||||
super(NoneCriterionOption, "none");
|
||||
}
|
||||
|
||||
// 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"
|
||||
);
|
||||
|
||||
@@ -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",
|
||||
true
|
||||
);
|
||||
|
||||
export class PerformersCriterion extends ILabeledIdCriterion {
|
||||
constructor() {
|
||||
super(PerformersCriterionOption, true);
|
||||
super(PerformersCriterionOption);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,8 @@
|
||||
import { CriterionModifier } from "src/core/generated-graphql";
|
||||
import { Criterion, CriterionOption, NumberCriterion } from "./criterion";
|
||||
import { NumberCriterionOption } from "./criterion";
|
||||
|
||||
export const RatingCriterionOption = new CriterionOption("rating", "rating");
|
||||
|
||||
export class RatingCriterion extends NumberCriterion {
|
||||
public modifier = CriterionModifier.Equals;
|
||||
public modifierOptions = [
|
||||
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]);
|
||||
}
|
||||
}
|
||||
export const RatingCriterionOption = new NumberCriterionOption(
|
||||
"rating",
|
||||
"rating",
|
||||
"rating",
|
||||
[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";
|
||||
|
||||
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 {
|
||||
switch (this.value) {
|
||||
case "144p":
|
||||
@@ -55,21 +36,40 @@ abstract class AbstractResolutionCriterion extends StringCriterion {
|
||||
}
|
||||
}
|
||||
|
||||
export const ResolutionCriterionOption = new CriterionOption(
|
||||
"resolution",
|
||||
class ResolutionCriterionOptionType extends CriterionOption {
|
||||
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"
|
||||
);
|
||||
export class ResolutionCriterion extends AbstractResolutionCriterion {
|
||||
public modifier = CriterionModifier.Equals;
|
||||
public modifierOptions = [];
|
||||
|
||||
constructor() {
|
||||
super(ResolutionCriterionOption);
|
||||
}
|
||||
}
|
||||
|
||||
export const AverageResolutionCriterionOption = new CriterionOption(
|
||||
"average_resolution",
|
||||
export const AverageResolutionCriterionOption = new ResolutionCriterionOptionType(
|
||||
"average_resolution"
|
||||
);
|
||||
|
||||
|
||||
@@ -1,23 +1,30 @@
|
||||
import {
|
||||
CriterionOption,
|
||||
IHierarchicalLabeledIdCriterion,
|
||||
ILabeledIdCriterion,
|
||||
ILabeledIdCriterionOption,
|
||||
} from "./criterion";
|
||||
|
||||
export const StudiosCriterionOption = new CriterionOption("studios", "studios");
|
||||
export const StudiosCriterionOption = new ILabeledIdCriterionOption(
|
||||
"studios",
|
||||
"studios",
|
||||
"studios",
|
||||
false
|
||||
);
|
||||
|
||||
export class StudiosCriterion extends IHierarchicalLabeledIdCriterion {
|
||||
constructor() {
|
||||
super(StudiosCriterionOption, false);
|
||||
super(StudiosCriterionOption);
|
||||
}
|
||||
}
|
||||
|
||||
export const ParentStudiosCriterionOption = new CriterionOption(
|
||||
export const ParentStudiosCriterionOption = new ILabeledIdCriterionOption(
|
||||
"parent_studios",
|
||||
"parent_studios",
|
||||
"parents"
|
||||
"parents",
|
||||
false
|
||||
);
|
||||
export class ParentStudiosCriterion extends ILabeledIdCriterion {
|
||||
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 {
|
||||
constructor(type: CriterionOption) {
|
||||
super(type, true);
|
||||
}
|
||||
}
|
||||
export class TagsCriterion extends ILabeledIdCriterion {}
|
||||
|
||||
export const TagsCriterionOption = new CriterionOption("tags", "tags");
|
||||
export const SceneTagsCriterionOption = new CriterionOption(
|
||||
export const TagsCriterionOption = new ILabeledIdCriterionOption(
|
||||
"tags",
|
||||
"tags",
|
||||
"tags",
|
||||
true
|
||||
);
|
||||
export const SceneTagsCriterionOption = new ILabeledIdCriterionOption(
|
||||
"sceneTags",
|
||||
"sceneTags",
|
||||
"scene_tags"
|
||||
"scene_tags",
|
||||
true
|
||||
);
|
||||
export const PerformerTagsCriterionOption = new CriterionOption(
|
||||
export const PerformerTagsCriterionOption = new ILabeledIdCriterionOption(
|
||||
"performerTags",
|
||||
"performerTags",
|
||||
"performer_tags"
|
||||
"performer_tags",
|
||||
true
|
||||
);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { FilterMode } from "src/core/generated-graphql";
|
||||
import { ListFilterOptions } from "./filter-options";
|
||||
import { GalleryListFilterOptions } from "./galleries";
|
||||
import { ImageListFilterOptions } from "./images";
|
||||
@@ -7,7 +8,6 @@ import { SceneMarkerListFilterOptions } from "./scene-markers";
|
||||
import { SceneListFilterOptions } from "./scenes";
|
||||
import { StudioListFilterOptions } from "./studios";
|
||||
import { TagListFilterOptions } from "./tags";
|
||||
import { FilterMode } from "./types";
|
||||
|
||||
export function getFilterOptions(mode: FilterMode): ListFilterOptions {
|
||||
switch (mode) {
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
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 { makeCriteria } from "./criteria/factory";
|
||||
import { DisplayMode } from "./types";
|
||||
@@ -23,6 +27,7 @@ const DEFAULT_PARAMS = {
|
||||
|
||||
// TODO: handle customCriteria
|
||||
export class ListFilterModel {
|
||||
public mode: FilterMode;
|
||||
public searchTerm?: string;
|
||||
public currentPage = DEFAULT_PARAMS.currentPage;
|
||||
public itemsPerPage = DEFAULT_PARAMS.itemsPerPage;
|
||||
@@ -33,16 +38,22 @@ export class ListFilterModel {
|
||||
public randomSeed = -1;
|
||||
|
||||
public constructor(
|
||||
mode: FilterMode,
|
||||
rawParms?: ParsedQuery<string>,
|
||||
defaultSort?: string,
|
||||
defaultDisplayMode?: DisplayMode
|
||||
) {
|
||||
this.mode = mode;
|
||||
const params = rawParms as IQueryParameters;
|
||||
this.sortBy = defaultSort;
|
||||
if (defaultDisplayMode !== undefined) this.displayMode = defaultDisplayMode;
|
||||
if (params) this.configureFromQueryParameters(params);
|
||||
}
|
||||
|
||||
public clone() {
|
||||
return Object.assign(new ListFilterModel(this.mode), this);
|
||||
}
|
||||
|
||||
public configureFromQueryParameters(params: IQueryParameters) {
|
||||
if (params.sortby !== undefined) {
|
||||
this.sortBy = params.sortby;
|
||||
@@ -64,7 +75,7 @@ export class ListFilterModel {
|
||||
params.sortdir === "desc"
|
||||
? SortDirectionEnum.Desc
|
||||
: SortDirectionEnum.Asc;
|
||||
if (params.disp) {
|
||||
if (params.disp !== undefined) {
|
||||
this.displayMode = Number.parseInt(params.disp, 10);
|
||||
}
|
||||
if (params.q) {
|
||||
@@ -153,6 +164,24 @@ export class ListFilterModel {
|
||||
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 {
|
||||
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 { NoneCriterionOption } from "./criteria/none";
|
||||
import { OrganizedCriterionOption } from "./criteria/organized";
|
||||
import { PerformersCriterionOption } from "./criteria/performers";
|
||||
import { RatingCriterionOption } from "./criteria/rating";
|
||||
@@ -39,20 +38,19 @@ const displayModeOptions = [
|
||||
];
|
||||
|
||||
const criterionOptions = [
|
||||
NoneCriterionOption,
|
||||
createCriterionOption("path"),
|
||||
createStringCriterionOption("path"),
|
||||
RatingCriterionOption,
|
||||
OrganizedCriterionOption,
|
||||
AverageResolutionCriterionOption,
|
||||
GalleryIsMissingCriterionOption,
|
||||
TagsCriterionOption,
|
||||
createCriterionOption("tag_count"),
|
||||
createStringCriterionOption("tag_count"),
|
||||
PerformerTagsCriterionOption,
|
||||
PerformersCriterionOption,
|
||||
createCriterionOption("performer_count"),
|
||||
createCriterionOption("image_count"),
|
||||
createStringCriterionOption("performer_count"),
|
||||
createStringCriterionOption("image_count"),
|
||||
StudiosCriterionOption,
|
||||
createCriterionOption("url"),
|
||||
createStringCriterionOption("url"),
|
||||
];
|
||||
|
||||
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 { NoneCriterionOption } from "./criteria/none";
|
||||
import { OrganizedCriterionOption } from "./criteria/organized";
|
||||
import { PerformersCriterionOption } from "./criteria/performers";
|
||||
import { RatingCriterionOption } from "./criteria/rating";
|
||||
@@ -29,18 +31,17 @@ const sortByOptions = [
|
||||
|
||||
const displayModeOptions = [DisplayMode.Grid, DisplayMode.Wall];
|
||||
const criterionOptions = [
|
||||
NoneCriterionOption,
|
||||
createCriterionOption("path"),
|
||||
createStringCriterionOption("path"),
|
||||
RatingCriterionOption,
|
||||
OrganizedCriterionOption,
|
||||
createCriterionOption("o_counter"),
|
||||
createMandatoryNumberCriterionOption("o_counter"),
|
||||
ResolutionCriterionOption,
|
||||
ImageIsMissingCriterionOption,
|
||||
TagsCriterionOption,
|
||||
createCriterionOption("tag_count"),
|
||||
createMandatoryNumberCriterionOption("tag_count"),
|
||||
PerformerTagsCriterionOption,
|
||||
PerformersCriterionOption,
|
||||
createCriterionOption("performer_count"),
|
||||
createMandatoryNumberCriterionOption("performer_count"),
|
||||
StudiosCriterionOption,
|
||||
];
|
||||
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 { NoneCriterionOption } from "./criteria/none";
|
||||
import { StudiosCriterionOption } from "./criteria/studios";
|
||||
import { ListFilterOptions } from "./filter-options";
|
||||
import { DisplayMode } from "./types";
|
||||
@@ -17,10 +16,9 @@ const sortByOptions = ["name", "random"]
|
||||
]);
|
||||
const displayModeOptions = [DisplayMode.Grid];
|
||||
const criterionOptions = [
|
||||
NoneCriterionOption,
|
||||
StudiosCriterionOption,
|
||||
MovieIsMissingCriterionOption,
|
||||
createCriterionOption("url"),
|
||||
createStringCriterionOption("url"),
|
||||
];
|
||||
|
||||
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 { GenderCriterionOption } from "./criteria/gender";
|
||||
import { PerformerIsMissingCriterionOption } from "./criteria/is-missing";
|
||||
import { NoneCriterionOption } from "./criteria/none";
|
||||
import { RatingCriterionOption } from "./criteria/rating";
|
||||
import { StudiosCriterionOption } from "./criteria/studios";
|
||||
import { TagsCriterionOption } from "./criteria/tags";
|
||||
@@ -55,19 +58,19 @@ const stringCriteria: CriterionType[] = [
|
||||
];
|
||||
|
||||
const criterionOptions = [
|
||||
NoneCriterionOption,
|
||||
FavoriteCriterionOption,
|
||||
GenderCriterionOption,
|
||||
PerformerIsMissingCriterionOption,
|
||||
TagsCriterionOption,
|
||||
RatingCriterionOption,
|
||||
StudiosCriterionOption,
|
||||
createCriterionOption("url"),
|
||||
createCriterionOption("tag_count"),
|
||||
createCriterionOption("scene_count"),
|
||||
createCriterionOption("image_count"),
|
||||
createCriterionOption("gallery_count"),
|
||||
...numberCriteria.concat(stringCriteria).map((c) => createCriterionOption(c)),
|
||||
createStringCriterionOption("url"),
|
||||
createMandatoryNumberCriterionOption("tag_count"),
|
||||
createMandatoryNumberCriterionOption("scene_count"),
|
||||
createMandatoryNumberCriterionOption("image_count"),
|
||||
createMandatoryNumberCriterionOption("gallery_count"),
|
||||
...numberCriteria.map((c) => createNumberCriterionOption(c)),
|
||||
...stringCriteria.map((c) => createStringCriterionOption(c)),
|
||||
];
|
||||
export const PerformerListFilterOptions = new ListFilterOptions(
|
||||
defaultSortBy,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { NoneCriterionOption } from "./criteria/none";
|
||||
import { PerformersCriterionOption } from "./criteria/performers";
|
||||
import { SceneTagsCriterionOption, TagsCriterionOption } from "./criteria/tags";
|
||||
import { ListFilterOptions } from "./filter-options";
|
||||
@@ -14,7 +13,6 @@ const sortByOptions = [
|
||||
].map(ListFilterOptions.createSortBy);
|
||||
const displayModeOptions = [DisplayMode.Wall];
|
||||
const criterionOptions = [
|
||||
NoneCriterionOption,
|
||||
TagsCriterionOption,
|
||||
SceneTagsCriterionOption,
|
||||
PerformersCriterionOption,
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { createCriterionOption } from "./criteria/criterion";
|
||||
import {
|
||||
createMandatoryNumberCriterionOption,
|
||||
createStringCriterionOption,
|
||||
} from "./criteria/criterion";
|
||||
import { HasMarkersCriterionOption } from "./criteria/has-markers";
|
||||
import { SceneIsMissingCriterionOption } from "./criteria/is-missing";
|
||||
import { MoviesCriterionOption } from "./criteria/movies";
|
||||
import { NoneCriterionOption } from "./criteria/none";
|
||||
import { OrganizedCriterionOption } from "./criteria/organized";
|
||||
import { PerformersCriterionOption } from "./criteria/performers";
|
||||
import { RatingCriterionOption } from "./criteria/rating";
|
||||
@@ -44,24 +46,23 @@ const displayModeOptions = [
|
||||
];
|
||||
|
||||
const criterionOptions = [
|
||||
NoneCriterionOption,
|
||||
createCriterionOption("path"),
|
||||
createStringCriterionOption("path"),
|
||||
RatingCriterionOption,
|
||||
OrganizedCriterionOption,
|
||||
createCriterionOption("o_counter"),
|
||||
createMandatoryNumberCriterionOption("o_counter"),
|
||||
ResolutionCriterionOption,
|
||||
createCriterionOption("duration"),
|
||||
createMandatoryNumberCriterionOption("duration"),
|
||||
HasMarkersCriterionOption,
|
||||
SceneIsMissingCriterionOption,
|
||||
TagsCriterionOption,
|
||||
createCriterionOption("tag_count"),
|
||||
createMandatoryNumberCriterionOption("tag_count"),
|
||||
PerformerTagsCriterionOption,
|
||||
PerformersCriterionOption,
|
||||
createCriterionOption("performer_count"),
|
||||
createMandatoryNumberCriterionOption("performer_count"),
|
||||
StudiosCriterionOption,
|
||||
MoviesCriterionOption,
|
||||
createCriterionOption("url"),
|
||||
createCriterionOption("stash_id"),
|
||||
createStringCriterionOption("url"),
|
||||
createStringCriterionOption("stash_id"),
|
||||
InteractiveCriterionOption,
|
||||
];
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { createCriterionOption } from "./criteria/criterion";
|
||||
import {
|
||||
createMandatoryNumberCriterionOption,
|
||||
createStringCriterionOption,
|
||||
} from "./criteria/criterion";
|
||||
import { StudioIsMissingCriterionOption } from "./criteria/is-missing";
|
||||
import { NoneCriterionOption } from "./criteria/none";
|
||||
import { RatingCriterionOption } from "./criteria/rating";
|
||||
import { ParentStudiosCriterionOption } from "./criteria/studios";
|
||||
import { ListFilterOptions } from "./filter-options";
|
||||
@@ -26,15 +28,14 @@ const sortByOptions = ["name", "random", "rating"]
|
||||
|
||||
const displayModeOptions = [DisplayMode.Grid];
|
||||
const criterionOptions = [
|
||||
NoneCriterionOption,
|
||||
ParentStudiosCriterionOption,
|
||||
StudioIsMissingCriterionOption,
|
||||
RatingCriterionOption,
|
||||
createCriterionOption("scene_count"),
|
||||
createCriterionOption("image_count"),
|
||||
createCriterionOption("gallery_count"),
|
||||
createCriterionOption("url"),
|
||||
createCriterionOption("stash_id"),
|
||||
createMandatoryNumberCriterionOption("scene_count"),
|
||||
createMandatoryNumberCriterionOption("image_count"),
|
||||
createMandatoryNumberCriterionOption("gallery_count"),
|
||||
createStringCriterionOption("url"),
|
||||
createStringCriterionOption("stash_id"),
|
||||
];
|
||||
|
||||
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 { NoneCriterionOption } from "./criteria/none";
|
||||
import { ListFilterOptions } from "./filter-options";
|
||||
import { DisplayMode } from "./types";
|
||||
|
||||
@@ -34,12 +33,11 @@ const sortByOptions = [
|
||||
|
||||
const displayModeOptions = [DisplayMode.Grid, DisplayMode.List];
|
||||
const criterionOptions = [
|
||||
NoneCriterionOption,
|
||||
TagIsMissingCriterionOption,
|
||||
createCriterionOption("scene_count"),
|
||||
createCriterionOption("image_count"),
|
||||
createCriterionOption("gallery_count"),
|
||||
createCriterionOption("performer_count"),
|
||||
createMandatoryNumberCriterionOption("scene_count"),
|
||||
createMandatoryNumberCriterionOption("image_count"),
|
||||
createMandatoryNumberCriterionOption("gallery_count"),
|
||||
createMandatoryNumberCriterionOption("performer_count"),
|
||||
// marker count has been disabled for now due to performance issues
|
||||
// ListFilterModel.createCriterionOption("marker_count"),
|
||||
];
|
||||
|
||||
@@ -8,17 +8,6 @@ export enum DisplayMode {
|
||||
Tagger,
|
||||
}
|
||||
|
||||
export enum FilterMode {
|
||||
Scenes,
|
||||
Performers,
|
||||
Studios,
|
||||
Galleries,
|
||||
SceneMarkers,
|
||||
Movies,
|
||||
Tags,
|
||||
Images,
|
||||
}
|
||||
|
||||
export interface ILabeledId {
|
||||
id: string;
|
||||
label: string;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import queryString from "query-string";
|
||||
import { RouteComponentProps } from "react-router-dom";
|
||||
import { FilterMode } from "src/core/generated-graphql";
|
||||
import { ListFilterModel } from "./list-filter/filter";
|
||||
import { SceneListFilterOptions } from "./list-filter/scenes";
|
||||
|
||||
@@ -27,7 +28,7 @@ export class SceneQueue {
|
||||
public static fromListFilterModel(filter: ListFilterModel) {
|
||||
const ret = new SceneQueue();
|
||||
|
||||
const filterCopy = Object.assign(new ListFilterModel(), filter);
|
||||
const filterCopy = filter.clone();
|
||||
filterCopy.itemsPerPage = 40;
|
||||
|
||||
ret.originalQueryPage = filter.currentPage;
|
||||
@@ -95,6 +96,7 @@ export class SceneQueue {
|
||||
|
||||
if (parsed.qfp) {
|
||||
const query = new ListFilterModel(
|
||||
FilterMode.Scenes,
|
||||
translated as queryString.ParsedQuery,
|
||||
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>[]
|
||||
) => {
|
||||
if (!performer.id) return "#";
|
||||
const filter = new ListFilterModel();
|
||||
const filter = new ListFilterModel(GQL.FilterMode.Scenes);
|
||||
const criterion = new PerformersCriterion();
|
||||
criterion.value = [
|
||||
{ id: performer.id, label: performer.name || `Performer ${performer.id}` },
|
||||
@@ -45,7 +45,7 @@ const makePerformerImagesUrl = (
|
||||
extraCriteria?: Criterion<CriterionValue>[]
|
||||
) => {
|
||||
if (!performer.id) return "#";
|
||||
const filter = new ListFilterModel();
|
||||
const filter = new ListFilterModel(GQL.FilterMode.Images);
|
||||
const criterion = new PerformersCriterion();
|
||||
criterion.value = [
|
||||
{ id: performer.id, label: performer.name || `Performer ${performer.id}` },
|
||||
@@ -60,7 +60,7 @@ const makePerformerGalleriesUrl = (
|
||||
extraCriteria?: Criterion<CriterionValue>[]
|
||||
) => {
|
||||
if (!performer.id) return "#";
|
||||
const filter = new ListFilterModel();
|
||||
const filter = new ListFilterModel(GQL.FilterMode.Galleries);
|
||||
const criterion = new PerformersCriterion();
|
||||
criterion.value = [
|
||||
{ id: performer.id, label: performer.name || `Performer ${performer.id}` },
|
||||
@@ -74,7 +74,7 @@ const makePerformersCountryUrl = (
|
||||
performer: Partial<GQL.PerformerDataFragment>
|
||||
) => {
|
||||
if (!performer.id) return "#";
|
||||
const filter = new ListFilterModel();
|
||||
const filter = new ListFilterModel(GQL.FilterMode.Performers);
|
||||
const criterion = new CountryCriterion();
|
||||
criterion.value = `${performer.country}`;
|
||||
filter.criteria.push(criterion);
|
||||
@@ -83,7 +83,7 @@ const makePerformersCountryUrl = (
|
||||
|
||||
const makeStudioScenesUrl = (studio: Partial<GQL.StudioDataFragment>) => {
|
||||
if (!studio.id) return "#";
|
||||
const filter = new ListFilterModel();
|
||||
const filter = new ListFilterModel(GQL.FilterMode.Scenes);
|
||||
const criterion = new StudiosCriterion();
|
||||
criterion.value = {
|
||||
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>) => {
|
||||
if (!studio.id) return "#";
|
||||
const filter = new ListFilterModel();
|
||||
const filter = new ListFilterModel(GQL.FilterMode.Images);
|
||||
const criterion = new StudiosCriterion();
|
||||
criterion.value = {
|
||||
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>) => {
|
||||
if (!studio.id) return "#";
|
||||
const filter = new ListFilterModel();
|
||||
const filter = new ListFilterModel(GQL.FilterMode.Galleries);
|
||||
const criterion = new StudiosCriterion();
|
||||
criterion.value = {
|
||||
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>) => {
|
||||
if (!studio.id) return "#";
|
||||
const filter = new ListFilterModel();
|
||||
const filter = new ListFilterModel(GQL.FilterMode.Studios);
|
||||
const criterion = new ParentStudiosCriterion();
|
||||
criterion.value = [
|
||||
{ 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>) => {
|
||||
if (!movie.id) return "#";
|
||||
const filter = new ListFilterModel();
|
||||
const filter = new ListFilterModel(GQL.FilterMode.Scenes);
|
||||
const criterion = new MoviesCriterion();
|
||||
criterion.value = [
|
||||
{ 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>) => {
|
||||
if (!tag.id) return "#";
|
||||
const filter = new ListFilterModel();
|
||||
const filter = new ListFilterModel(GQL.FilterMode.Scenes);
|
||||
const criterion = new TagsCriterion(TagsCriterionOption);
|
||||
criterion.value = [{ id: tag.id, label: tag.name || `Tag ${tag.id}` }];
|
||||
filter.criteria.push(criterion);
|
||||
@@ -150,7 +150,7 @@ const makeTagScenesUrl = (tag: Partial<GQL.TagDataFragment>) => {
|
||||
|
||||
const makeTagPerformersUrl = (tag: Partial<GQL.TagDataFragment>) => {
|
||||
if (!tag.id) return "#";
|
||||
const filter = new ListFilterModel();
|
||||
const filter = new ListFilterModel(GQL.FilterMode.Performers);
|
||||
const criterion = new TagsCriterion(TagsCriterionOption);
|
||||
criterion.value = [{ id: tag.id, label: tag.name || `Tag ${tag.id}` }];
|
||||
filter.criteria.push(criterion);
|
||||
@@ -159,7 +159,7 @@ const makeTagPerformersUrl = (tag: Partial<GQL.TagDataFragment>) => {
|
||||
|
||||
const makeTagSceneMarkersUrl = (tag: Partial<GQL.TagDataFragment>) => {
|
||||
if (!tag.id) return "#";
|
||||
const filter = new ListFilterModel();
|
||||
const filter = new ListFilterModel(GQL.FilterMode.SceneMarkers);
|
||||
const criterion = new TagsCriterion(TagsCriterionOption);
|
||||
criterion.value = [{ id: tag.id, label: tag.name || `Tag ${tag.id}` }];
|
||||
filter.criteria.push(criterion);
|
||||
@@ -168,7 +168,7 @@ const makeTagSceneMarkersUrl = (tag: Partial<GQL.TagDataFragment>) => {
|
||||
|
||||
const makeTagGalleriesUrl = (tag: Partial<GQL.TagDataFragment>) => {
|
||||
if (!tag.id) return "#";
|
||||
const filter = new ListFilterModel();
|
||||
const filter = new ListFilterModel(GQL.FilterMode.Galleries);
|
||||
const criterion = new TagsCriterion(TagsCriterionOption);
|
||||
criterion.value = [{ id: tag.id, label: tag.name || `Tag ${tag.id}` }];
|
||||
filter.criteria.push(criterion);
|
||||
@@ -177,7 +177,7 @@ const makeTagGalleriesUrl = (tag: Partial<GQL.TagDataFragment>) => {
|
||||
|
||||
const makeTagImagesUrl = (tag: Partial<GQL.TagDataFragment>) => {
|
||||
if (!tag.id) return "#";
|
||||
const filter = new ListFilterModel();
|
||||
const filter = new ListFilterModel(GQL.FilterMode.Images);
|
||||
const criterion = new TagsCriterion(TagsCriterionOption);
|
||||
criterion.value = [{ id: tag.id, label: tag.name || `Tag ${tag.id}` }];
|
||||
filter.criteria.push(criterion);
|
||||
|
||||
Reference in New Issue
Block a user