From db06eae7cbc0af08b4e7be821eeb1090db6b7a77 Mon Sep 17 00:00:00 2001 From: its-josh4 <74079536+its-josh4@users.noreply.github.com> Date: Tue, 1 Apr 2025 20:26:14 -0700 Subject: [PATCH] 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> --- internal/api/resolver_query_find_tag.go | 6 +++++- internal/api/resolver_query_scraper.go | 5 +++++ pkg/models/model_scraped_item.go | 5 +++++ pkg/scraper/tag.go | 2 ++ pkg/sqlite/tag.go | 2 +- pkg/stashbox/graphql/generated_models.go | 7 ++++++- ui/v2.5/src/components/Tags/TagSelect.tsx | 20 ++++++++++++++++++-- 7 files changed, 42 insertions(+), 5 deletions(-) diff --git a/internal/api/resolver_query_find_tag.go b/internal/api/resolver_query_find_tag.go index b157cb329..f0e1d8b97 100644 --- a/internal/api/resolver_query_find_tag.go +++ b/internal/api/resolver_query_find_tag.go @@ -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) { if err := r.withReadTxn(ctx, func(ctx context.Context) error { ret, err = r.repository.Tag.All(ctx) - return err + if err != nil { + return err + } + + return nil }); err != nil { return nil, err } diff --git a/internal/api/resolver_query_scraper.go b/internal/api/resolver_query_scraper.go index e70b18650..cbede59a7 100644 --- a/internal/api/resolver_query_scraper.go +++ b/internal/api/resolver_query_scraper.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "slices" "strconv" "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) } + for i := range ret { + slices.SortFunc(ret[i].Tags, models.ScrapedTagSortFunction) + } + return ret, nil } diff --git a/pkg/models/model_scraped_item.go b/pkg/models/model_scraped_item.go index c3f686a60..008a05c3d 100644 --- a/pkg/models/model_scraped_item.go +++ b/pkg/models/model_scraped_item.go @@ -3,6 +3,7 @@ package models import ( "context" "strconv" + "strings" "time" "github.com/stashapp/stash/pkg/sliceutil/stringslice" @@ -400,6 +401,10 @@ type ScrapedTag struct { 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... type ScrapedMovie struct { StoredID *string `json:"stored_id"` diff --git a/pkg/scraper/tag.go b/pkg/scraper/tag.go index 9a5f16088..c26aa855e 100644 --- a/pkg/scraper/tag.go +++ b/pkg/scraper/tag.go @@ -32,6 +32,8 @@ func FilterTags(excludeRegexps []*regexp.Regexp, tags []*models.ScrapedTag) (new return tags, nil } + newTags = make([]*models.ScrapedTag, 0, len(tags)) + for _, t := range tags { ignore := false for _, reg := range excludeRegexps { diff --git a/pkg/sqlite/tag.go b/pkg/sqlite/tag.go index 00450085d..f690220a7 100644 --- a/pkg/sqlite/tag.go +++ b/pkg/sqlite/tag.go @@ -562,7 +562,7 @@ func (qb *TagStore) All(ctx context.Context) ([]*models.Tag, error) { table := qb.table() 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(), )) } diff --git a/pkg/stashbox/graphql/generated_models.go b/pkg/stashbox/graphql/generated_models.go index 6802c61f5..3cba902c1 100644 --- a/pkg/stashbox/graphql/generated_models.go +++ b/pkg/stashbox/graphql/generated_models.go @@ -1018,6 +1018,7 @@ type StashBoxConfig struct { GuidelinesURL string `json:"guidelines_url"` RequireSceneDraft bool `json:"require_scene_draft"` EditUpdateLimit int `json:"edit_update_limit"` + RequireTagRole bool `json:"require_tag_role"` } type StringCriterionInput struct { @@ -2143,6 +2144,8 @@ const ( // May grant and rescind invite tokens and resind invite keys RoleEnumManageInvites RoleEnum = "MANAGE_INVITES" RoleEnumBot RoleEnum = "BOT" + RoleEnumReadOnly RoleEnum = "READ_ONLY" + RoleEnumEditTags RoleEnum = "EDIT_TAGS" ) var AllRoleEnum = []RoleEnum{ @@ -2154,11 +2157,13 @@ var AllRoleEnum = []RoleEnum{ RoleEnumInvite, RoleEnumManageInvites, RoleEnumBot, + RoleEnumReadOnly, + RoleEnumEditTags, } func (e RoleEnum) IsValid() bool { 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 false diff --git a/ui/v2.5/src/components/Tags/TagSelect.tsx b/ui/v2.5/src/components/Tags/TagSelect.tsx index b93270e68..2f6fb9a3e 100644 --- a/ui/v2.5/src/components/Tags/TagSelect.tsx +++ b/ui/v2.5/src/components/Tags/TagSelect.tsx @@ -36,7 +36,10 @@ export type SelectObject = { title?: string | null; }; -export type Tag = Pick; +export type Tag = Pick< + GQL.Tag, + "id" | "name" | "sort_name" | "aliases" | "image_path" +>; type Option = SelectOption; type FindTagsResult = Awaited< @@ -293,7 +296,20 @@ const _TagIDSelect: React.FC> = (props) => { const load = async () => { 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();