Skip to content

Commit ac1a0a2

Browse files
committed
Renders servers in the library's accordion and search views
Server virtual groups were computed with permanently empty tile lists, so any user with library grouping enabled (or an active search filter) saw a blank Servers tab even though server creation succeeded. Servers are now placed into their game-version groups and search results, sorted by the shared library sort setting, and the accordion view renders server tiles alongside instance tiles.
1 parent 0afb01c commit ac1a0a2

7 files changed

Lines changed: 524 additions & 158 deletions

File tree

apps/desktop/packages/mainWindow/src/pages/Library/HomeGrid.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -580,6 +580,7 @@ const HomeGridInner = () => {
580580
<Match when={!showFoldersView()}>
581581
<AccordionView
582582
virtualGroups={data().virtualGroups}
583+
libraryMode={libraryMode()}
583584
tileSize={tileSize}
584585
selection={selection}
585586
onDragStart={(type, ids, e) =>

apps/desktop/packages/mainWindow/src/pages/Library/hooks/useServerData.ts

Lines changed: 10 additions & 129 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import { createMemo, createEffect, on, Accessor } from "solid-js"
88
import { createStore, reconcile } from "solid-js/store"
9+
import { useTransContext } from "@gd/i18n"
910
import { rspc } from "@/utils/rspcClient"
1011
import { useGlobalStore } from "@/components/GlobalStoreContext"
1112
import { ListServer } from "@gd/core_module/bindings"
@@ -15,16 +16,20 @@ import {
1516
LibraryViewMode,
1617
getViewMode
1718
} from "../types"
19+
import {
20+
computeServerLibraryItems,
21+
computeServerVirtualGroups
22+
} from "../utils/serverGrouping"
1823

1924
interface ServerDataStore {
2025
libraryItems: LibraryItem[]
21-
virtualGroups: VirtualGroup[]
26+
virtualGroups: VirtualGroup<ListServer>[]
2227
favoriteIds: number[]
2328
}
2429

2530
export interface UseServerDataReturn {
2631
libraryItems: LibraryItem[]
27-
virtualGroups: VirtualGroup[]
32+
virtualGroups: VirtualGroup<ListServer>[]
2833
favoriteIds: number[]
2934
viewMode: Accessor<LibraryViewMode>
3035
isFoldersView: Accessor<boolean>
@@ -34,6 +39,7 @@ export interface UseServerDataReturn {
3439
}
3540

3641
export function useServerData(filter: Accessor<string>): UseServerDataReturn {
42+
const [t] = useTransContext()
3743
const globalStore = useGlobalStore()
3844

3945
const defaultGroupQuery = rspc.createQuery(() => ({
@@ -120,7 +126,8 @@ export function useServerData(filter: Accessor<string>): UseServerDataReturn {
120126
const virtualGroups = computeServerVirtualGroups(
121127
servers || [],
122128
settings,
123-
filterValue
129+
filterValue,
130+
t("library:_trn_search_results")
124131
)
125132
setStore("virtualGroups", reconcile(virtualGroups, { key: "id" }))
126133
}
@@ -155,129 +162,3 @@ export function useServerData(filter: Accessor<string>): UseServerDataReturn {
155162
isEmpty
156163
}
157164
}
158-
159-
function computeServerLibraryItems(
160-
servers: ListServer[],
161-
groups: { id: number; name: string; libraryPosition: number | null }[],
162-
filterValue: string,
163-
defaultGroupId: number
164-
): LibraryItem[] {
165-
const items: LibraryItem[] = []
166-
const nameFilter = filterValue.replaceAll(" ", "").toLowerCase()
167-
168-
// Group servers by group_id
169-
const serversByGroup = new Map<number, ListServer[]>()
170-
for (const server of servers) {
171-
const list = serversByGroup.get(server.groupId) || []
172-
list.push(server)
173-
serversByGroup.set(server.groupId, list)
174-
}
175-
176-
for (const group of groups) {
177-
const groupServers = serversByGroup.get(group.id) || []
178-
const filteredServers = groupServers.filter((s) =>
179-
s.name.toLowerCase().replaceAll(" ", "").includes(nameFilter)
180-
)
181-
182-
if (group.id === defaultGroupId) {
183-
for (const server of filteredServers) {
184-
items.push({
185-
id: `server-${server.id}`,
186-
type: "server",
187-
data: server
188-
})
189-
}
190-
} else if (groupServers.length === 0 || filteredServers.length > 0) {
191-
// Show as folder — servers inside are rendered by ExpandedFolderContent
192-
items.push({
193-
id: `folder-${group.id}`,
194-
type: "folder",
195-
data: {
196-
id: group.id,
197-
name: group.name,
198-
libraryPosition: group.libraryPosition,
199-
instances: filteredServers
200-
}
201-
})
202-
}
203-
}
204-
205-
// Folders always come before ungrouped servers, regardless of
206-
// libraryPosition. Within each bucket, sort ascending by libraryPosition
207-
// (falling back to index for servers).
208-
items.sort((a, b) => {
209-
const aFolder = a.type === "folder"
210-
const bFolder = b.type === "folder"
211-
if (aFolder !== bFolder) return aFolder ? -1 : 1
212-
const getKey = (item: LibraryItem) => {
213-
if (item.type === "server") {
214-
return item.data.libraryPosition ?? item.data.index
215-
}
216-
if (item.type === "folder") {
217-
return item.data.libraryPosition ?? 10000
218-
}
219-
return 10000
220-
}
221-
return getKey(a) - getKey(b)
222-
})
223-
224-
return items
225-
}
226-
227-
function computeServerVirtualGroups(
228-
servers: ListServer[],
229-
settings:
230-
| {
231-
instancesGroupBy?: string | null
232-
instancesSortBy?: string | null
233-
instancesSortByAsc?: boolean
234-
instancesGroupByAsc?: boolean
235-
}
236-
| undefined,
237-
filterValue: string
238-
): VirtualGroup[] {
239-
const nameFilter = filterValue.replaceAll(" ", "").toLowerCase()
240-
const groupBy = settings?.instancesGroupBy
241-
const groupByAsc = settings?.instancesGroupByAsc ?? true
242-
243-
const matching = servers.filter((s) =>
244-
s.name.toLowerCase().replaceAll(" ", "").includes(nameFilter)
245-
)
246-
247-
if (groupBy === null || groupBy === undefined) {
248-
// Flat search results or folders mode — single group
249-
matching.sort((a, b) => a.name.localeCompare(b.name))
250-
return matching.length > 0
251-
? [{ id: "search-results", name: "Search Results", instances: [] }]
252-
: []
253-
}
254-
255-
// Group by gameVersion is the only meaningful grouping for servers initially
256-
const groupsMap = new Map<string, VirtualGroup>()
257-
258-
for (const server of matching) {
259-
let groupName = server.gameVersion || "Unknown"
260-
let groupId: string = groupName
261-
262-
if (groupBy === "gameVersion") {
263-
groupName = server.gameVersion || "Unknown"
264-
groupId = groupName
265-
}
266-
267-
if (!groupsMap.has(groupName)) {
268-
groupsMap.set(groupName, {
269-
id: groupId,
270-
name: groupName,
271-
instances: [] // VirtualGroup uses instances field
272-
})
273-
}
274-
}
275-
276-
const result = Array.from(groupsMap.values())
277-
result.sort((a, b) => {
278-
const cmp = a.name.localeCompare(b.name, undefined, { numeric: true })
279-
return groupByAsc ? cmp : -cmp
280-
})
281-
282-
return result
283-
}

apps/desktop/packages/mainWindow/src/pages/Library/types.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,12 @@ export interface FolderData {
4242
/**
4343
* A virtual group for accordion mode.
4444
* Groups are computed based on instancesGroupBy setting (modloader, gameVersion, modplatform).
45+
* Instance groups hold ListInstance tiles; server groups hold ListServer tiles.
4546
*/
46-
export interface VirtualGroup {
47+
export interface VirtualGroup<T = ListInstance> {
4748
id: string | number | null
4849
name: string
49-
instances: ListInstance[]
50+
instances: T[]
5051
}
5152

5253
/**
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
/**
2+
* Pure computation helpers for the servers library views.
3+
*
4+
* Mirrors the instance-side logic in useLibraryData: computeServerLibraryItems
5+
* feeds the folders view, computeServerVirtualGroups feeds the accordion view
6+
* and flat search results.
7+
*/
8+
9+
import { ListServer } from "@gd/core_module/bindings"
10+
import { LibraryItem, VirtualGroup } from "../types"
11+
12+
export function computeServerLibraryItems(
13+
servers: ListServer[],
14+
groups: { id: number; name: string; libraryPosition: number | null }[],
15+
filterValue: string,
16+
defaultGroupId: number
17+
): LibraryItem[] {
18+
const items: LibraryItem[] = []
19+
const nameFilter = filterValue.replaceAll(" ", "").toLowerCase()
20+
21+
// Group servers by group_id
22+
const serversByGroup = new Map<number, ListServer[]>()
23+
for (const server of servers) {
24+
const list = serversByGroup.get(server.groupId) || []
25+
list.push(server)
26+
serversByGroup.set(server.groupId, list)
27+
}
28+
29+
for (const group of groups) {
30+
const groupServers = serversByGroup.get(group.id) || []
31+
const filteredServers = groupServers.filter((s) =>
32+
s.name.toLowerCase().replaceAll(" ", "").includes(nameFilter)
33+
)
34+
35+
if (group.id === defaultGroupId) {
36+
for (const server of filteredServers) {
37+
items.push({
38+
id: `server-${server.id}`,
39+
type: "server",
40+
data: server
41+
})
42+
}
43+
} else if (groupServers.length === 0 || filteredServers.length > 0) {
44+
// Show as folder — servers inside are rendered by ExpandedFolderContent
45+
items.push({
46+
id: `folder-${group.id}`,
47+
type: "folder",
48+
data: {
49+
id: group.id,
50+
name: group.name,
51+
libraryPosition: group.libraryPosition,
52+
instances: filteredServers
53+
}
54+
})
55+
}
56+
}
57+
58+
// Folders always come before ungrouped servers, regardless of
59+
// libraryPosition. Within each bucket, sort ascending by libraryPosition
60+
// (falling back to index for servers).
61+
items.sort((a, b) => {
62+
const aFolder = a.type === "folder"
63+
const bFolder = b.type === "folder"
64+
if (aFolder !== bFolder) return aFolder ? -1 : 1
65+
const getKey = (item: LibraryItem) => {
66+
if (item.type === "server") {
67+
return item.data.libraryPosition ?? item.data.index
68+
}
69+
if (item.type === "folder") {
70+
return item.data.libraryPosition ?? 10000
71+
}
72+
return 10000
73+
}
74+
return getKey(a) - getKey(b)
75+
})
76+
77+
return items
78+
}
79+
80+
export function computeServerVirtualGroups(
81+
servers: ListServer[],
82+
settings:
83+
| {
84+
instancesGroupBy?: string | null
85+
instancesSortBy?: string | null
86+
instancesSortByAsc?: boolean
87+
instancesGroupByAsc?: boolean
88+
}
89+
| undefined,
90+
filterValue: string,
91+
searchResultsLabel: string
92+
): VirtualGroup<ListServer>[] {
93+
const nameFilter = filterValue.replaceAll(" ", "").toLowerCase()
94+
const groupBy = settings?.instancesGroupBy
95+
const sortBy = settings?.instancesSortBy
96+
const sortByAsc = settings?.instancesSortByAsc ?? true
97+
const groupByAsc = settings?.instancesGroupByAsc ?? true
98+
99+
const matching = servers.filter((s) =>
100+
s.name.toLowerCase().replaceAll(" ", "").includes(nameFilter)
101+
)
102+
103+
if (groupBy === null || groupBy === undefined) {
104+
// Flat search results in folders mode — single group
105+
matching.sort((a, b) => a.name.localeCompare(b.name))
106+
return matching.length > 0
107+
? [
108+
{
109+
id: "search-results",
110+
name: searchResultsLabel,
111+
instances: matching
112+
}
113+
]
114+
: []
115+
}
116+
117+
// Servers only support grouping by game version, but the setting is shared
118+
// with the instances library and can hold instance-only values (modloader,
119+
// modplatform) — those fall back to game version grouping too.
120+
const groupsMap = new Map<string, VirtualGroup<ListServer>>()
121+
122+
for (const server of matching) {
123+
const groupName = server.gameVersion || "Unknown"
124+
125+
if (!groupsMap.has(groupName)) {
126+
groupsMap.set(groupName, {
127+
id: groupName,
128+
name: groupName,
129+
instances: []
130+
})
131+
}
132+
133+
groupsMap.get(groupName)!.instances.push(server)
134+
}
135+
136+
// Sort servers within each group
137+
for (const group of groupsMap.values()) {
138+
group.instances.sort((a, b) => {
139+
let result = sortServers(a, b, sortBy)
140+
if (!sortByAsc) result = -result
141+
return result || a.name.localeCompare(b.name)
142+
})
143+
}
144+
145+
const result = Array.from(groupsMap.values())
146+
result.sort((a, b) => {
147+
const cmp = a.name.localeCompare(b.name, undefined, {
148+
numeric: true,
149+
sensitivity: "base"
150+
})
151+
return groupByAsc ? cmp : -cmp
152+
})
153+
154+
return result
155+
}
156+
157+
/**
158+
* Sort servers by the shared instancesSortBy setting. Server mode only offers
159+
* name, gameVersion and created; instance-only criteria fall through to the
160+
* caller's name fallback.
161+
*/
162+
function sortServers(
163+
a: ListServer,
164+
b: ListServer,
165+
sortBy: string | null | undefined
166+
): number {
167+
if (sortBy === null || sortBy === undefined) {
168+
return a.index - b.index
169+
}
170+
171+
switch (sortBy) {
172+
case "name":
173+
return a.name.localeCompare(b.name)
174+
case "gameVersion":
175+
return a.gameVersion.localeCompare(b.gameVersion, undefined, {
176+
numeric: true,
177+
sensitivity: "base"
178+
})
179+
case "created": {
180+
const aTime = a.dateCreated ? Date.parse(a.dateCreated) : 0
181+
const bTime = b.dateCreated ? Date.parse(b.dateCreated) : 0
182+
return aTime - bTime
183+
}
184+
default:
185+
return 0
186+
}
187+
}

0 commit comments

Comments
 (0)