From 3e51ab27d378d084bd6280d6408c1fa167cc6783 Mon Sep 17 00:00:00 2001 From: Edouard Vanbelle Date: Sat, 20 Jun 2026 02:27:24 +0200 Subject: [PATCH] feat(shared-with-me): add missing group by sections --- frontend/src/routes/favorites/+page.svelte | 2 +- frontend/src/routes/recent/+page.svelte | 2 +- .../src/routes/shared-with-me/+page.svelte | 80 ++++++++++++++++--- 3 files changed, 72 insertions(+), 12 deletions(-) diff --git a/frontend/src/routes/favorites/+page.svelte b/frontend/src/routes/favorites/+page.svelte index df2f5f80..fe750825 100644 --- a/frontend/src/routes/favorites/+page.svelte +++ b/frontend/src/routes/favorites/+page.svelte @@ -60,7 +60,7 @@ ); const groupBys: GroupByDef[] = [ - { key: '', label: t('files.name', 'Name'), orderBy: 'name' }, + { key: '', label: t('files.name', 'Name'), orderBy: 'name', icon: 'arrow-up-a-z' }, { key: 'owner', label: t('groupby.owner', 'Owner'), diff --git a/frontend/src/routes/recent/+page.svelte b/frontend/src/routes/recent/+page.svelte index 32ac1585..883c3527 100644 --- a/frontend/src/routes/recent/+page.svelte +++ b/frontend/src/routes/recent/+page.svelte @@ -61,7 +61,7 @@ ); const groupBys: GroupByDef[] = [ - { key: '', label: t('files.name', 'Name'), orderBy: 'name' }, + { key: '', label: t('files.name', 'Name'), orderBy: 'name', icon: 'arrow-up-a-z' }, { key: 'owner', label: t('groupby.owner', 'Owner'), diff --git a/frontend/src/routes/shared-with-me/+page.svelte b/frontend/src/routes/shared-with-me/+page.svelte index 0ac91ecb..a1820b6c 100644 --- a/frontend/src/routes/shared-with-me/+page.svelte +++ b/frontend/src/routes/shared-with-me/+page.svelte @@ -2,22 +2,32 @@ import { errorMessage } from '$lib/utils/errors'; import { goto } from '$app/navigation'; import { onMount } from 'svelte'; + import { dateBucket, resolveOwnerName, typeLabel } from '$lib/api/endpoints/favorites'; import { fetchSharedWithMe, type IncomingGrantItem } from '$lib/api/endpoints/grants'; import type { FileItem } from '$lib/api/types'; import { lazyComponent } from '$lib/composables/lazyComponent.svelte'; - import ResourceList, { type ResourceEntry } from '$lib/components/ResourceList.svelte'; + import { useOwnerCache } from '$lib/composables/useOwnerCache.svelte'; + import ResourceList, { + type GroupByDef, + type ResourceEntry + } from '$lib/components/ResourceList.svelte'; import { t } from '$lib/i18n/index.svelte'; let raw = $state([]); let cursor = $state(undefined); let loading = $state(false); let error = $state(null); + let groupBy = $state(''); + let reversed = $state(false); + + const sharers = useOwnerCache(resolveOwnerName); const byId = $derived(new Map(raw.map((it) => [it.resource.id, it]))); const entries = $derived( - raw.map( - (it): ResourceEntry => ({ + raw.map((it): ResourceEntry => { + const isFile = it.resource_type === 'file'; + return { id: it.resource.id, name: it.resource.name, kind: it.resource_type, @@ -28,20 +38,63 @@ // resource's real location so the row still shows where it // lives, not a translated string. ownerId: it.granted_by ?? null, + ownerName: sharers.name(it.granted_by), path: it.resource.path, - size: it.resource_type === 'file' ? (it.resource as FileItem).size : null, - date: it.granted_at - }) - ) + size: isFile ? (it.resource as FileItem).size : null, + date: it.granted_at, + category: isFile ? it.resource.category : 'Folder' + }; + }) ); - async function load(reset = false) { + // Server-supported sort_by values (see grant_handler.rs:615): + // granted_at, granted_by, name, type + // The first entry (no `bucketOf`) renders a flat list sorted by name — + // the A-Z icon flags it as "sort, not group" so users don't read it as + // a real bucket dimension. The remaining three are honest groupings and + // get the default layer-group icon. + const groupBys: GroupByDef[] = [ + { key: '', label: t('files.name', 'Name'), orderBy: 'name', icon: 'arrow-up-a-z' }, + { + key: 'sharedBy', + label: t('groupby.sharedBy', 'Shared by'), + orderBy: 'granted_by', + bucketOf: (e) => e.ownerId ?? null, + labelOf: (id) => sharers.label(id) + }, + { + key: 'type', + label: t('groupby.type', 'Type'), + orderBy: 'type', + bucketOf: (e) => e.category ?? 'other', + labelOf: (k) => typeLabel(k) + }, + { + key: 'sharedAt', + label: t('groupby.sharedAt', 'Shared date'), + orderBy: 'granted_at', + bucketOf: (e) => dateBucket(e.date) + } + ]; + + function orderByForGroup(): string { + return groupBys.find((g) => g.key === groupBy)?.orderBy ?? 'granted_at'; + } + + async function load(reset = false, orderBy = 'granted_at', rev = reversed) { loading = true; error = null; try { - const page = await fetchSharedWithMe({ cursor: reset ? undefined : cursor }); + const page = await fetchSharedWithMe({ + cursor: reset ? undefined : cursor, + orderBy, + reverse: rev + }); raw = reset ? page.items : [...raw, ...page.items]; cursor = page.next_cursor; + // Warm the sharer-name cache so the "Shared by" group headers + // show real names instead of UUIDs. + void sharers.resolve(page.items.map((i) => i.granted_by).filter((id): id is string => !!id)); } catch (e) { error = errorMessage(e); } finally { @@ -84,8 +137,15 @@ emptyText={t('shared_with_me.empty', 'Nothing has been shared with you yet.')} hasMore={!!cursor} showOwner={true} - onloadmore={() => load(false)} + {groupBys} + bind:groupBy + bind:reversed + onloadmore={() => load(false, orderByForGroup())} onopen={open} + onreload={(orderBy, rev) => { + cursor = undefined; + load(true, orderBy, rev); + }} /> {#if fileViewer.component}