diff --git a/pkg/sqlite/group.go b/pkg/sqlite/group.go index 13a6905a5..666ff2e84 100644 --- a/pkg/sqlite/group.go +++ b/pkg/sqlite/group.go @@ -500,6 +500,7 @@ var groupSortOptions = sortOptions{ "rating", "scenes_count", "o_counter", + "sub_group_description", "sub_group_order", "tag_count", "updated_at", @@ -532,6 +533,14 @@ func (qb *GroupStore) setGroupSort(query *queryBuilder, findFilter *models.FindF query.joinSort(groupRelationsTable, "", "groups.id = groups_relations.sub_id") query.sortAndPagination += getSort("order_index", direction, groupRelationsTable) } + case "sub_group_description": + // as above, we need to handle parent groups differently here + if query.hasJoin("groups_parents") { + query.sortAndPagination += getSort("description", direction, "groups_parents") + } else { + query.joinSort(groupRelationsTable, "", "groups.id = groups_relations.sub_id") + query.sortAndPagination += getSort("description", direction, groupRelationsTable) + } case "tag_count": query.sortAndPagination += getCountSort(groupTable, groupsTagsTable, groupIDColumn, direction) case "scenes_count": // generic getSort won't work for this diff --git a/pkg/sqlite/group_test.go b/pkg/sqlite/group_test.go index 22b551e02..dc47d6e0b 100644 --- a/pkg/sqlite/group_test.go +++ b/pkg/sqlite/group_test.go @@ -1124,6 +1124,90 @@ func TestGroupQuerySortOrderIndex(t *testing.T) { }) } +func TestGroupQuerySortSubGroupDescription(t *testing.T) { + runWithRollbackTxn(t, "sort subgroup description", func(t *testing.T, ctx context.Context) { + assert := assert.New(t) + + cEmpty := models.Group{Name: "sort-desc-child-empty"} + c01 := models.Group{Name: "sort-desc-child-01"} + c2 := models.Group{Name: "sort-desc-child-2"} + c10 := models.Group{Name: "sort-desc-child-10"} + assert.NoError(db.Group.Create(ctx, &cEmpty)) + assert.NoError(db.Group.Create(ctx, &c01)) + assert.NoError(db.Group.Create(ctx, &c2)) + assert.NoError(db.Group.Create(ctx, &c10)) + + parent := models.Group{ + Name: "sort-desc-parent", + SubGroups: models.NewRelatedGroupDescriptions([]models.GroupIDDescription{ + {GroupID: cEmpty.ID, Description: ""}, + {GroupID: c10.ID, Description: "10"}, + {GroupID: c2.ID, Description: "2"}, + {GroupID: c01.ID, Description: "01"}, + }), + } + assert.NoError(db.Group.Create(ctx, &parent)) + + sortKey := "sub_group_description" + dirAsc := models.SortDirectionEnumAsc + findFilter := models.FindFilterType{ + Sort: &sortKey, + Direction: &dirAsc, + } + groupFilter := models.GroupFilterType{ + ContainingGroups: &models.HierarchicalMultiCriterionInput{ + Value: []string{strconv.Itoa(parent.ID)}, + Modifier: models.CriterionModifierIncludes, + }, + } + + groups, _, err := db.Group.Query(ctx, &groupFilter, &findFilter) + assert.NoError(err) + assert.Len(groups, 4) + assert.Equal(cEmpty.ID, groups[0].ID) + assert.Equal(c01.ID, groups[1].ID) + assert.Equal(c2.ID, groups[2].ID) + assert.Equal(c10.ID, groups[3].ID) + + dirDesc := models.SortDirectionEnumDesc + findFilter.Direction = &dirDesc + groups, _, err = db.Group.Query(ctx, &groupFilter, &findFilter) + assert.NoError(err) + assert.Len(groups, 4) + assert.Equal(c10.ID, groups[0].ID) + assert.Equal(c2.ID, groups[1].ID) + assert.Equal(c01.ID, groups[2].ID) + assert.Equal(cEmpty.ID, groups[3].ID) + + // Exercise the non-groups_parents code path by filtering on name only. + nameCriterion := models.StringCriterionInput{ + Value: "sort-desc-child-", + Modifier: models.CriterionModifierIncludes, + } + nameFilter := models.GroupFilterType{ + Name: &nameCriterion, + } + + findFilter.Direction = &dirAsc + groups, _, err = db.Group.Query(ctx, &nameFilter, &findFilter) + assert.NoError(err) + assert.Len(groups, 4) + assert.Equal(cEmpty.ID, groups[0].ID) + assert.Equal(c01.ID, groups[1].ID) + assert.Equal(c2.ID, groups[2].ID) + assert.Equal(c10.ID, groups[3].ID) + + findFilter.Direction = &dirDesc + groups, _, err = db.Group.Query(ctx, &nameFilter, &findFilter) + assert.NoError(err) + assert.Len(groups, 4) + assert.Equal(c10.ID, groups[0].ID) + assert.Equal(c2.ID, groups[1].ID) + assert.Equal(c01.ID, groups[2].ID) + assert.Equal(cEmpty.ID, groups[3].ID) + }) +} + func TestGroupUpdateFrontImage(t *testing.T) { if err := withRollbackTxn(func(ctx context.Context) error { qb := db.Group diff --git a/pkg/sqlite/sql.go b/pkg/sqlite/sql.go index 87376c2c1..13156b772 100644 --- a/pkg/sqlite/sql.go +++ b/pkg/sqlite/sql.go @@ -88,6 +88,23 @@ func getSortDirection(direction string) string { return direction } } + +func isNaturalSort(sort string) bool { + switch sort { + case "name", "title", "description": + return true + default: + return false + } +} + +func isCoalesceSort(column, sort string) string { + if sort == "description" { + return coalesce(column) + } + return column +} + func getSort(sort string, direction string, tableName string) string { direction = getSortDirection(direction) @@ -115,11 +132,8 @@ func getSort(sort string, direction string, tableName string) string { if strings.Contains(sort, ".") { colName = sort } - if strings.Compare(sort, "name") == 0 { - return " ORDER BY " + colName + " COLLATE NATURAL_CI " + direction - } - if strings.Compare(sort, "title") == 0 { - return " ORDER BY " + colName + " COLLATE NATURAL_CI " + direction + if isNaturalSort(sort) { + return " ORDER BY " + isCoalesceSort(colName, sort) + " COLLATE NATURAL_CI " + direction } return " ORDER BY " + colName + " " + direction diff --git a/ui/v2.5/src/components/Groups/GroupDetails/GroupSubGroupsPanel.tsx b/ui/v2.5/src/components/Groups/GroupDetails/GroupSubGroupsPanel.tsx index 6a11f7004..98e44ac88 100644 --- a/ui/v2.5/src/components/Groups/GroupDetails/GroupSubGroupsPanel.tsx +++ b/ui/v2.5/src/components/Groups/GroupDetails/GroupSubGroupsPanel.tsx @@ -54,9 +54,6 @@ const useContainingGroupFilterHook = ( filter.criteria.push(groupCriterion); } - filter.sortBy = "sub_group_order"; - filter.sortDirection = GQL.SortDirectionEnum.Asc; - return filter; }; }; @@ -68,15 +65,10 @@ interface IGroupSubGroupsPanel { } const defaultFilter = (() => { - const sortBy = "sub_group_order"; - const ret = new ListFilterModel(GQL.FilterMode.Groups, undefined, { - defaultSortBy: sortBy, + return new ListFilterModel(GQL.FilterMode.Groups, undefined, { + defaultSortBy: "sub_group_description", + defaultSortDir: GQL.SortDirectionEnum.Asc, }); - - // unset the sort by so that its not included in the URL - ret.sortBy = undefined; - - return ret; })(); export const GroupSubGroupsPanel: React.FC = diff --git a/ui/v2.5/src/components/Groups/GroupList.tsx b/ui/v2.5/src/components/Groups/GroupList.tsx index 69961f783..39af2f42d 100644 --- a/ui/v2.5/src/components/Groups/GroupList.tsx +++ b/ui/v2.5/src/components/Groups/GroupList.tsx @@ -215,7 +215,7 @@ export const FilteredGroupList = PatchComponent( const withSidebar = view !== View.GroupSubGroups; const filterable = view !== View.GroupSubGroups; - const sortable = view !== View.GroupSubGroups; + const sortable = true; // States const { diff --git a/ui/v2.5/src/locales/en-GB.json b/ui/v2.5/src/locales/en-GB.json index 4974c06ca..25dbb9342 100644 --- a/ui/v2.5/src/locales/en-GB.json +++ b/ui/v2.5/src/locales/en-GB.json @@ -1611,6 +1611,7 @@ "sub_group_count": "Sub-Group Count", "sub_group_of": "Sub-group of {parent}", "sub_group_order": "Sub-Group Order", + "sub_group_description": "Sub-Group Description", "sub_groups": "Sub-Groups", "sub_tag_count": "Sub-Tag Count", "sub_tag_of": "Sub-tag of {parent}", diff --git a/ui/v2.5/src/models/list-filter/groups.ts b/ui/v2.5/src/models/list-filter/groups.ts index 9c5b3f2d4..899d4d0f9 100644 --- a/ui/v2.5/src/models/list-filter/groups.ts +++ b/ui/v2.5/src/models/list-filter/groups.ts @@ -28,6 +28,7 @@ const sortByOptions = [ "duration", "rating", "tag_count", + "sub_group_description", "sub_group_order", ] .map(ListFilterOptions.createSortBy)