From 8c01f5ba9099dace7384a88fab85fb04e59cc382 Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Thu, 10 Sep 2026 20:50:15 +0200 Subject: [PATCH] fix: page the other users group on its own and use a design-system toggle Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_013eAE5cpKPdtH9DNJm5TjcH --- .../src/lib/components/home/TreeView.svelte | 5 +- .../lib/components/home/TreeViewRoot.svelte | 144 +++++++++++------- 2 files changed, 88 insertions(+), 61 deletions(-) diff --git a/frontend/src/lib/components/home/TreeView.svelte b/frontend/src/lib/components/home/TreeView.svelte index 5fa52c9527..0581700316 100644 --- a/frontend/src/lib/components/home/TreeView.svelte +++ b/frontend/src/lib/components/home/TreeView.svelte @@ -32,8 +32,9 @@ // `all` pages the prefix to the end in one call instead of fetching a single page. onExpandOwner?: (prefix: string, more?: boolean, opts?: { all?: boolean }) => void onCollapseOwner?: (prefix: string) => void - // Position of this node among the rendered root nodes; "expand all" only - // auto-loads the first EXPAND_ALL_LOAD_LIMIT of them (see the effect below). + // This root owner's place in line for "expand all", which only auto-loads the first + // EXPAND_ALL_LOAD_LIMIT (see the effect below). Not always its rendered position: + // owners nested under a grouping row are ranked after the rest. rootIndex?: number showEditButton?: boolean // Path prefix of the parent node, so this one can name its own (`ownerLoad` and diff --git a/frontend/src/lib/components/home/TreeViewRoot.svelte b/frontend/src/lib/components/home/TreeViewRoot.svelte index 70abbed45a..062dfb69f8 100644 --- a/frontend/src/lib/components/home/TreeViewRoot.svelte +++ b/frontend/src/lib/components/home/TreeViewRoot.svelte @@ -162,11 +162,7 @@ type RootNode = ReturnType[number] // `loadRank` is what "expand all" caps its requests by (see TreeView's rootIndex). - // Other users rank after every folder, so opening their row can't push the folders - // out of the auto-loaded set. - type RootRow = - | { kind: 'node'; node: RootNode; nested: boolean; loadRank: number } - | { kind: 'otherUsers'; users: number; items: number | undefined } + type RootRow = { kind: 'node'; node: RootNode; loadRank: number } | { kind: 'otherUsers' } const OTHER_USERS_OPEN_SETTING_NAME = 'homeTreeOtherUsersOpen' let otherUsersOpen = $state(getLocalSetting(OTHER_USERS_OPEN_SETTING_NAME) == 'true') @@ -174,43 +170,43 @@ otherUsersOpen = !otherUsersOpen storeLocalSetting(OTHER_USERS_OPEN_SETTING_NAME, otherUsersOpen ? 'true' : undefined) } + // The group pages its rows separately from the root slice. Drawing from `nbDisplayed` + // instead, opening it would push folders past the slice and unmount them, dropping + // whatever they had expanded. + let nbOtherUsersDisplayed = $state(ROOT_PAGE) function isOtherUser(node: RootNode): node is UserItem { return 'username' in node && node.username !== selfUsername } + let otherUsers: UserItem[] = $derived( + groupOtherUsers && selfUsername != undefined && Array.isArray(groupedItems) + ? groupedItems.filter(isOtherUser) + : [] + ) + let otherUsersItemCount = $derived( + ownerCounts != undefined + ? otherUsers.reduce( + (sum, u) => sum + Math.max(ownerCounts[`u/${u.username}`] ?? 0, countLeaves(u)), + 0 + ) + : undefined + ) + let rows: RootRow[] = $derived.by(() => { if (!Array.isArray(groupedItems)) return [] - const others = - groupOtherUsers && selfUsername != undefined ? groupedItems.filter(isOtherUser) : [] - if (others.length === 0) { - return groupedItems.map( - (node, i): RootRow => ({ kind: 'node', node, nested: false, loadRank: i }) - ) + if (otherUsers.length === 0) { + return groupedItems.map((node, i): RootRow => ({ kind: 'node', node, loadRank: i })) } // `groupItems` orders users before folders, so with the others taken out the viewer's // own space is what leads. const main = groupedItems.filter((g) => !isOtherUser(g)) const lead = main.filter((g) => 'username' in g) const rest = main.filter((g) => !('username' in g)) - const items = - ownerCounts != undefined - ? others.reduce( - (sum, u) => sum + Math.max(ownerCounts[`u/${u.username}`] ?? 0, countLeaves(u)), - 0 - ) - : undefined return [ - ...lead.map((node, i): RootRow => ({ kind: 'node', node, nested: false, loadRank: i })), - { kind: 'otherUsers', users: others.length, items }, - ...(otherUsersOpen - ? others.map( - (node, i): RootRow => ({ kind: 'node', node, nested: true, loadRank: main.length + i }) - ) - : []), - ...rest.map( - (node, i): RootRow => ({ kind: 'node', node, nested: false, loadRank: lead.length + i }) - ) + ...lead.map((node, i): RootRow => ({ kind: 'node', node, loadRank: i })), + { kind: 'otherUsers' }, + ...rest.map((node, i): RootRow => ({ kind: 'node', node, loadRank: lead.length + i })) ] }) // Owner rows only, for the footer: the "Other users" row is neither a folder nor a user. @@ -242,6 +238,29 @@ }) +{#snippet ownerNode(node: RootNode, loadRank: number, indent: number)} + +{/snippet} + {#if groupedItems === 'loading'}
Other users
- ({pluralize(row.users, 'user')}{row.items != undefined - ? ` · ${pluralize(row.items, 'item')}` + ({pluralize(otherUsers.length, 'user')}{otherUsersItemCount != undefined + ? ` · ${pluralize(otherUsersItemCount, 'item')}` : ''})
- + onClick={toggleOtherUsers} + /> + {#if otherUsersOpen} + + {#each otherUsers.slice(0, nbOtherUsersDisplayed) as user, i (user.username)} + {@render ownerNode(user, ownerRowCount + i, 1)} + {/each} + {#if nbOtherUsersDisplayed < otherUsers.length} +
+ + Showing {nbOtherUsersDisplayed} of {otherUsers.length} users + + +
+ {/if} + {/if} {:else} - + {@render ownerNode(row.node, row.loadRank, 0)} {/if} {/each} {#if nbDisplayed < rows.length || hasMoreServer}