Sort tags by name while scraping or merging scenes (#5752)

* Sort tags by name while scraping scenes
* TagStore.All should sort by sort_name first
* Sort tag by sort name/name in TagIDSelect
---------
Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
This commit is contained in:
its-josh4 2025-04-01 20:26:14 -07:00 committed by GitHub
parent 0f2bc3e01d
commit db06eae7cb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 42 additions and 5 deletions

View file

@ -62,7 +62,11 @@ func (r *queryResolver) FindTags(ctx context.Context, tagFilter *models.TagFilte
func (r *queryResolver) AllTags(ctx context.Context) (ret []*models.Tag, err error) { func (r *queryResolver) AllTags(ctx context.Context) (ret []*models.Tag, err error) {
if err := r.withReadTxn(ctx, func(ctx context.Context) error { if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Tag.All(ctx) ret, err = r.repository.Tag.All(ctx)
if err != nil {
return err return err
}
return nil
}); err != nil { }); err != nil {
return nil, err return nil, err
} }

View file

@ -4,6 +4,7 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"slices"
"strconv" "strconv"
"github.com/stashapp/stash/pkg/match" "github.com/stashapp/stash/pkg/match"
@ -207,6 +208,10 @@ func (r *queryResolver) ScrapeSingleScene(ctx context.Context, source scraper.So
return nil, fmt.Errorf("%w: scraper_id or stash_box_index must be set", ErrInput) return nil, fmt.Errorf("%w: scraper_id or stash_box_index must be set", ErrInput)
} }
for i := range ret {
slices.SortFunc(ret[i].Tags, models.ScrapedTagSortFunction)
}
return ret, nil return ret, nil
} }

View file

@ -3,6 +3,7 @@ package models
import ( import (
"context" "context"
"strconv" "strconv"
"strings"
"time" "time"
"github.com/stashapp/stash/pkg/sliceutil/stringslice" "github.com/stashapp/stash/pkg/sliceutil/stringslice"
@ -400,6 +401,10 @@ type ScrapedTag struct {
func (ScrapedTag) IsScrapedContent() {} func (ScrapedTag) IsScrapedContent() {}
func ScrapedTagSortFunction(a, b *ScrapedTag) int {
return strings.Compare(strings.ToLower(a.Name), strings.ToLower(b.Name))
}
// A movie from a scraping operation... // A movie from a scraping operation...
type ScrapedMovie struct { type ScrapedMovie struct {
StoredID *string `json:"stored_id"` StoredID *string `json:"stored_id"`

View file

@ -32,6 +32,8 @@ func FilterTags(excludeRegexps []*regexp.Regexp, tags []*models.ScrapedTag) (new
return tags, nil return tags, nil
} }
newTags = make([]*models.ScrapedTag, 0, len(tags))
for _, t := range tags { for _, t := range tags {
ignore := false ignore := false
for _, reg := range excludeRegexps { for _, reg := range excludeRegexps {

View file

@ -562,7 +562,7 @@ func (qb *TagStore) All(ctx context.Context) ([]*models.Tag, error) {
table := qb.table() table := qb.table()
return qb.getMany(ctx, qb.selectDataset().Order( return qb.getMany(ctx, qb.selectDataset().Order(
table.Col("name").Asc(), goqu.L("COALESCE(tags.sort_name, tags.name) COLLATE NATURAL_CI").Asc(),
table.Col(idColumn).Asc(), table.Col(idColumn).Asc(),
)) ))
} }

View file

@ -1018,6 +1018,7 @@ type StashBoxConfig struct {
GuidelinesURL string `json:"guidelines_url"` GuidelinesURL string `json:"guidelines_url"`
RequireSceneDraft bool `json:"require_scene_draft"` RequireSceneDraft bool `json:"require_scene_draft"`
EditUpdateLimit int `json:"edit_update_limit"` EditUpdateLimit int `json:"edit_update_limit"`
RequireTagRole bool `json:"require_tag_role"`
} }
type StringCriterionInput struct { type StringCriterionInput struct {
@ -2143,6 +2144,8 @@ const (
// May grant and rescind invite tokens and resind invite keys // May grant and rescind invite tokens and resind invite keys
RoleEnumManageInvites RoleEnum = "MANAGE_INVITES" RoleEnumManageInvites RoleEnum = "MANAGE_INVITES"
RoleEnumBot RoleEnum = "BOT" RoleEnumBot RoleEnum = "BOT"
RoleEnumReadOnly RoleEnum = "READ_ONLY"
RoleEnumEditTags RoleEnum = "EDIT_TAGS"
) )
var AllRoleEnum = []RoleEnum{ var AllRoleEnum = []RoleEnum{
@ -2154,11 +2157,13 @@ var AllRoleEnum = []RoleEnum{
RoleEnumInvite, RoleEnumInvite,
RoleEnumManageInvites, RoleEnumManageInvites,
RoleEnumBot, RoleEnumBot,
RoleEnumReadOnly,
RoleEnumEditTags,
} }
func (e RoleEnum) IsValid() bool { func (e RoleEnum) IsValid() bool {
switch e { switch e {
case RoleEnumRead, RoleEnumVote, RoleEnumEdit, RoleEnumModify, RoleEnumAdmin, RoleEnumInvite, RoleEnumManageInvites, RoleEnumBot: case RoleEnumRead, RoleEnumVote, RoleEnumEdit, RoleEnumModify, RoleEnumAdmin, RoleEnumInvite, RoleEnumManageInvites, RoleEnumBot, RoleEnumReadOnly, RoleEnumEditTags:
return true return true
} }
return false return false

View file

@ -36,7 +36,10 @@ export type SelectObject = {
title?: string | null; title?: string | null;
}; };
export type Tag = Pick<GQL.Tag, "id" | "name" | "aliases" | "image_path">; export type Tag = Pick<
GQL.Tag,
"id" | "name" | "sort_name" | "aliases" | "image_path"
>;
type Option = SelectOption<Tag>; type Option = SelectOption<Tag>;
type FindTagsResult = Awaited< type FindTagsResult = Awaited<
@ -293,7 +296,20 @@ const _TagIDSelect: React.FC<IFilterProps & IFilterIDProps<Tag>> = (props) => {
const load = async () => { const load = async () => {
const items = await loadObjectsByID(ids); const items = await loadObjectsByID(ids);
setValues(items);
// #4684 - sort items by sort name/name
const sortedItems = [...items];
sortedItems.sort((a, b) => {
const aName = a.sort_name || a.name;
const bName = b.sort_name || b.name;
if (aName && bName) {
return aName.localeCompare(bName);
}
return 0;
});
setValues(sortedItems);
}; };
load(); load();