From cd6c54c4b8b057db270ac0ae2d4a2e136101a94b Mon Sep 17 00:00:00 2001 From: KennyG Date: Fri, 8 May 2026 11:40:35 -0400 Subject: [PATCH 1/3] 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. --- pkg/sqlite/group.go | 9 ++ pkg/sqlite/group_test.go | 84 +++++++++++++++++++ pkg/sqlite/sql.go | 24 ++++-- .../GroupDetails/GroupSubGroupsPanel.tsx | 14 +--- ui/v2.5/src/components/Groups/GroupList.tsx | 2 +- ui/v2.5/src/locales/en-GB.json | 1 + ui/v2.5/src/models/list-filter/groups.ts | 1 + 7 files changed, 118 insertions(+), 17 deletions(-) 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) From c77fc7d675cc432d9b39de17f03e08677920cf6c Mon Sep 17 00:00:00 2001 From: KennyG Date: Fri, 8 May 2026 17:47:48 -0400 Subject: [PATCH 2/3] Implement view filtering and saving. - Refactor GroupList and GroupSubGroupsPanel components for consistency with general models. - Removed unused defaultFilter from GroupList and GroupSubGroupsPanel. - Simplified FilteredGroupList by eliminating unnecessary state variables. - Fixed dropdown alignment issue in FilteredListToolbar by setting menuPortalTarget to document.body. --- .../Groups/GroupDetails/GroupSubGroupsPanel.tsx | 8 -------- ui/v2.5/src/components/Groups/GroupList.tsx | 12 ------------ ui/v2.5/src/components/List/FilteredListToolbar.tsx | 5 +++++ 3 files changed, 5 insertions(+), 20 deletions(-) diff --git a/ui/v2.5/src/components/Groups/GroupDetails/GroupSubGroupsPanel.tsx b/ui/v2.5/src/components/Groups/GroupDetails/GroupSubGroupsPanel.tsx index 98e44ac88..5dfba0569 100644 --- a/ui/v2.5/src/components/Groups/GroupDetails/GroupSubGroupsPanel.tsx +++ b/ui/v2.5/src/components/Groups/GroupDetails/GroupSubGroupsPanel.tsx @@ -64,13 +64,6 @@ interface IGroupSubGroupsPanel { extraOperations?: IItemListOperation[]; } -const defaultFilter = (() => { - return new ListFilterModel(GQL.FilterMode.Groups, undefined, { - defaultSortBy: "sub_group_description", - defaultSortDir: GQL.SortDirectionEnum.Asc, - }); -})(); - export const GroupSubGroupsPanel: React.FC = PatchComponent( "GroupSubGroupsPanel", @@ -155,7 +148,6 @@ export const GroupSubGroupsPanel: React.FC = <> {modal} ListFilterModel; - defaultFilter?: ListFilterModel; view?: View; alterQuery?: boolean; } @@ -210,12 +209,8 @@ export const FilteredGroupList = PatchComponent( onMove, fromGroupId, otherOperations: providedOperations = [], - defaultFilter, } = props; - const withSidebar = view !== View.GroupSubGroups; - const filterable = view !== View.GroupSubGroups; - const sortable = true; // States const { @@ -230,7 +225,6 @@ export const FilteredGroupList = PatchComponent( useFilteredItemList({ filterStateProps: { filterMode: GQL.FilterMode.Groups, - defaultFilter, view, useURL: alterQuery, }, @@ -402,8 +396,6 @@ export const FilteredGroupList = PatchComponent( operationComponent={operations} view={view} zoomable - filterable={filterable} - sortable={sortable} /> ); - if (!withSidebar) { - return content; - } - return (
= ({ sortable = true, }) => { const filterOptions = filter.options; + // Something in the popper layout for groups.sub-groups tab to double calculates the offset + // causing the dropdown to be misaligned. Portal to document.body to fix this. + const menuPortalTarget = + typeof document !== "undefined" ? document.body : undefined; const { setDisplayMode, setZoom } = useFilterOperations({ filter, setFilter, @@ -142,6 +146,7 @@ export const FilteredListToolbar: React.FC = ({ filter={filter} onSetFilter={setFilter} view={view} + menuPortalTarget={menuPortalTarget} /> showEditFilter()} From f5651acd87cd11438c73f49fc9b270e6d10790b4 Mon Sep 17 00:00:00 2001 From: KennyG Date: Fri, 8 May 2026 18:18:02 -0400 Subject: [PATCH 3/3] Format GroupList.tsx for Prettier (fix CI validate-ui) Co-authored-by: Cursor --- ui/v2.5/src/components/Groups/GroupList.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/ui/v2.5/src/components/Groups/GroupList.tsx b/ui/v2.5/src/components/Groups/GroupList.tsx index 4fb9001d4..502e44307 100644 --- a/ui/v2.5/src/components/Groups/GroupList.tsx +++ b/ui/v2.5/src/components/Groups/GroupList.tsx @@ -211,7 +211,6 @@ export const FilteredGroupList = PatchComponent( otherOperations: providedOperations = [], } = props; - // States const { showSidebar,