mirror of
https://github.com/stashapp/stash.git
synced 2025-12-16 20:07:05 +03:00
Add Sort Name to Tags (#5531)
* override "name" sort with COALESCE * tag sort_name frontend adds `data-sort-name` attribute to tag links prioritizes sort_name value but will default to tag name if not present in the same way that COALESCE will prioritize the same values in the same way * add sort_name filter, update locale per request * Include sort name in anonymiser * Add import/export support --------- Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
This commit is contained in:
@@ -11,6 +11,7 @@ import (
|
||||
|
||||
type Tag struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
SortName string `json:"sort_name,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Favorite bool `json:"favorite,omitempty"`
|
||||
Aliases []string `json:"aliases,omitempty"`
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
type Tag struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
SortName string `json:"sort_name"`
|
||||
Favorite bool `json:"favorite"`
|
||||
Description string `json:"description"`
|
||||
IgnoreAutoTag bool `json:"ignore_auto_tag"`
|
||||
@@ -47,6 +48,7 @@ func (s *Tag) LoadChildIDs(ctx context.Context, l TagRelationLoader) error {
|
||||
|
||||
type TagPartial struct {
|
||||
Name OptionalString
|
||||
SortName OptionalString
|
||||
Description OptionalString
|
||||
Favorite OptionalBool
|
||||
IgnoreAutoTag OptionalBool
|
||||
|
||||
@@ -4,6 +4,8 @@ type TagFilterType struct {
|
||||
OperatorFilter[TagFilterType]
|
||||
// Filter by tag name
|
||||
Name *StringCriterionInput `json:"name"`
|
||||
// Filter by tag sort_name
|
||||
SortName *StringCriterionInput `json:"sort_name"`
|
||||
// Filter by tag aliases
|
||||
Aliases *StringCriterionInput `json:"aliases"`
|
||||
// Filter by tag favorites
|
||||
|
||||
@@ -816,6 +816,7 @@ func (db *Anonymiser) anonymiseTags(ctx context.Context) error {
|
||||
query := dialect.From(table).Select(
|
||||
table.Col(idColumn),
|
||||
table.Col("name"),
|
||||
table.Col("sort_name"),
|
||||
table.Col("description"),
|
||||
).Where(table.Col(idColumn).Gt(lastID)).Limit(1000)
|
||||
|
||||
@@ -826,12 +827,14 @@ func (db *Anonymiser) anonymiseTags(ctx context.Context) error {
|
||||
var (
|
||||
id int
|
||||
name sql.NullString
|
||||
sortName sql.NullString
|
||||
description sql.NullString
|
||||
)
|
||||
|
||||
if err := rows.Scan(
|
||||
&id,
|
||||
&name,
|
||||
&sortName,
|
||||
&description,
|
||||
); err != nil {
|
||||
return err
|
||||
@@ -839,6 +842,7 @@ func (db *Anonymiser) anonymiseTags(ctx context.Context) error {
|
||||
|
||||
set := goqu.Record{}
|
||||
db.obfuscateNullString(set, "name", name)
|
||||
db.obfuscateNullString(set, "sort_name", sortName)
|
||||
db.obfuscateNullString(set, "description", description)
|
||||
|
||||
if len(set) > 0 {
|
||||
|
||||
@@ -34,7 +34,7 @@ const (
|
||||
cacheSizeEnv = "STASH_SQLITE_CACHE_SIZE"
|
||||
)
|
||||
|
||||
var appSchemaVersion uint = 71
|
||||
var appSchemaVersion uint = 72
|
||||
|
||||
//go:embed migrations/*.sql
|
||||
var migrationsBox embed.FS
|
||||
|
||||
@@ -155,7 +155,7 @@ var (
|
||||
},
|
||||
fkColumn: "tag_id",
|
||||
foreignTable: tagTable,
|
||||
orderBy: "tags.name ASC",
|
||||
orderBy: "COALESCE(tags.sort_name, tags.name) ASC",
|
||||
},
|
||||
images: joinRepository{
|
||||
repository: repository{
|
||||
|
||||
@@ -122,7 +122,7 @@ var (
|
||||
},
|
||||
fkColumn: tagIDColumn,
|
||||
foreignTable: tagTable,
|
||||
orderBy: "tags.name ASC",
|
||||
orderBy: "COALESCE(tags.sort_name, tags.name) ASC",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
@@ -177,7 +177,7 @@ var (
|
||||
},
|
||||
fkColumn: tagIDColumn,
|
||||
foreignTable: tagTable,
|
||||
orderBy: "tags.name ASC",
|
||||
orderBy: "COALESCE(tags.sort_name, tags.name) ASC",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
2
pkg/sqlite/migrations/72_tag_sort_name.up.sql
Normal file
2
pkg/sqlite/migrations/72_tag_sort_name.up.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE `tags` ADD COLUMN `sort_name` varchar(255);
|
||||
|
||||
@@ -189,7 +189,7 @@ var (
|
||||
},
|
||||
fkColumn: tagIDColumn,
|
||||
foreignTable: tagTable,
|
||||
orderBy: "tags.name ASC",
|
||||
orderBy: "COALESCE(tags.sort_name, tags.name) ASC",
|
||||
},
|
||||
stashIDs: stashIDRepository{
|
||||
repository{
|
||||
|
||||
@@ -201,7 +201,7 @@ var (
|
||||
},
|
||||
fkColumn: tagIDColumn,
|
||||
foreignTable: tagTable,
|
||||
orderBy: "tags.name ASC",
|
||||
orderBy: "COALESCE(tags.sort_name, tags.name) ASC",
|
||||
},
|
||||
performers: joinRepository{
|
||||
repository: repository{
|
||||
|
||||
@@ -133,7 +133,7 @@ var (
|
||||
},
|
||||
fkColumn: tagIDColumn,
|
||||
foreignTable: tagTable,
|
||||
orderBy: "tags.name ASC",
|
||||
orderBy: "COALESCE(tags.sort_name, tags.name) ASC",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
@@ -33,6 +33,7 @@ const (
|
||||
type tagRow struct {
|
||||
ID int `db:"id" goqu:"skipinsert"`
|
||||
Name null.String `db:"name"` // TODO: make schema non-nullable
|
||||
SortName zero.String `db:"sort_name"`
|
||||
Favorite bool `db:"favorite"`
|
||||
Description zero.String `db:"description"`
|
||||
IgnoreAutoTag bool `db:"ignore_auto_tag"`
|
||||
@@ -46,6 +47,7 @@ type tagRow struct {
|
||||
func (r *tagRow) fromTag(o models.Tag) {
|
||||
r.ID = o.ID
|
||||
r.Name = null.StringFrom(o.Name)
|
||||
r.SortName = zero.StringFrom((o.SortName))
|
||||
r.Favorite = o.Favorite
|
||||
r.Description = zero.StringFrom(o.Description)
|
||||
r.IgnoreAutoTag = o.IgnoreAutoTag
|
||||
@@ -57,6 +59,7 @@ func (r *tagRow) resolve() *models.Tag {
|
||||
ret := &models.Tag{
|
||||
ID: r.ID,
|
||||
Name: r.Name.String,
|
||||
SortName: r.SortName.String,
|
||||
Favorite: r.Favorite,
|
||||
Description: r.Description.String,
|
||||
IgnoreAutoTag: r.IgnoreAutoTag,
|
||||
@@ -87,6 +90,7 @@ type tagRowRecord struct {
|
||||
|
||||
func (r *tagRowRecord) fromPartial(o models.TagPartial) {
|
||||
r.setString("name", o.Name)
|
||||
r.setNullString("sort_name", o.SortName)
|
||||
r.setNullString("description", o.Description)
|
||||
r.setBool("favorite", o.Favorite)
|
||||
r.setBool("ignore_auto_tag", o.IgnoreAutoTag)
|
||||
@@ -672,6 +676,8 @@ func (qb *TagStore) getTagSort(query *queryBuilder, findFilter *models.FindFilte
|
||||
|
||||
sortQuery := ""
|
||||
switch sort {
|
||||
case "name":
|
||||
sortQuery += fmt.Sprintf(" ORDER BY COALESCE(tags.sort_name, tags.name) COLLATE NATURAL_CI %s", getSortDirection(direction))
|
||||
case "scenes_count":
|
||||
sortQuery += getCountSort(tagTable, scenesTagsTable, tagIDColumn, direction)
|
||||
case "scene_markers_count":
|
||||
@@ -690,8 +696,8 @@ func (qb *TagStore) getTagSort(query *queryBuilder, findFilter *models.FindFilte
|
||||
sortQuery += getSort(sort, direction, "tags")
|
||||
}
|
||||
|
||||
// Whatever the sorting, always use name/id as a final sort
|
||||
sortQuery += ", COALESCE(tags.name, tags.id) COLLATE NATURAL_CI ASC"
|
||||
// Whatever the sorting, always use sort_name/name/id as a final sort
|
||||
sortQuery += ", COALESCE(tags.sort_name, tags.name, tags.id) COLLATE NATURAL_CI ASC"
|
||||
return sortQuery, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -62,6 +62,7 @@ func (qb *tagFilterHandler) criterionHandler() criterionHandler {
|
||||
tagFilter := qb.tagFilter
|
||||
return compoundHandler{
|
||||
stringCriterionHandler(tagFilter.Name, tagTable+".name"),
|
||||
stringCriterionHandler(tagFilter.SortName, tagTable+".sort_name"),
|
||||
qb.aliasCriterionHandler(tagFilter.Aliases),
|
||||
|
||||
boolCriterionHandler(tagFilter.Favorite, tagTable+".favorite", nil),
|
||||
|
||||
@@ -21,6 +21,7 @@ type FinderAliasImageGetter interface {
|
||||
func ToJSON(ctx context.Context, reader FinderAliasImageGetter, tag *models.Tag) (*jsonschema.Tag, error) {
|
||||
newTagJSON := jsonschema.Tag{
|
||||
Name: tag.Name,
|
||||
SortName: tag.SortName,
|
||||
Description: tag.Description,
|
||||
Favorite: tag.Favorite,
|
||||
IgnoreAutoTag: tag.IgnoreAutoTag,
|
||||
|
||||
@@ -24,6 +24,7 @@ const (
|
||||
|
||||
const (
|
||||
tagName = "testTag"
|
||||
sortName = "sortName"
|
||||
description = "description"
|
||||
)
|
||||
|
||||
@@ -37,6 +38,7 @@ func createTag(id int) models.Tag {
|
||||
return models.Tag{
|
||||
ID: id,
|
||||
Name: tagName,
|
||||
SortName: sortName,
|
||||
Favorite: true,
|
||||
Description: description,
|
||||
IgnoreAutoTag: autoTagIgnored,
|
||||
@@ -48,6 +50,7 @@ func createTag(id int) models.Tag {
|
||||
func createJSONTag(aliases []string, image string, parents []string) *jsonschema.Tag {
|
||||
return &jsonschema.Tag{
|
||||
Name: tagName,
|
||||
SortName: sortName,
|
||||
Favorite: true,
|
||||
Description: description,
|
||||
Aliases: aliases,
|
||||
|
||||
@@ -38,6 +38,7 @@ type Importer struct {
|
||||
func (i *Importer) PreImport(ctx context.Context) error {
|
||||
i.tag = models.Tag{
|
||||
Name: i.Input.Name,
|
||||
SortName: i.Input.SortName,
|
||||
Description: i.Input.Description,
|
||||
Favorite: i.Input.Favorite,
|
||||
IgnoreAutoTag: i.Input.IgnoreAutoTag,
|
||||
|
||||
@@ -40,6 +40,7 @@ func TestImporterPreImport(t *testing.T) {
|
||||
i := Importer{
|
||||
Input: jsonschema.Tag{
|
||||
Name: tagName,
|
||||
SortName: sortName,
|
||||
Description: description,
|
||||
Image: invalidImage,
|
||||
IgnoreAutoTag: autoTagIgnored,
|
||||
|
||||
Reference in New Issue
Block a user