fix: page the other users group on its own and use a design-system toggle

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013eAE5cpKPdtH9DNJm5TjcH
This commit is contained in:
Ruben Fiszel
2026-09-10 20:50:15 +02:00
co-authored by Claude Opus 5
parent 5f75564fa8
commit 8c01f5ba90
2 changed files with 88 additions and 61 deletions
@@ -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
@@ -162,11 +162,7 @@
type RootNode = ReturnType<typeof groupItems>[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 @@
})
</script>
{#snippet ownerNode(node: RootNode, loadRank: number, indent: number)}
<TreeView
rootIndex={loadRank}
{indent}
{isSearching}
{collapseAll}
item={node}
{pipelineFolders}
{ownerCounts}
ancestorHasMore={hasMoreServer}
{ownerLoad}
{onExpandOwner}
{onCollapseOwner}
on:scriptChanged
on:flowChanged
on:appChanged
on:rawAppChanged
on:reload
{showCode}
{showEditButton}
/>
{/snippet}
{#if groupedItems === 'loading'}
<div class="flex flex-row items-center justify-center">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900 dark:border-gray-100"
@@ -268,45 +287,52 @@
<div>
<span class="whitespace-nowrap text-xs text-emphasis font-semibold">Other users</span>
<div class="text-2xs font-normal text-secondary whitespace-nowrap">
({pluralize(row.users, 'user')}{row.items != undefined
? ` · ${pluralize(row.items, 'item')}`
({pluralize(otherUsers.length, 'user')}{otherUsersItemCount != undefined
? ` · ${pluralize(otherUsersItemCount, 'item')}`
: ''})
</div>
</div>
</div>
<button
class="w-full flex flex-row-reverse"
<Button
iconOnly
unifiedSize="xs"
variant="subtle"
startIcon={{ icon: otherUsersOpen ? ChevronUp : ChevronDown }}
title={otherUsersOpen ? 'Hide other users' : 'Show other users'}
aria-label="Other users"
aria-expanded={otherUsersOpen}
>
{#if otherUsersOpen}
<ChevronUp size={16} />
{:else}
<ChevronDown size={16} />
{/if}
</button>
onClick={toggleOtherUsers}
/>
</div>
{#if otherUsersOpen}
<!-- Ranked after every root owner, so opening this row can't push folders out
of what "expand all" auto-loads. -->
{#each otherUsers.slice(0, nbOtherUsersDisplayed) as user, i (user.username)}
{@render ownerNode(user, ownerRowCount + i, 1)}
{/each}
{#if nbOtherUsersDisplayed < otherUsers.length}
<div
class="pl-8 pr-4 py-2 border-b flex flex-row items-center justify-between gap-4 bg-surface-secondary"
>
<span class="text-xs text-secondary">
Showing {nbOtherUsersDisplayed} of {otherUsers.length} users
</span>
<Button
unifiedSize="sm"
variant="subtle"
onClick={() =>
(nbOtherUsersDisplayed = Math.min(
nbOtherUsersDisplayed + ROOT_PAGE,
otherUsers.length
))}
>
Show more
</Button>
</div>
{/if}
{/if}
{:else}
<TreeView
rootIndex={row.loadRank}
indent={row.nested ? 1 : 0}
{isSearching}
{collapseAll}
item={row.node}
{pipelineFolders}
{ownerCounts}
ancestorHasMore={hasMoreServer}
{ownerLoad}
{onExpandOwner}
{onCollapseOwner}
on:scriptChanged
on:flowChanged
on:appChanged
on:rawAppChanged
on:reload
{showCode}
{showEditButton}
/>
{@render ownerNode(row.node, row.loadRank, 0)}
{/if}
{/each}
{#if nbDisplayed < rows.length || hasMoreServer}