mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 12:24:38 +03:00
Filter studio hierarchy (#1397)
* Add basic support for hierarchical filters Add a new `hierarchicalMultiCriterionHandlerBuilder` filter type which can / will be used for filtering hierarchical things like the parent/child relation of the studios. On the frontend side a new IHierarchicalLabeledIdCriterion criterion type has been added to accompany this new filter type. * Refactor movieQueryBuilder to use filterBuilder Refactor the movieQueryBuilder to use the filterBuilder just as scene, image and gallery as well. * Support specifying depth for studios filter Add an optional depth field to the studios filter for scenes, images, galleries and movies. When specified that number of included (grant)children are shown as well. In other words: this adds support for showing scenes set to child studios when searching on the parent studio. Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
This commit is contained in:
@@ -122,7 +122,7 @@ input SceneFilterType {
|
|||||||
"""Filter to only include scenes missing this property"""
|
"""Filter to only include scenes missing this property"""
|
||||||
is_missing: String
|
is_missing: String
|
||||||
"""Filter to only include scenes with this studio"""
|
"""Filter to only include scenes with this studio"""
|
||||||
studios: MultiCriterionInput
|
studios: HierarchicalMultiCriterionInput
|
||||||
"""Filter to only include scenes with this movie"""
|
"""Filter to only include scenes with this movie"""
|
||||||
movies: MultiCriterionInput
|
movies: MultiCriterionInput
|
||||||
"""Filter to only include scenes with these tags"""
|
"""Filter to only include scenes with these tags"""
|
||||||
@@ -145,7 +145,7 @@ input SceneFilterType {
|
|||||||
|
|
||||||
input MovieFilterType {
|
input MovieFilterType {
|
||||||
"""Filter to only include movies with this studio"""
|
"""Filter to only include movies with this studio"""
|
||||||
studios: MultiCriterionInput
|
studios: HierarchicalMultiCriterionInput
|
||||||
"""Filter to only include movies missing this property"""
|
"""Filter to only include movies missing this property"""
|
||||||
is_missing: String
|
is_missing: String
|
||||||
"""Filter by url"""
|
"""Filter by url"""
|
||||||
@@ -189,7 +189,7 @@ input GalleryFilterType {
|
|||||||
"""Filter by average image resolution"""
|
"""Filter by average image resolution"""
|
||||||
average_resolution: ResolutionEnum
|
average_resolution: ResolutionEnum
|
||||||
"""Filter to only include galleries with this studio"""
|
"""Filter to only include galleries with this studio"""
|
||||||
studios: MultiCriterionInput
|
studios: HierarchicalMultiCriterionInput
|
||||||
"""Filter to only include galleries with these tags"""
|
"""Filter to only include galleries with these tags"""
|
||||||
tags: MultiCriterionInput
|
tags: MultiCriterionInput
|
||||||
"""Filter by tag count"""
|
"""Filter by tag count"""
|
||||||
@@ -254,7 +254,7 @@ input ImageFilterType {
|
|||||||
"""Filter to only include images missing this property"""
|
"""Filter to only include images missing this property"""
|
||||||
is_missing: String
|
is_missing: String
|
||||||
"""Filter to only include images with this studio"""
|
"""Filter to only include images with this studio"""
|
||||||
studios: MultiCriterionInput
|
studios: HierarchicalMultiCriterionInput
|
||||||
"""Filter to only include images with these tags"""
|
"""Filter to only include images with these tags"""
|
||||||
tags: MultiCriterionInput
|
tags: MultiCriterionInput
|
||||||
"""Filter by tag count"""
|
"""Filter by tag count"""
|
||||||
@@ -311,3 +311,9 @@ input GenderCriterionInput {
|
|||||||
value: GenderEnum
|
value: GenderEnum
|
||||||
modifier: CriterionModifier!
|
modifier: CriterionModifier!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input HierarchicalMultiCriterionInput {
|
||||||
|
value: [ID!]
|
||||||
|
modifier: CriterionModifier!
|
||||||
|
depth: Int!
|
||||||
|
}
|
||||||
|
|||||||
@@ -505,9 +505,10 @@ func (me *contentDirectoryService) getStudios() []interface{} {
|
|||||||
|
|
||||||
func (me *contentDirectoryService) getStudioScenes(paths []string, host string) []interface{} {
|
func (me *contentDirectoryService) getStudioScenes(paths []string, host string) []interface{} {
|
||||||
sceneFilter := &models.SceneFilterType{
|
sceneFilter := &models.SceneFilterType{
|
||||||
Studios: &models.MultiCriterionInput{
|
Studios: &models.HierarchicalMultiCriterionInput{
|
||||||
Modifier: models.CriterionModifierIncludes,
|
Modifier: models.CriterionModifierIncludes,
|
||||||
Value: []string{paths[0]},
|
Value: []string{paths[0]},
|
||||||
|
Depth: 0,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,9 +19,10 @@ func CountByPerformerID(r models.GalleryReader, id int) (int, error) {
|
|||||||
|
|
||||||
func CountByStudioID(r models.GalleryReader, id int) (int, error) {
|
func CountByStudioID(r models.GalleryReader, id int) (int, error) {
|
||||||
filter := &models.GalleryFilterType{
|
filter := &models.GalleryFilterType{
|
||||||
Studios: &models.MultiCriterionInput{
|
Studios: &models.HierarchicalMultiCriterionInput{
|
||||||
Value: []string{strconv.Itoa(id)},
|
Value: []string{strconv.Itoa(id)},
|
||||||
Modifier: models.CriterionModifierIncludes,
|
Modifier: models.CriterionModifierIncludes,
|
||||||
|
Depth: 0,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,9 +19,10 @@ func CountByPerformerID(r models.ImageReader, id int) (int, error) {
|
|||||||
|
|
||||||
func CountByStudioID(r models.ImageReader, id int) (int, error) {
|
func CountByStudioID(r models.ImageReader, id int) (int, error) {
|
||||||
filter := &models.ImageFilterType{
|
filter := &models.ImageFilterType{
|
||||||
Studios: &models.MultiCriterionInput{
|
Studios: &models.HierarchicalMultiCriterionInput{
|
||||||
Value: []string{strconv.Itoa(id)},
|
Value: []string{strconv.Itoa(id)},
|
||||||
Modifier: models.CriterionModifierIncludes,
|
Modifier: models.CriterionModifierIncludes,
|
||||||
|
Depth: 0,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ type filterBuilder struct {
|
|||||||
joins joins
|
joins joins
|
||||||
whereClauses []sqlClause
|
whereClauses []sqlClause
|
||||||
havingClauses []sqlClause
|
havingClauses []sqlClause
|
||||||
|
withClauses []sqlClause
|
||||||
|
|
||||||
err error
|
err error
|
||||||
}
|
}
|
||||||
@@ -169,6 +170,15 @@ func (f *filterBuilder) addHaving(sql string, args ...interface{}) {
|
|||||||
f.havingClauses = append(f.havingClauses, makeClause(sql, args...))
|
f.havingClauses = append(f.havingClauses, makeClause(sql, args...))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// addWith adds a with clause and arguments to the filter
|
||||||
|
func (f *filterBuilder) addWith(sql string, args ...interface{}) {
|
||||||
|
if sql == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
f.withClauses = append(f.withClauses, makeClause(sql, args...))
|
||||||
|
}
|
||||||
|
|
||||||
func (f *filterBuilder) getSubFilterClause(clause, subFilterClause string) string {
|
func (f *filterBuilder) getSubFilterClause(clause, subFilterClause string) string {
|
||||||
ret := clause
|
ret := clause
|
||||||
|
|
||||||
@@ -226,6 +236,21 @@ func (f *filterBuilder) generateHavingClauses() (string, []interface{}) {
|
|||||||
return clause, args
|
return clause, args
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *filterBuilder) generateWithClauses() (string, []interface{}) {
|
||||||
|
var clauses []string
|
||||||
|
var args []interface{}
|
||||||
|
for _, w := range f.withClauses {
|
||||||
|
clauses = append(clauses, w.sql)
|
||||||
|
args = append(args, w.args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(clauses) > 0 {
|
||||||
|
return strings.Join(clauses, ", "), args
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
// getAllJoins returns all of the joins in this filter and any sub-filter(s).
|
// getAllJoins returns all of the joins in this filter and any sub-filter(s).
|
||||||
// Redundant joins will not be duplicated in the return value.
|
// Redundant joins will not be duplicated in the return value.
|
||||||
func (f *filterBuilder) getAllJoins() joins {
|
func (f *filterBuilder) getAllJoins() joins {
|
||||||
@@ -501,6 +526,58 @@ func (m *stringListCriterionHandlerBuilder) handler(criterion *models.StringCrit
|
|||||||
m.addJoinTable(f)
|
m.addJoinTable(f)
|
||||||
|
|
||||||
stringCriterionHandler(criterion, m.joinTable+"."+m.stringColumn)(f)
|
stringCriterionHandler(criterion, m.joinTable+"."+m.stringColumn)(f)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type hierarchicalMultiCriterionHandlerBuilder struct {
|
||||||
|
primaryTable string
|
||||||
|
foreignTable string
|
||||||
|
foreignFK string
|
||||||
|
|
||||||
|
derivedTable string
|
||||||
|
parentFK string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *hierarchicalMultiCriterionHandlerBuilder) handler(criterion *models.HierarchicalMultiCriterionInput) criterionHandlerFunc {
|
||||||
|
return func(f *filterBuilder) {
|
||||||
|
if criterion != nil && len(criterion.Value) > 0 {
|
||||||
|
var args []interface{}
|
||||||
|
for _, value := range criterion.Value {
|
||||||
|
args = append(args, value)
|
||||||
|
}
|
||||||
|
inCount := len(args)
|
||||||
|
|
||||||
|
f.addJoin(m.derivedTable, "", fmt.Sprintf("%s.child_id = %s.%s", m.derivedTable, m.primaryTable, m.foreignFK))
|
||||||
|
|
||||||
|
var depthCondition string
|
||||||
|
if criterion.Depth != -1 {
|
||||||
|
depthCondition = "WHERE depth < ?"
|
||||||
|
args = append(args, criterion.Depth)
|
||||||
|
}
|
||||||
|
|
||||||
|
withClause := fmt.Sprintf(
|
||||||
|
"RECURSIVE %s AS (SELECT id as id, id as child_id, 0 as depth FROM %s WHERE id in %s UNION SELECT p.id, c.id, depth + 1 FROM %s as c INNER JOIN %s as p ON c.%s = p.child_id %s)",
|
||||||
|
m.derivedTable,
|
||||||
|
m.foreignTable,
|
||||||
|
getInBinding(inCount),
|
||||||
|
m.foreignTable,
|
||||||
|
m.derivedTable,
|
||||||
|
m.parentFK,
|
||||||
|
depthCondition,
|
||||||
|
)
|
||||||
|
|
||||||
|
f.addWith(withClause, args...)
|
||||||
|
|
||||||
|
if criterion.Modifier == models.CriterionModifierIncludes {
|
||||||
|
f.addWhere(fmt.Sprintf("%s.id IS NOT NULL", m.derivedTable))
|
||||||
|
} else if criterion.Modifier == models.CriterionModifierIncludesAll {
|
||||||
|
f.addWhere(fmt.Sprintf("%s.id IS NOT NULL", m.derivedTable))
|
||||||
|
f.addHaving(fmt.Sprintf("count(distinct %s.id) IS %d", m.derivedTable, inCount))
|
||||||
|
} else if criterion.Modifier == models.CriterionModifierExcludes {
|
||||||
|
f.addWhere(fmt.Sprintf("%s.id IS NULL", m.derivedTable))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -382,11 +382,14 @@ func galleryImageCountCriterionHandler(qb *galleryQueryBuilder, imageCount *mode
|
|||||||
return h.handler(imageCount)
|
return h.handler(imageCount)
|
||||||
}
|
}
|
||||||
|
|
||||||
func galleryStudioCriterionHandler(qb *galleryQueryBuilder, studios *models.MultiCriterionInput) criterionHandlerFunc {
|
func galleryStudioCriterionHandler(qb *galleryQueryBuilder, studios *models.HierarchicalMultiCriterionInput) criterionHandlerFunc {
|
||||||
addJoinsFunc := func(f *filterBuilder) {
|
h := hierarchicalMultiCriterionHandlerBuilder{
|
||||||
f.addJoin(studioTable, "studio", "studio.id = galleries.studio_id")
|
primaryTable: galleryTable,
|
||||||
|
foreignTable: studioTable,
|
||||||
|
foreignFK: studioIDColumn,
|
||||||
|
derivedTable: "studio",
|
||||||
|
parentFK: "parent_id",
|
||||||
}
|
}
|
||||||
h := qb.getMultiCriterionHandlerBuilder("studio", "", studioIDColumn, addJoinsFunc)
|
|
||||||
|
|
||||||
return h.handler(studios)
|
return h.handler(studios)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -675,11 +675,12 @@ func TestGalleryQueryTags(t *testing.T) {
|
|||||||
func TestGalleryQueryStudio(t *testing.T) {
|
func TestGalleryQueryStudio(t *testing.T) {
|
||||||
withTxn(func(r models.Repository) error {
|
withTxn(func(r models.Repository) error {
|
||||||
sqb := r.Gallery()
|
sqb := r.Gallery()
|
||||||
studioCriterion := models.MultiCriterionInput{
|
studioCriterion := models.HierarchicalMultiCriterionInput{
|
||||||
Value: []string{
|
Value: []string{
|
||||||
strconv.Itoa(studioIDs[studioIdxWithGallery]),
|
strconv.Itoa(studioIDs[studioIdxWithGallery]),
|
||||||
},
|
},
|
||||||
Modifier: models.CriterionModifierIncludes,
|
Modifier: models.CriterionModifierIncludes,
|
||||||
|
Depth: 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
galleryFilter := models.GalleryFilterType{
|
galleryFilter := models.GalleryFilterType{
|
||||||
@@ -693,11 +694,12 @@ func TestGalleryQueryStudio(t *testing.T) {
|
|||||||
// ensure id is correct
|
// ensure id is correct
|
||||||
assert.Equal(t, galleryIDs[galleryIdxWithStudio], galleries[0].ID)
|
assert.Equal(t, galleryIDs[galleryIdxWithStudio], galleries[0].ID)
|
||||||
|
|
||||||
studioCriterion = models.MultiCriterionInput{
|
studioCriterion = models.HierarchicalMultiCriterionInput{
|
||||||
Value: []string{
|
Value: []string{
|
||||||
strconv.Itoa(studioIDs[studioIdxWithGallery]),
|
strconv.Itoa(studioIDs[studioIdxWithGallery]),
|
||||||
},
|
},
|
||||||
Modifier: models.CriterionModifierExcludes,
|
Modifier: models.CriterionModifierExcludes,
|
||||||
|
Depth: 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
q := getGalleryStringValue(galleryIdxWithStudio, titleField)
|
q := getGalleryStringValue(galleryIdxWithStudio, titleField)
|
||||||
@@ -712,6 +714,64 @@ func TestGalleryQueryStudio(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGalleryQueryStudioDepth(t *testing.T) {
|
||||||
|
withTxn(func(r models.Repository) error {
|
||||||
|
sqb := r.Gallery()
|
||||||
|
studioCriterion := models.HierarchicalMultiCriterionInput{
|
||||||
|
Value: []string{
|
||||||
|
strconv.Itoa(studioIDs[studioIdxWithGrandChild]),
|
||||||
|
},
|
||||||
|
Modifier: models.CriterionModifierIncludes,
|
||||||
|
Depth: 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
galleryFilter := models.GalleryFilterType{
|
||||||
|
Studios: &studioCriterion,
|
||||||
|
}
|
||||||
|
|
||||||
|
galleries := queryGallery(t, sqb, &galleryFilter, nil)
|
||||||
|
assert.Len(t, galleries, 1)
|
||||||
|
|
||||||
|
studioCriterion.Depth = 1
|
||||||
|
|
||||||
|
galleries = queryGallery(t, sqb, &galleryFilter, nil)
|
||||||
|
assert.Len(t, galleries, 0)
|
||||||
|
|
||||||
|
studioCriterion.Value = []string{strconv.Itoa(studioIDs[studioIdxWithParentAndChild])}
|
||||||
|
galleries = queryGallery(t, sqb, &galleryFilter, nil)
|
||||||
|
assert.Len(t, galleries, 1)
|
||||||
|
|
||||||
|
// ensure id is correct
|
||||||
|
assert.Equal(t, galleryIDs[galleryIdxWithGrandChildStudio], galleries[0].ID)
|
||||||
|
|
||||||
|
studioCriterion = models.HierarchicalMultiCriterionInput{
|
||||||
|
Value: []string{
|
||||||
|
strconv.Itoa(studioIDs[studioIdxWithGrandChild]),
|
||||||
|
},
|
||||||
|
Modifier: models.CriterionModifierExcludes,
|
||||||
|
Depth: 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
q := getGalleryStringValue(galleryIdxWithGrandChildStudio, pathField)
|
||||||
|
findFilter := models.FindFilterType{
|
||||||
|
Q: &q,
|
||||||
|
}
|
||||||
|
|
||||||
|
galleries = queryGallery(t, sqb, &galleryFilter, &findFilter)
|
||||||
|
assert.Len(t, galleries, 0)
|
||||||
|
|
||||||
|
studioCriterion.Depth = 1
|
||||||
|
galleries = queryGallery(t, sqb, &galleryFilter, &findFilter)
|
||||||
|
assert.Len(t, galleries, 1)
|
||||||
|
|
||||||
|
studioCriterion.Value = []string{strconv.Itoa(studioIDs[studioIdxWithParentAndChild])}
|
||||||
|
galleries = queryGallery(t, sqb, &galleryFilter, &findFilter)
|
||||||
|
assert.Len(t, galleries, 0)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestGalleryQueryPerformerTags(t *testing.T) {
|
func TestGalleryQueryPerformerTags(t *testing.T) {
|
||||||
withTxn(func(r models.Repository) error {
|
withTxn(func(r models.Repository) error {
|
||||||
sqb := r.Gallery()
|
sqb := r.Gallery()
|
||||||
|
|||||||
@@ -408,11 +408,14 @@ func imagePerformerCountCriterionHandler(qb *imageQueryBuilder, performerCount *
|
|||||||
return h.handler(performerCount)
|
return h.handler(performerCount)
|
||||||
}
|
}
|
||||||
|
|
||||||
func imageStudioCriterionHandler(qb *imageQueryBuilder, studios *models.MultiCriterionInput) criterionHandlerFunc {
|
func imageStudioCriterionHandler(qb *imageQueryBuilder, studios *models.HierarchicalMultiCriterionInput) criterionHandlerFunc {
|
||||||
addJoinsFunc := func(f *filterBuilder) {
|
h := hierarchicalMultiCriterionHandlerBuilder{
|
||||||
f.addJoin(studioTable, "studio", "studio.id = images.studio_id")
|
primaryTable: imageTable,
|
||||||
|
foreignTable: studioTable,
|
||||||
|
foreignFK: studioIDColumn,
|
||||||
|
derivedTable: "studio",
|
||||||
|
parentFK: "parent_id",
|
||||||
}
|
}
|
||||||
h := qb.getMultiCriterionHandlerBuilder("studio", "", studioIDColumn, addJoinsFunc)
|
|
||||||
|
|
||||||
return h.handler(studios)
|
return h.handler(studios)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -783,11 +783,12 @@ func TestImageQueryTags(t *testing.T) {
|
|||||||
func TestImageQueryStudio(t *testing.T) {
|
func TestImageQueryStudio(t *testing.T) {
|
||||||
withTxn(func(r models.Repository) error {
|
withTxn(func(r models.Repository) error {
|
||||||
sqb := r.Image()
|
sqb := r.Image()
|
||||||
studioCriterion := models.MultiCriterionInput{
|
studioCriterion := models.HierarchicalMultiCriterionInput{
|
||||||
Value: []string{
|
Value: []string{
|
||||||
strconv.Itoa(studioIDs[studioIdxWithImage]),
|
strconv.Itoa(studioIDs[studioIdxWithImage]),
|
||||||
},
|
},
|
||||||
Modifier: models.CriterionModifierIncludes,
|
Modifier: models.CriterionModifierIncludes,
|
||||||
|
Depth: 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
imageFilter := models.ImageFilterType{
|
imageFilter := models.ImageFilterType{
|
||||||
@@ -804,11 +805,12 @@ func TestImageQueryStudio(t *testing.T) {
|
|||||||
// ensure id is correct
|
// ensure id is correct
|
||||||
assert.Equal(t, imageIDs[imageIdxWithStudio], images[0].ID)
|
assert.Equal(t, imageIDs[imageIdxWithStudio], images[0].ID)
|
||||||
|
|
||||||
studioCriterion = models.MultiCriterionInput{
|
studioCriterion = models.HierarchicalMultiCriterionInput{
|
||||||
Value: []string{
|
Value: []string{
|
||||||
strconv.Itoa(studioIDs[studioIdxWithImage]),
|
strconv.Itoa(studioIDs[studioIdxWithImage]),
|
||||||
},
|
},
|
||||||
Modifier: models.CriterionModifierExcludes,
|
Modifier: models.CriterionModifierExcludes,
|
||||||
|
Depth: 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
q := getImageStringValue(imageIdxWithStudio, titleField)
|
q := getImageStringValue(imageIdxWithStudio, titleField)
|
||||||
@@ -826,6 +828,64 @@ func TestImageQueryStudio(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestImageQueryStudioDepth(t *testing.T) {
|
||||||
|
withTxn(func(r models.Repository) error {
|
||||||
|
sqb := r.Image()
|
||||||
|
studioCriterion := models.HierarchicalMultiCriterionInput{
|
||||||
|
Value: []string{
|
||||||
|
strconv.Itoa(studioIDs[studioIdxWithGrandChild]),
|
||||||
|
},
|
||||||
|
Modifier: models.CriterionModifierIncludes,
|
||||||
|
Depth: 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
imageFilter := models.ImageFilterType{
|
||||||
|
Studios: &studioCriterion,
|
||||||
|
}
|
||||||
|
|
||||||
|
images := queryImages(t, sqb, &imageFilter, nil)
|
||||||
|
assert.Len(t, images, 1)
|
||||||
|
|
||||||
|
studioCriterion.Depth = 1
|
||||||
|
|
||||||
|
images = queryImages(t, sqb, &imageFilter, nil)
|
||||||
|
assert.Len(t, images, 0)
|
||||||
|
|
||||||
|
studioCriterion.Value = []string{strconv.Itoa(studioIDs[studioIdxWithParentAndChild])}
|
||||||
|
images = queryImages(t, sqb, &imageFilter, nil)
|
||||||
|
assert.Len(t, images, 1)
|
||||||
|
|
||||||
|
// ensure id is correct
|
||||||
|
assert.Equal(t, imageIDs[imageIdxWithGrandChildStudio], images[0].ID)
|
||||||
|
|
||||||
|
studioCriterion = models.HierarchicalMultiCriterionInput{
|
||||||
|
Value: []string{
|
||||||
|
strconv.Itoa(studioIDs[studioIdxWithGrandChild]),
|
||||||
|
},
|
||||||
|
Modifier: models.CriterionModifierExcludes,
|
||||||
|
Depth: 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
q := getImageStringValue(imageIdxWithGrandChildStudio, titleField)
|
||||||
|
findFilter := models.FindFilterType{
|
||||||
|
Q: &q,
|
||||||
|
}
|
||||||
|
|
||||||
|
images = queryImages(t, sqb, &imageFilter, &findFilter)
|
||||||
|
assert.Len(t, images, 0)
|
||||||
|
|
||||||
|
studioCriterion.Depth = 1
|
||||||
|
images = queryImages(t, sqb, &imageFilter, &findFilter)
|
||||||
|
assert.Len(t, images, 1)
|
||||||
|
|
||||||
|
studioCriterion.Value = []string{strconv.Itoa(studioIDs[studioIdxWithParentAndChild])}
|
||||||
|
images = queryImages(t, sqb, &imageFilter, &findFilter)
|
||||||
|
assert.Len(t, images, 0)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func queryImages(t *testing.T, sqb models.ImageReader, imageFilter *models.ImageFilterType, findFilter *models.FindFilterType) []*models.Image {
|
func queryImages(t *testing.T, sqb models.ImageReader, imageFilter *models.ImageFilterType, findFilter *models.FindFilterType) []*models.Image {
|
||||||
images, _, err := sqb.Query(imageFilter, findFilter)
|
images, _, err := sqb.Query(imageFilter, findFilter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const movieTable = "movies"
|
const movieTable = "movies"
|
||||||
|
const movieIDColumn = "movie_id"
|
||||||
|
|
||||||
type movieQueryBuilder struct {
|
type movieQueryBuilder struct {
|
||||||
repository
|
repository
|
||||||
@@ -114,6 +115,16 @@ func (qb *movieQueryBuilder) All() ([]*models.Movie, error) {
|
|||||||
return qb.queryMovies(selectAll("movies")+qb.getMovieSort(nil), nil)
|
return qb.queryMovies(selectAll("movies")+qb.getMovieSort(nil), nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (qb *movieQueryBuilder) makeFilter(movieFilter *models.MovieFilterType) *filterBuilder {
|
||||||
|
query := &filterBuilder{}
|
||||||
|
|
||||||
|
query.handleCriterionFunc(movieIsMissingCriterionHandler(qb, movieFilter.IsMissing))
|
||||||
|
query.handleCriterionFunc(stringCriterionHandler(movieFilter.URL, "movies.url"))
|
||||||
|
query.handleCriterionFunc(movieStudioCriterionHandler(qb, movieFilter.Studios))
|
||||||
|
|
||||||
|
return query
|
||||||
|
}
|
||||||
|
|
||||||
func (qb *movieQueryBuilder) Query(movieFilter *models.MovieFilterType, findFilter *models.FindFilterType) ([]*models.Movie, int, error) {
|
func (qb *movieQueryBuilder) Query(movieFilter *models.MovieFilterType, findFilter *models.FindFilterType) ([]*models.Movie, int, error) {
|
||||||
if findFilter == nil {
|
if findFilter == nil {
|
||||||
findFilter = &models.FindFilterType{}
|
findFilter = &models.FindFilterType{}
|
||||||
@@ -125,11 +136,6 @@ func (qb *movieQueryBuilder) Query(movieFilter *models.MovieFilterType, findFilt
|
|||||||
query := qb.newQuery()
|
query := qb.newQuery()
|
||||||
|
|
||||||
query.body = selectDistinctIDs("movies")
|
query.body = selectDistinctIDs("movies")
|
||||||
query.body += `
|
|
||||||
left join movies_scenes as scenes_join on scenes_join.movie_id = movies.id
|
|
||||||
left join scenes on scenes_join.scene_id = scenes.id
|
|
||||||
left join studios as studio on studio.id = movies.studio_id
|
|
||||||
`
|
|
||||||
|
|
||||||
if q := findFilter.Q; q != nil && *q != "" {
|
if q := findFilter.Q; q != nil && *q != "" {
|
||||||
searchColumns := []string{"movies.name"}
|
searchColumns := []string{"movies.name"}
|
||||||
@@ -138,36 +144,9 @@ func (qb *movieQueryBuilder) Query(movieFilter *models.MovieFilterType, findFilt
|
|||||||
query.addArg(thisArgs...)
|
query.addArg(thisArgs...)
|
||||||
}
|
}
|
||||||
|
|
||||||
if studiosFilter := movieFilter.Studios; studiosFilter != nil && len(studiosFilter.Value) > 0 {
|
filter := qb.makeFilter(movieFilter)
|
||||||
for _, studioID := range studiosFilter.Value {
|
|
||||||
query.addArg(studioID)
|
|
||||||
}
|
|
||||||
|
|
||||||
whereClause, havingClause := getMultiCriterionClause("movies", "studio", "", "", "studio_id", studiosFilter)
|
query.addFilter(filter)
|
||||||
query.addWhere(whereClause)
|
|
||||||
query.addHaving(havingClause)
|
|
||||||
}
|
|
||||||
|
|
||||||
if isMissingFilter := movieFilter.IsMissing; isMissingFilter != nil && *isMissingFilter != "" {
|
|
||||||
switch *isMissingFilter {
|
|
||||||
case "front_image":
|
|
||||||
query.body += `left join movies_images on movies_images.movie_id = movies.id
|
|
||||||
`
|
|
||||||
query.addWhere("movies_images.front_image IS NULL")
|
|
||||||
case "back_image":
|
|
||||||
query.body += `left join movies_images on movies_images.movie_id = movies.id
|
|
||||||
`
|
|
||||||
query.addWhere("movies_images.back_image IS NULL")
|
|
||||||
case "scenes":
|
|
||||||
query.body += `left join movies_scenes on movies_scenes.movie_id = movies.id
|
|
||||||
`
|
|
||||||
query.addWhere("movies_scenes.scene_id IS NULL")
|
|
||||||
default:
|
|
||||||
query.addWhere("movies." + *isMissingFilter + " IS NULL")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
query.handleStringCriterionInput(movieFilter.URL, "movies.url")
|
|
||||||
|
|
||||||
query.sortAndPagination = qb.getMovieSort(findFilter) + getPagination(findFilter)
|
query.sortAndPagination = qb.getMovieSort(findFilter) + getPagination(findFilter)
|
||||||
idsResult, countResult, err := query.executeFind()
|
idsResult, countResult, err := query.executeFind()
|
||||||
@@ -188,6 +167,38 @@ func (qb *movieQueryBuilder) Query(movieFilter *models.MovieFilterType, findFilt
|
|||||||
return movies, countResult, nil
|
return movies, countResult, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func movieIsMissingCriterionHandler(qb *movieQueryBuilder, isMissing *string) criterionHandlerFunc {
|
||||||
|
return func(f *filterBuilder) {
|
||||||
|
if isMissing != nil && *isMissing != "" {
|
||||||
|
switch *isMissing {
|
||||||
|
case "front_image":
|
||||||
|
f.addJoin("movies_images", "", "movies_images.movie_id = movies.id")
|
||||||
|
f.addWhere("movies_images.front_image IS NULL")
|
||||||
|
case "back_image":
|
||||||
|
f.addJoin("movies_images", "", "movies_images.movie_id = movies.id")
|
||||||
|
f.addWhere("movies_images.back_image IS NULL")
|
||||||
|
case "scenes":
|
||||||
|
f.addJoin("movies_scenes", "", "movies_scenes.movie_id = movies.id")
|
||||||
|
f.addWhere("movies_scenes.scene_id IS NULL")
|
||||||
|
default:
|
||||||
|
f.addWhere("(movies." + *isMissing + " IS NULL OR TRIM(movies." + *isMissing + ") = '')")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func movieStudioCriterionHandler(qb *movieQueryBuilder, studios *models.HierarchicalMultiCriterionInput) criterionHandlerFunc {
|
||||||
|
h := hierarchicalMultiCriterionHandlerBuilder{
|
||||||
|
primaryTable: movieTable,
|
||||||
|
foreignTable: studioTable,
|
||||||
|
foreignFK: studioIDColumn,
|
||||||
|
derivedTable: "studio",
|
||||||
|
parentFK: "parent_id",
|
||||||
|
}
|
||||||
|
|
||||||
|
return h.handler(studios)
|
||||||
|
}
|
||||||
|
|
||||||
func (qb *movieQueryBuilder) getMovieSort(findFilter *models.FindFilterType) string {
|
func (qb *movieQueryBuilder) getMovieSort(findFilter *models.FindFilterType) string {
|
||||||
var sort string
|
var sort string
|
||||||
var direction string
|
var direction string
|
||||||
|
|||||||
@@ -76,11 +76,12 @@ func TestMovieFindByNames(t *testing.T) {
|
|||||||
func TestMovieQueryStudio(t *testing.T) {
|
func TestMovieQueryStudio(t *testing.T) {
|
||||||
withTxn(func(r models.Repository) error {
|
withTxn(func(r models.Repository) error {
|
||||||
mqb := r.Movie()
|
mqb := r.Movie()
|
||||||
studioCriterion := models.MultiCriterionInput{
|
studioCriterion := models.HierarchicalMultiCriterionInput{
|
||||||
Value: []string{
|
Value: []string{
|
||||||
strconv.Itoa(studioIDs[studioIdxWithMovie]),
|
strconv.Itoa(studioIDs[studioIdxWithMovie]),
|
||||||
},
|
},
|
||||||
Modifier: models.CriterionModifierIncludes,
|
Modifier: models.CriterionModifierIncludes,
|
||||||
|
Depth: 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
movieFilter := models.MovieFilterType{
|
movieFilter := models.MovieFilterType{
|
||||||
@@ -97,11 +98,12 @@ func TestMovieQueryStudio(t *testing.T) {
|
|||||||
// ensure id is correct
|
// ensure id is correct
|
||||||
assert.Equal(t, movieIDs[movieIdxWithStudio], movies[0].ID)
|
assert.Equal(t, movieIDs[movieIdxWithStudio], movies[0].ID)
|
||||||
|
|
||||||
studioCriterion = models.MultiCriterionInput{
|
studioCriterion = models.HierarchicalMultiCriterionInput{
|
||||||
Value: []string{
|
Value: []string{
|
||||||
strconv.Itoa(studioIDs[studioIdxWithMovie]),
|
strconv.Itoa(studioIDs[studioIdxWithMovie]),
|
||||||
},
|
},
|
||||||
Modifier: models.CriterionModifierExcludes,
|
Modifier: models.CriterionModifierExcludes,
|
||||||
|
Depth: 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
q := getMovieStringValue(movieIdxWithStudio, titleField)
|
q := getMovieStringValue(movieIdxWithStudio, titleField)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package sqlite
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/stashapp/stash/pkg/models"
|
"github.com/stashapp/stash/pkg/models"
|
||||||
)
|
)
|
||||||
@@ -16,6 +17,7 @@ type queryBuilder struct {
|
|||||||
whereClauses []string
|
whereClauses []string
|
||||||
havingClauses []string
|
havingClauses []string
|
||||||
args []interface{}
|
args []interface{}
|
||||||
|
withClauses []string
|
||||||
|
|
||||||
sortAndPagination string
|
sortAndPagination string
|
||||||
|
|
||||||
@@ -30,7 +32,7 @@ func (qb queryBuilder) executeFind() ([]int, int, error) {
|
|||||||
body := qb.body
|
body := qb.body
|
||||||
body += qb.joins.toSQL()
|
body += qb.joins.toSQL()
|
||||||
|
|
||||||
return qb.repository.executeFindQuery(body, qb.args, qb.sortAndPagination, qb.whereClauses, qb.havingClauses)
|
return qb.repository.executeFindQuery(body, qb.args, qb.sortAndPagination, qb.whereClauses, qb.havingClauses, qb.withClauses)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (qb queryBuilder) executeCount() (int, error) {
|
func (qb queryBuilder) executeCount() (int, error) {
|
||||||
@@ -41,8 +43,13 @@ func (qb queryBuilder) executeCount() (int, error) {
|
|||||||
body := qb.body
|
body := qb.body
|
||||||
body += qb.joins.toSQL()
|
body += qb.joins.toSQL()
|
||||||
|
|
||||||
|
withClause := ""
|
||||||
|
if len(qb.withClauses) > 0 {
|
||||||
|
withClause = "WITH " + strings.Join(qb.withClauses, ", ") + " "
|
||||||
|
}
|
||||||
|
|
||||||
body = qb.repository.buildQueryBody(body, qb.whereClauses, qb.havingClauses)
|
body = qb.repository.buildQueryBody(body, qb.whereClauses, qb.havingClauses)
|
||||||
countQuery := qb.repository.buildCountQuery(body)
|
countQuery := withClause + qb.repository.buildCountQuery(body)
|
||||||
return qb.repository.runCountQuery(countQuery, qb.args)
|
return qb.repository.runCountQuery(countQuery, qb.args)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,6 +69,14 @@ func (qb *queryBuilder) addHaving(clauses ...string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (qb *queryBuilder) addWith(clauses ...string) {
|
||||||
|
for _, clause := range clauses {
|
||||||
|
if len(clause) > 0 {
|
||||||
|
qb.withClauses = append(qb.withClauses, clause)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (qb *queryBuilder) addArg(args ...interface{}) {
|
func (qb *queryBuilder) addArg(args ...interface{}) {
|
||||||
qb.args = append(qb.args, args...)
|
qb.args = append(qb.args, args...)
|
||||||
}
|
}
|
||||||
@@ -87,7 +102,17 @@ func (qb *queryBuilder) addFilter(f *filterBuilder) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
clause, args := f.generateWhereClauses()
|
clause, args := f.generateWithClauses()
|
||||||
|
if len(clause) > 0 {
|
||||||
|
qb.addWith(clause)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(args) > 0 {
|
||||||
|
// WITH clause always comes first and thus precedes alk args
|
||||||
|
qb.args = append(args, qb.args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
clause, args = f.generateWhereClauses()
|
||||||
if len(clause) > 0 {
|
if len(clause) > 0 {
|
||||||
qb.addWhere(clause)
|
qb.addWhere(clause)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -246,11 +246,16 @@ func (r *repository) buildQueryBody(body string, whereClauses []string, havingCl
|
|||||||
return body
|
return body
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *repository) executeFindQuery(body string, args []interface{}, sortAndPagination string, whereClauses []string, havingClauses []string) ([]int, int, error) {
|
func (r *repository) executeFindQuery(body string, args []interface{}, sortAndPagination string, whereClauses []string, havingClauses []string, withClauses []string) ([]int, int, error) {
|
||||||
body = r.buildQueryBody(body, whereClauses, havingClauses)
|
body = r.buildQueryBody(body, whereClauses, havingClauses)
|
||||||
|
|
||||||
countQuery := r.buildCountQuery(body)
|
withClause := ""
|
||||||
idsQuery := body + sortAndPagination
|
if len(withClauses) > 0 {
|
||||||
|
withClause = "WITH " + strings.Join(withClauses, ", ") + " "
|
||||||
|
}
|
||||||
|
|
||||||
|
countQuery := withClause + r.buildCountQuery(body)
|
||||||
|
idsQuery := withClause + body + sortAndPagination
|
||||||
|
|
||||||
// Perform query and fetch result
|
// Perform query and fetch result
|
||||||
logger.Tracef("SQL: %s, args: %v", idsQuery, args)
|
logger.Tracef("SQL: %s, args: %v", idsQuery, args)
|
||||||
|
|||||||
@@ -588,11 +588,14 @@ func scenePerformerCountCriterionHandler(qb *sceneQueryBuilder, performerCount *
|
|||||||
return h.handler(performerCount)
|
return h.handler(performerCount)
|
||||||
}
|
}
|
||||||
|
|
||||||
func sceneStudioCriterionHandler(qb *sceneQueryBuilder, studios *models.MultiCriterionInput) criterionHandlerFunc {
|
func sceneStudioCriterionHandler(qb *sceneQueryBuilder, studios *models.HierarchicalMultiCriterionInput) criterionHandlerFunc {
|
||||||
addJoinsFunc := func(f *filterBuilder) {
|
h := hierarchicalMultiCriterionHandlerBuilder{
|
||||||
f.addJoin("studios", "studio", "studio.id = scenes.studio_id")
|
primaryTable: sceneTable,
|
||||||
|
foreignTable: studioTable,
|
||||||
|
foreignFK: studioIDColumn,
|
||||||
|
derivedTable: "studio",
|
||||||
|
parentFK: "parent_id",
|
||||||
}
|
}
|
||||||
h := qb.getMultiCriterionHandlerBuilder("studio", "", studioIDColumn, addJoinsFunc)
|
|
||||||
|
|
||||||
return h.handler(studios)
|
return h.handler(studios)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -249,7 +249,7 @@ func (qb *sceneMarkerQueryBuilder) Query(sceneMarkerFilter *models.SceneMarkerFi
|
|||||||
}
|
}
|
||||||
|
|
||||||
sortAndPagination := qb.getSceneMarkerSort(findFilter) + getPagination(findFilter)
|
sortAndPagination := qb.getSceneMarkerSort(findFilter) + getPagination(findFilter)
|
||||||
idsResult, countResult, err := qb.executeFindQuery(body, args, sortAndPagination, whereClauses, havingClauses)
|
idsResult, countResult, err := qb.executeFindQuery(body, args, sortAndPagination, whereClauses, havingClauses, []string{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1070,11 +1070,12 @@ func TestSceneQueryPerformerTags(t *testing.T) {
|
|||||||
func TestSceneQueryStudio(t *testing.T) {
|
func TestSceneQueryStudio(t *testing.T) {
|
||||||
withTxn(func(r models.Repository) error {
|
withTxn(func(r models.Repository) error {
|
||||||
sqb := r.Scene()
|
sqb := r.Scene()
|
||||||
studioCriterion := models.MultiCriterionInput{
|
studioCriterion := models.HierarchicalMultiCriterionInput{
|
||||||
Value: []string{
|
Value: []string{
|
||||||
strconv.Itoa(studioIDs[studioIdxWithScene]),
|
strconv.Itoa(studioIDs[studioIdxWithScene]),
|
||||||
},
|
},
|
||||||
Modifier: models.CriterionModifierIncludes,
|
Modifier: models.CriterionModifierIncludes,
|
||||||
|
Depth: 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
sceneFilter := models.SceneFilterType{
|
sceneFilter := models.SceneFilterType{
|
||||||
@@ -1088,11 +1089,12 @@ func TestSceneQueryStudio(t *testing.T) {
|
|||||||
// ensure id is correct
|
// ensure id is correct
|
||||||
assert.Equal(t, sceneIDs[sceneIdxWithStudio], scenes[0].ID)
|
assert.Equal(t, sceneIDs[sceneIdxWithStudio], scenes[0].ID)
|
||||||
|
|
||||||
studioCriterion = models.MultiCriterionInput{
|
studioCriterion = models.HierarchicalMultiCriterionInput{
|
||||||
Value: []string{
|
Value: []string{
|
||||||
strconv.Itoa(studioIDs[studioIdxWithScene]),
|
strconv.Itoa(studioIDs[studioIdxWithScene]),
|
||||||
},
|
},
|
||||||
Modifier: models.CriterionModifierExcludes,
|
Modifier: models.CriterionModifierExcludes,
|
||||||
|
Depth: 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
q := getSceneStringValue(sceneIdxWithStudio, titleField)
|
q := getSceneStringValue(sceneIdxWithStudio, titleField)
|
||||||
@@ -1107,6 +1109,64 @@ func TestSceneQueryStudio(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSceneQueryStudioDepth(t *testing.T) {
|
||||||
|
withTxn(func(r models.Repository) error {
|
||||||
|
sqb := r.Scene()
|
||||||
|
studioCriterion := models.HierarchicalMultiCriterionInput{
|
||||||
|
Value: []string{
|
||||||
|
strconv.Itoa(studioIDs[studioIdxWithGrandChild]),
|
||||||
|
},
|
||||||
|
Modifier: models.CriterionModifierIncludes,
|
||||||
|
Depth: 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
sceneFilter := models.SceneFilterType{
|
||||||
|
Studios: &studioCriterion,
|
||||||
|
}
|
||||||
|
|
||||||
|
scenes := queryScene(t, sqb, &sceneFilter, nil)
|
||||||
|
assert.Len(t, scenes, 1)
|
||||||
|
|
||||||
|
studioCriterion.Depth = 1
|
||||||
|
|
||||||
|
scenes = queryScene(t, sqb, &sceneFilter, nil)
|
||||||
|
assert.Len(t, scenes, 0)
|
||||||
|
|
||||||
|
studioCriterion.Value = []string{strconv.Itoa(studioIDs[studioIdxWithParentAndChild])}
|
||||||
|
scenes = queryScene(t, sqb, &sceneFilter, nil)
|
||||||
|
assert.Len(t, scenes, 1)
|
||||||
|
|
||||||
|
// ensure id is correct
|
||||||
|
assert.Equal(t, sceneIDs[sceneIdxWithGrandChildStudio], scenes[0].ID)
|
||||||
|
|
||||||
|
studioCriterion = models.HierarchicalMultiCriterionInput{
|
||||||
|
Value: []string{
|
||||||
|
strconv.Itoa(studioIDs[studioIdxWithGrandChild]),
|
||||||
|
},
|
||||||
|
Modifier: models.CriterionModifierExcludes,
|
||||||
|
Depth: 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
q := getSceneStringValue(sceneIdxWithGrandChildStudio, titleField)
|
||||||
|
findFilter := models.FindFilterType{
|
||||||
|
Q: &q,
|
||||||
|
}
|
||||||
|
|
||||||
|
scenes = queryScene(t, sqb, &sceneFilter, &findFilter)
|
||||||
|
assert.Len(t, scenes, 0)
|
||||||
|
|
||||||
|
studioCriterion.Depth = 1
|
||||||
|
scenes = queryScene(t, sqb, &sceneFilter, &findFilter)
|
||||||
|
assert.Len(t, scenes, 1)
|
||||||
|
|
||||||
|
studioCriterion.Value = []string{strconv.Itoa(studioIDs[studioIdxWithParentAndChild])}
|
||||||
|
scenes = queryScene(t, sqb, &sceneFilter, &findFilter)
|
||||||
|
assert.Len(t, scenes, 0)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestSceneQueryMovies(t *testing.T) {
|
func TestSceneQueryMovies(t *testing.T) {
|
||||||
withTxn(func(r models.Repository) error {
|
withTxn(func(r models.Repository) error {
|
||||||
sqb := r.Scene()
|
sqb := r.Scene()
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ const (
|
|||||||
sceneIdxWithPerformerTwoTags
|
sceneIdxWithPerformerTwoTags
|
||||||
sceneIdxWithSpacedName
|
sceneIdxWithSpacedName
|
||||||
sceneIdxWithStudioPerformer
|
sceneIdxWithStudioPerformer
|
||||||
|
sceneIdxWithGrandChildStudio
|
||||||
// new indexes above
|
// new indexes above
|
||||||
lastSceneIdx
|
lastSceneIdx
|
||||||
|
|
||||||
@@ -65,6 +66,7 @@ const (
|
|||||||
imageIdxInZip // TODO - not implemented
|
imageIdxInZip // TODO - not implemented
|
||||||
imageIdxWithPerformerTag
|
imageIdxWithPerformerTag
|
||||||
imageIdxWithPerformerTwoTags
|
imageIdxWithPerformerTwoTags
|
||||||
|
imageIdxWithGrandChildStudio
|
||||||
// new indexes above
|
// new indexes above
|
||||||
totalImages
|
totalImages
|
||||||
)
|
)
|
||||||
@@ -125,6 +127,7 @@ const (
|
|||||||
galleryIdxWithPerformerTag
|
galleryIdxWithPerformerTag
|
||||||
galleryIdxWithPerformerTwoTags
|
galleryIdxWithPerformerTwoTags
|
||||||
galleryIdxWithStudioPerformer
|
galleryIdxWithStudioPerformer
|
||||||
|
galleryIdxWithGrandChildStudio
|
||||||
// new indexes above
|
// new indexes above
|
||||||
lastGalleryIdx
|
lastGalleryIdx
|
||||||
|
|
||||||
@@ -169,6 +172,9 @@ const (
|
|||||||
studioIdxWithScenePerformer
|
studioIdxWithScenePerformer
|
||||||
studioIdxWithImagePerformer
|
studioIdxWithImagePerformer
|
||||||
studioIdxWithGalleryPerformer
|
studioIdxWithGalleryPerformer
|
||||||
|
studioIdxWithGrandChild
|
||||||
|
studioIdxWithParentAndChild
|
||||||
|
studioIdxWithGrandParent
|
||||||
// new indexes above
|
// new indexes above
|
||||||
// studios with dup names start from the end
|
// studios with dup names start from the end
|
||||||
studioIdxWithDupName
|
studioIdxWithDupName
|
||||||
@@ -241,6 +247,7 @@ var (
|
|||||||
{sceneIdx1WithStudio, studioIdxWithTwoScenes},
|
{sceneIdx1WithStudio, studioIdxWithTwoScenes},
|
||||||
{sceneIdx2WithStudio, studioIdxWithTwoScenes},
|
{sceneIdx2WithStudio, studioIdxWithTwoScenes},
|
||||||
{sceneIdxWithStudioPerformer, studioIdxWithScenePerformer},
|
{sceneIdxWithStudioPerformer, studioIdxWithScenePerformer},
|
||||||
|
{sceneIdxWithGrandChildStudio, studioIdxWithGrandParent},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -257,6 +264,7 @@ var (
|
|||||||
{imageIdx1WithStudio, studioIdxWithTwoImages},
|
{imageIdx1WithStudio, studioIdxWithTwoImages},
|
||||||
{imageIdx2WithStudio, studioIdxWithTwoImages},
|
{imageIdx2WithStudio, studioIdxWithTwoImages},
|
||||||
{imageIdxWithStudioPerformer, studioIdxWithImagePerformer},
|
{imageIdxWithStudioPerformer, studioIdxWithImagePerformer},
|
||||||
|
{imageIdxWithGrandChildStudio, studioIdxWithGrandParent},
|
||||||
}
|
}
|
||||||
imageTagLinks = [][2]int{
|
imageTagLinks = [][2]int{
|
||||||
{imageIdxWithTag, tagIdxWithImage},
|
{imageIdxWithTag, tagIdxWithImage},
|
||||||
@@ -292,6 +300,7 @@ var (
|
|||||||
{galleryIdx1WithStudio, studioIdxWithTwoGalleries},
|
{galleryIdx1WithStudio, studioIdxWithTwoGalleries},
|
||||||
{galleryIdx2WithStudio, studioIdxWithTwoGalleries},
|
{galleryIdx2WithStudio, studioIdxWithTwoGalleries},
|
||||||
{galleryIdxWithStudioPerformer, studioIdxWithGalleryPerformer},
|
{galleryIdxWithStudioPerformer, studioIdxWithGalleryPerformer},
|
||||||
|
{galleryIdxWithGrandChildStudio, studioIdxWithGrandParent},
|
||||||
}
|
}
|
||||||
|
|
||||||
galleryTagLinks = [][2]int{
|
galleryTagLinks = [][2]int{
|
||||||
@@ -310,6 +319,8 @@ var (
|
|||||||
var (
|
var (
|
||||||
studioParentLinks = [][2]int{
|
studioParentLinks = [][2]int{
|
||||||
{studioIdxWithChildStudio, studioIdxWithParentStudio},
|
{studioIdxWithChildStudio, studioIdxWithParentStudio},
|
||||||
|
{studioIdxWithGrandChild, studioIdxWithParentAndChild},
|
||||||
|
{studioIdxWithParentAndChild, studioIdxWithGrandParent},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -359,9 +359,10 @@ func verifyStudiosImageCount(t *testing.T, imageCountCriterion models.IntCriteri
|
|||||||
pp := 0
|
pp := 0
|
||||||
|
|
||||||
_, count, err := r.Image().Query(&models.ImageFilterType{
|
_, count, err := r.Image().Query(&models.ImageFilterType{
|
||||||
Studios: &models.MultiCriterionInput{
|
Studios: &models.HierarchicalMultiCriterionInput{
|
||||||
Value: []string{strconv.Itoa(studio.ID)},
|
Value: []string{strconv.Itoa(studio.ID)},
|
||||||
Modifier: models.CriterionModifierIncludes,
|
Modifier: models.CriterionModifierIncludes,
|
||||||
|
Depth: 0,
|
||||||
},
|
},
|
||||||
}, &models.FindFilterType{
|
}, &models.FindFilterType{
|
||||||
PerPage: &pp,
|
PerPage: &pp,
|
||||||
@@ -409,9 +410,10 @@ func verifyStudiosGalleryCount(t *testing.T, galleryCountCriterion models.IntCri
|
|||||||
pp := 0
|
pp := 0
|
||||||
|
|
||||||
_, count, err := r.Gallery().Query(&models.GalleryFilterType{
|
_, count, err := r.Gallery().Query(&models.GalleryFilterType{
|
||||||
Studios: &models.MultiCriterionInput{
|
Studios: &models.HierarchicalMultiCriterionInput{
|
||||||
Value: []string{strconv.Itoa(studio.ID)},
|
Value: []string{strconv.Itoa(studio.ID)},
|
||||||
Modifier: models.CriterionModifierIncludes,
|
Modifier: models.CriterionModifierIncludes,
|
||||||
|
Depth: 0,
|
||||||
},
|
},
|
||||||
}, &models.FindFilterType{
|
}, &models.FindFilterType{
|
||||||
PerPage: &pp,
|
PerPage: &pp,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
### ✨ New Features
|
### ✨ New Features
|
||||||
|
* Support Studio filter including child studios. ([#1397](https://github.com/stashapp/stash/pull/1397))
|
||||||
* Added support for tag aliases. ([#1412](https://github.com/stashapp/stash/pull/1412))
|
* Added support for tag aliases. ([#1412](https://github.com/stashapp/stash/pull/1412))
|
||||||
* Support embedded Javascript plugins. ([#1393](https://github.com/stashapp/stash/pull/1393))
|
* Support embedded Javascript plugins. ([#1393](https://github.com/stashapp/stash/pull/1393))
|
||||||
* Revamped job management: tasks can now be queued. ([#1379](https://github.com/stashapp/stash/pull/1379))
|
* Revamped job management: tasks can now be queued. ([#1379](https://github.com/stashapp/stash/pull/1379))
|
||||||
|
|||||||
@@ -8,12 +8,16 @@ import {
|
|||||||
DurationCriterion,
|
DurationCriterion,
|
||||||
CriterionValue,
|
CriterionValue,
|
||||||
Criterion,
|
Criterion,
|
||||||
|
IHierarchicalLabeledIdCriterion,
|
||||||
} from "src/models/list-filter/criteria/criterion";
|
} from "src/models/list-filter/criteria/criterion";
|
||||||
import { NoneCriterion } from "src/models/list-filter/criteria/none";
|
import { NoneCriterion } from "src/models/list-filter/criteria/none";
|
||||||
import { makeCriteria } from "src/models/list-filter/criteria/factory";
|
import { makeCriteria } from "src/models/list-filter/criteria/factory";
|
||||||
import { ListFilterOptions } from "src/models/list-filter/filter-options";
|
import { ListFilterOptions } from "src/models/list-filter/filter-options";
|
||||||
import { useIntl } from "react-intl";
|
import { defineMessages, useIntl } from "react-intl";
|
||||||
import { CriterionType } from "src/models/list-filter/types";
|
import {
|
||||||
|
criterionIsHierarchicalLabelValue,
|
||||||
|
CriterionType,
|
||||||
|
} from "src/models/list-filter/types";
|
||||||
|
|
||||||
interface IAddFilterProps {
|
interface IAddFilterProps {
|
||||||
onAddCriterion: (
|
onAddCriterion: (
|
||||||
@@ -39,6 +43,13 @@ export const AddFilter: React.FC<IAddFilterProps> = (
|
|||||||
|
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
studio_depth: {
|
||||||
|
id: "studio_depth",
|
||||||
|
defaultMessage: "Levels (empty for all)",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// configure keyboard shortcuts
|
// configure keyboard shortcuts
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
Mousetrap.bind("f", () => setIsOpen(true));
|
Mousetrap.bind("f", () => setIsOpen(true));
|
||||||
@@ -183,7 +194,29 @@ export const AddFilter: React.FC<IAddFilterProps> = (
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (criterion.options) {
|
if (criterion instanceof IHierarchicalLabeledIdCriterion) {
|
||||||
|
if (criterion.criterionOption.value !== "studios") return;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FilterSelect
|
||||||
|
type={criterion.criterionOption.value}
|
||||||
|
isMulti
|
||||||
|
onSelect={(items) => {
|
||||||
|
const newCriterion = _.cloneDeep(criterion);
|
||||||
|
newCriterion.value.items = items.map((i) => ({
|
||||||
|
id: i.id,
|
||||||
|
label: i.name!,
|
||||||
|
}));
|
||||||
|
setCriterion(newCriterion);
|
||||||
|
}}
|
||||||
|
ids={criterion.value.items.map((labeled) => labeled.id)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
criterion.options &&
|
||||||
|
!criterionIsHierarchicalLabelValue(criterion.value)
|
||||||
|
) {
|
||||||
defaultValue.current = criterion.value;
|
defaultValue.current = criterion.value;
|
||||||
return (
|
return (
|
||||||
<Form.Control
|
<Form.Control
|
||||||
@@ -219,10 +252,53 @@ export const AddFilter: React.FC<IAddFilterProps> = (
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
function renderAdditional() {
|
||||||
|
if (criterion instanceof IHierarchicalLabeledIdCriterion) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Form.Group>
|
||||||
|
<Form.Check
|
||||||
|
checked={criterion.value.depth !== 0}
|
||||||
|
label="Include child studios"
|
||||||
|
onChange={() => {
|
||||||
|
const newCriterion = _.cloneDeep(criterion);
|
||||||
|
newCriterion.value.depth =
|
||||||
|
newCriterion.value.depth !== 0 ? 0 : -1;
|
||||||
|
setCriterion(newCriterion);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
{criterion.value.depth !== 0 && (
|
||||||
|
<Form.Group>
|
||||||
|
<Form.Control
|
||||||
|
className="btn-secondary"
|
||||||
|
type="number"
|
||||||
|
placeholder={intl.formatMessage(messages.studio_depth)}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newCriterion = _.cloneDeep(criterion);
|
||||||
|
newCriterion.value.depth = e.target.value
|
||||||
|
? parseInt(e.target.value, 10)
|
||||||
|
: -1;
|
||||||
|
setCriterion(newCriterion);
|
||||||
|
}}
|
||||||
|
defaultValue={
|
||||||
|
criterion.value && criterion.value.depth !== -1
|
||||||
|
? criterion.value.depth
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
min="1"
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Form.Group>{renderModifier()}</Form.Group>
|
<Form.Group>{renderModifier()}</Form.Group>
|
||||||
<Form.Group>{renderSelect()}</Form.Group>
|
<Form.Group>{renderSelect()}</Form.Group>
|
||||||
|
{renderAdditional()}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,9 +12,10 @@ export const StudioPerformersPanel: React.FC<IStudioPerformersPanel> = ({
|
|||||||
studio,
|
studio,
|
||||||
}) => {
|
}) => {
|
||||||
const studioCriterion = new StudiosCriterion();
|
const studioCriterion = new StudiosCriterion();
|
||||||
studioCriterion.value = [
|
studioCriterion.value = {
|
||||||
{ id: studio.id!, label: studio.name || `Studio ${studio.id}` },
|
items: [{ id: studio.id!, label: studio.name || `Studio ${studio.id}` }],
|
||||||
];
|
depth: 0,
|
||||||
|
};
|
||||||
|
|
||||||
const extraCriteria = {
|
const extraCriteria = {
|
||||||
scenes: [studioCriterion],
|
scenes: [studioCriterion],
|
||||||
|
|||||||
@@ -17,18 +17,21 @@ export const studioFilterHook = (studio: Partial<GQL.StudioDataFragment>) => {
|
|||||||
) {
|
) {
|
||||||
// add the studio if not present
|
// add the studio if not present
|
||||||
if (
|
if (
|
||||||
!studioCriterion.value.find((p) => {
|
!studioCriterion.value.items.find((p) => {
|
||||||
return p.id === studio.id;
|
return p.id === studio.id;
|
||||||
})
|
})
|
||||||
) {
|
) {
|
||||||
studioCriterion.value.push(studioValue);
|
studioCriterion.value.items.push(studioValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
studioCriterion.modifier = GQL.CriterionModifier.IncludesAll;
|
studioCriterion.modifier = GQL.CriterionModifier.IncludesAll;
|
||||||
} else {
|
} else {
|
||||||
// overwrite
|
// overwrite
|
||||||
studioCriterion = new StudiosCriterion();
|
studioCriterion = new StudiosCriterion();
|
||||||
studioCriterion.value = [studioValue];
|
studioCriterion.value = {
|
||||||
|
items: [studioValue],
|
||||||
|
depth: 0,
|
||||||
|
};
|
||||||
filter.criteria.push(studioCriterion);
|
filter.criteria.push(studioCriterion);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -73,6 +73,7 @@
|
|||||||
"scenes_updated_at": "Scene Updated At",
|
"scenes_updated_at": "Scene Updated At",
|
||||||
"seconds": "Seconds",
|
"seconds": "Seconds",
|
||||||
"stash_id": "Stash ID",
|
"stash_id": "Stash ID",
|
||||||
|
"studio_depth": "Levels (empty for all)",
|
||||||
"studios": "Studios",
|
"studios": "Studios",
|
||||||
"tag_count": "Tag Count",
|
"tag_count": "Tag Count",
|
||||||
"tags": "Tags",
|
"tags": "Tags",
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { IntlShape } from "react-intl";
|
import { IntlShape } from "react-intl";
|
||||||
import {
|
import {
|
||||||
CriterionModifier,
|
CriterionModifier,
|
||||||
|
HierarchicalMultiCriterionInput,
|
||||||
MultiCriterionInput,
|
MultiCriterionInput,
|
||||||
} from "src/core/generated-graphql";
|
} from "src/core/generated-graphql";
|
||||||
import DurationUtils from "src/utils/duration";
|
import DurationUtils from "src/utils/duration";
|
||||||
@@ -12,10 +13,15 @@ import {
|
|||||||
ILabeledId,
|
ILabeledId,
|
||||||
ILabeledValue,
|
ILabeledValue,
|
||||||
IOptionType,
|
IOptionType,
|
||||||
|
IHierarchicalLabelValue,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
|
|
||||||
type Option = string | number | IOptionType;
|
type Option = string | number | IOptionType;
|
||||||
export type CriterionValue = string | number | ILabeledId[];
|
export type CriterionValue =
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| ILabeledId[]
|
||||||
|
| IHierarchicalLabelValue;
|
||||||
|
|
||||||
// V = criterion value type
|
// V = criterion value type
|
||||||
export abstract class Criterion<V extends CriterionValue> {
|
export abstract class Criterion<V extends CriterionValue> {
|
||||||
@@ -305,6 +311,69 @@ export abstract class ILabeledIdCriterion extends Criterion<ILabeledId[]> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
|
||||||
|
public encodeValue() {
|
||||||
|
return {
|
||||||
|
items: this.value.items.map((o) => {
|
||||||
|
return encodeILabeledId(o);
|
||||||
|
}),
|
||||||
|
depth: this.value.depth,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected toCriterionInput(): HierarchicalMultiCriterionInput {
|
||||||
|
return {
|
||||||
|
value: this.value.items.map((v) => v.id),
|
||||||
|
modifier: this.modifier,
|
||||||
|
depth: this.value.depth,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public getLabelValue(): string {
|
||||||
|
const labels = this.value.items.map((v) => v.label).join(", ");
|
||||||
|
|
||||||
|
if (this.value.depth === 0) {
|
||||||
|
return labels;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${labels} (+${this.value.depth > 0 ? this.value.depth : "all"})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
public toJSON() {
|
||||||
|
const encodedCriterion = {
|
||||||
|
type: this.criterionOption.value,
|
||||||
|
value: this.encodeValue(),
|
||||||
|
modifier: this.modifier,
|
||||||
|
};
|
||||||
|
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),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class MandatoryNumberCriterion extends NumberCriterion {
|
export class MandatoryNumberCriterion extends NumberCriterion {
|
||||||
public modifierOptions = [
|
public modifierOptions = [
|
||||||
Criterion.getModifierOption(CriterionModifier.Equals),
|
Criterion.getModifierOption(CriterionModifier.Equals),
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
import { CriterionOption, ILabeledIdCriterion } from "./criterion";
|
import {
|
||||||
|
CriterionOption,
|
||||||
abstract class AbstractStudiosCriterion extends ILabeledIdCriterion {
|
IHierarchicalLabeledIdCriterion,
|
||||||
constructor(type: CriterionOption) {
|
ILabeledIdCriterion,
|
||||||
super(type, false);
|
} from "./criterion";
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const StudiosCriterionOption = new CriterionOption("studios", "studios");
|
export const StudiosCriterionOption = new CriterionOption("studios", "studios");
|
||||||
export class StudiosCriterion extends AbstractStudiosCriterion {
|
export class StudiosCriterion extends IHierarchicalLabeledIdCriterion {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(StudiosCriterionOption);
|
super(StudiosCriterionOption, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -18,8 +16,8 @@ export const ParentStudiosCriterionOption = new CriterionOption(
|
|||||||
"parent_studios",
|
"parent_studios",
|
||||||
"parents"
|
"parents"
|
||||||
);
|
);
|
||||||
export class ParentStudiosCriterion extends AbstractStudiosCriterion {
|
export class ParentStudiosCriterion extends ILabeledIdCriterion {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(ParentStudiosCriterionOption);
|
super(ParentStudiosCriterionOption, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,18 @@ export interface ILabeledValue {
|
|||||||
value: string;
|
value: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IHierarchicalLabelValue {
|
||||||
|
items: ILabeledId[];
|
||||||
|
depth: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function criterionIsHierarchicalLabelValue(
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
value: any
|
||||||
|
): value is IHierarchicalLabelValue {
|
||||||
|
return typeof value === "object" && "items" in value && "depth" in value;
|
||||||
|
}
|
||||||
|
|
||||||
export function encodeILabeledId(o: ILabeledId) {
|
export function encodeILabeledId(o: ILabeledId) {
|
||||||
// escape " and \ and by encoding to JSON so that it encodes to JSON correctly down the line
|
// escape " and \ and by encoding to JSON so that it encodes to JSON correctly down the line
|
||||||
const adjustedLabel = JSON.stringify(o.label).slice(1, -1);
|
const adjustedLabel = JSON.stringify(o.label).slice(1, -1);
|
||||||
|
|||||||
@@ -85,9 +85,10 @@ const makeStudioScenesUrl = (studio: Partial<GQL.StudioDataFragment>) => {
|
|||||||
if (!studio.id) return "#";
|
if (!studio.id) return "#";
|
||||||
const filter = new ListFilterModel();
|
const filter = new ListFilterModel();
|
||||||
const criterion = new StudiosCriterion();
|
const criterion = new StudiosCriterion();
|
||||||
criterion.value = [
|
criterion.value = {
|
||||||
{ id: studio.id, label: studio.name || `Studio ${studio.id}` },
|
items: [{ id: studio.id, label: studio.name || `Studio ${studio.id}` }],
|
||||||
];
|
depth: 0,
|
||||||
|
};
|
||||||
filter.criteria.push(criterion);
|
filter.criteria.push(criterion);
|
||||||
return `/scenes?${filter.makeQueryParameters()}`;
|
return `/scenes?${filter.makeQueryParameters()}`;
|
||||||
};
|
};
|
||||||
@@ -96,9 +97,10 @@ const makeStudioImagesUrl = (studio: Partial<GQL.StudioDataFragment>) => {
|
|||||||
if (!studio.id) return "#";
|
if (!studio.id) return "#";
|
||||||
const filter = new ListFilterModel();
|
const filter = new ListFilterModel();
|
||||||
const criterion = new StudiosCriterion();
|
const criterion = new StudiosCriterion();
|
||||||
criterion.value = [
|
criterion.value = {
|
||||||
{ id: studio.id, label: studio.name || `Studio ${studio.id}` },
|
items: [{ id: studio.id, label: studio.name || `Studio ${studio.id}` }],
|
||||||
];
|
depth: 0,
|
||||||
|
};
|
||||||
filter.criteria.push(criterion);
|
filter.criteria.push(criterion);
|
||||||
return `/images?${filter.makeQueryParameters()}`;
|
return `/images?${filter.makeQueryParameters()}`;
|
||||||
};
|
};
|
||||||
@@ -107,9 +109,10 @@ const makeStudioGalleriesUrl = (studio: Partial<GQL.StudioDataFragment>) => {
|
|||||||
if (!studio.id) return "#";
|
if (!studio.id) return "#";
|
||||||
const filter = new ListFilterModel();
|
const filter = new ListFilterModel();
|
||||||
const criterion = new StudiosCriterion();
|
const criterion = new StudiosCriterion();
|
||||||
criterion.value = [
|
criterion.value = {
|
||||||
{ id: studio.id, label: studio.name || `Studio ${studio.id}` },
|
items: [{ id: studio.id, label: studio.name || `Studio ${studio.id}` }],
|
||||||
];
|
depth: 0,
|
||||||
|
};
|
||||||
filter.criteria.push(criterion);
|
filter.criteria.push(criterion);
|
||||||
return `/galleries?${filter.makeQueryParameters()}`;
|
return `/galleries?${filter.makeQueryParameters()}`;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user