From 5de65e60bad868093a3560704bdf8c539f93712b Mon Sep 17 00:00:00 2001 From: Edouard Vanbelle Date: Sun, 19 Jul 2026 12:36:04 +0200 Subject: [PATCH] feat(drive): UX: refresh drive list on change this fix an issue where the list was cleared but never refreshed --- frontend/src/lib/stores/drives.svelte.ts | 19 ++++++++++++++++--- frontend/src/routes/admin/+page.svelte | 15 +++++++++------ .../routes/config/drive/[uuid]/+page.svelte | 6 ++---- 3 files changed, 27 insertions(+), 13 deletions(-) diff --git a/frontend/src/lib/stores/drives.svelte.ts b/frontend/src/lib/stores/drives.svelte.ts index 4e76ac62..c89a2b64 100644 --- a/frontend/src/lib/stores/drives.svelte.ts +++ b/frontend/src/lib/stores/drives.svelte.ts @@ -30,10 +30,23 @@ class DrivesStore { return this.inflight; } - /** Force a refresh after a mutation (rename, member change, …). */ - invalidate(): void { + /** + * Re-fetch after a mutation (rename, member change, policy update, …). + * + * Deliberately keeps `this.drives` populated during the refetch — + * the sidebar picker and breadcrumb keep rendering the stale list + * until the new one lands, avoiding an empty-flash during the + * mutation. The atomic replacement inside `load()` swaps in the + * fresh list in a single reactive tick. + * + * Only `loaded` is flipped so `load()`'s cache guard falls through. + * Callers can `await` the returned promise if they need to observe + * the settled list; a fire-and-forget `refresh()` is also fine for + * pure UI-refresh scenarios. + */ + async refresh(): Promise { this.loaded = false; - this.drives = []; + return this.load(); } /** Caller's default-personal drive (one per internal user), or null. */ diff --git a/frontend/src/routes/admin/+page.svelte b/frontend/src/routes/admin/+page.svelte index 35938abb..23b91c24 100644 --- a/frontend/src/routes/admin/+page.svelte +++ b/frontend/src/routes/admin/+page.svelte @@ -1136,11 +1136,14 @@ ); // The shared `drivesStore` (feeds `/config/drive/{uuid}`, the // sidebar picker, the breadcrumb) caches `GET /api/drives` with - // `loaded=true` after the first fetch — without this invalidate + // `loaded=true` after the first fetch — without this refresh // call the admin's policy change wouldn't propagate to those // surfaces until a full page reload. Sibling `requestDeleteDrive` // does the same after `deleteDriveAdmin`. - drivesStore.invalidate(); + // + // Fire-and-forget: the modal closes immediately; the picker + // re-renders in place when the promise settles a few ms later. + void drivesStore.refresh(); closeManagePolicies(); } catch (e) { managePoliciesError = errorMessage(e); @@ -1168,10 +1171,10 @@ try { await deleteDriveAdmin(d.id); // Refresh the listing + the sidebar picker. Both have a cached - // view of this drive; without the invalidate the row lingers + // view of this drive; without the refresh the row lingers // until the next full reload. await loadDrivesTab(); - drivesStore.invalidate(); + await drivesStore.refresh(); ui.notify(t('admin.drive_deleted', 'Drive deleted.'), 'success'); } catch (e) { reportError(e); @@ -1205,10 +1208,10 @@ }); driveCreateOpen = false; await loadDrivesTab(); - // The global drives store backs the sidebar picker; drop its cache + // The global drives store backs the sidebar picker; re-fetch it // so the new drive shows up for every consumer (picker, breadcrumb, // session bootstrap) without a page reload. - drivesStore.invalidate(); + await drivesStore.refresh(); ui.notify(t('admin.drive_created', 'Drive created.'), 'success'); } catch (err) { driveCreateError = errorMessage(err); diff --git a/frontend/src/routes/config/drive/[uuid]/+page.svelte b/frontend/src/routes/config/drive/[uuid]/+page.svelte index 1a4d688c..6c496421 100644 --- a/frontend/src/routes/config/drive/[uuid]/+page.svelte +++ b/frontend/src/routes/config/drive/[uuid]/+page.svelte @@ -60,8 +60,7 @@ deleting = true; try { await deleteDrive(drive.id); - drivesStore.invalidate(); - await drivesStore.load(); + await drivesStore.refresh(); ui.notify(t('drive.deleted', 'Drive deleted.'), 'success'); // Send the user back to /files. The picker's reload above // already removed the now-deleted drive from the sidebar. @@ -109,8 +108,7 @@ // parent_id IS NULL, so a non-Owner caller would 404 here // (but the UI also hid this button for non-Owners). await renameFolder(drive.root_folder_id, next); - drivesStore.invalidate(); - await drivesStore.load(); + await drivesStore.refresh(); renaming = false; } catch (e) { errorToast(e);