Add sorting to sub-group UI

- Implemented sorting functionality for sub-groups based on their descriptions in the database queries.
- Updated the GroupList component to allow sorting regardless of the view.
- Changed default sorting in the GroupSubGroupsPanel to sub-group description.
- Added localization for sub-group description in en-GB.json.
- Included sub-group description in the list of sortable options in the groups filter model.
This commit is contained in:
KennyG 2026-05-08 11:40:35 -04:00
parent 01a7583364
commit cd6c54c4b8
7 changed files with 118 additions and 17 deletions

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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<IGroupSubGroupsPanel> =

View file

@ -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 {

View file

@ -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}",

View file

@ -28,6 +28,7 @@ const sortByOptions = [
"duration",
"rating",
"tag_count",
"sub_group_description",
"sub_group_order",
]
.map(ListFilterOptions.createSortBy)