From b9d6fa39c06b7270420479e7bff9648a28e4ae6f Mon Sep 17 00:00:00 2001 From: Edouard Vanbelle Date: Mon, 13 Jul 2026 23:16:04 +0200 Subject: [PATCH] feat(user-pref): revert view mode as user-prefs serverside previous change is breaking playwright tests, need to check later changes --- .../src/lib/components/ListToolbar.svelte | 13 ++++---- .../src/lib/components/ResourceList.svelte | 8 ++--- .../src/lib/components/SkeletonList.svelte | 8 ++--- frontend/src/lib/stores/files.svelte.ts | 26 +++++++++++----- frontend/src/lib/stores/files.test.ts | 12 ++++--- frontend/src/lib/stores/preferences.svelte.ts | 31 ++++++++----------- .../src/routes/files/[...path]/+page.svelte | 4 +-- frontend/src/routes/files/page.test.ts | 9 ++---- 8 files changed, 57 insertions(+), 54 deletions(-) diff --git a/frontend/src/lib/components/ListToolbar.svelte b/frontend/src/lib/components/ListToolbar.svelte index dbef38c8..10819273 100644 --- a/frontend/src/lib/components/ListToolbar.svelte +++ b/frontend/src/lib/components/ListToolbar.svelte @@ -12,6 +12,7 @@ import type { Snippet } from 'svelte'; import Icon from '$lib/icons/Icon.svelte'; import { t } from '$lib/i18n/index.svelte'; + import { files as filesStore } from '$lib/stores/files.svelte'; import { preferences } from '$lib/stores/preferences.svelte'; interface Props { @@ -126,19 +127,19 @@ {#if showViewToggle} filesStore.setViewMode('grid')}> filesStore.setViewMode('list')}> {/if} {#if showDotfileToggle} diff --git a/frontend/src/lib/components/ResourceList.svelte b/frontend/src/lib/components/ResourceList.svelte index 6953c916..8b690ab9 100644 --- a/frontend/src/lib/components/ResourceList.svelte +++ b/frontend/src/lib/components/ResourceList.svelte @@ -58,7 +58,7 @@ import UserVignette from '$lib/components/UserVignette.svelte'; import VirtualList from '$lib/components/VirtualList.svelte'; import { t } from '$lib/i18n/index.svelte'; - import { preferences } from '$lib/stores/preferences.svelte'; + import { files as filesStore } from '$lib/stores/files.svelte'; import { formatBytes } from '$lib/utils/format'; import { formatDate, iconNameFromClass, fileIconKindClass } from '$lib/utils/display'; import { gridColumns } from '$lib/utils/grid'; @@ -164,7 +164,7 @@ const isEmpty = $derived(items.length === 0); const viewClass = $derived( - preferences.viewMode === 'grid' ? 'files-grid-view' : 'files-list-view' + filesStore.viewMode === 'grid' ? 'files-grid-view' : 'files-list-view' ); /** Content width, for computing the grid's column count to match auto-fill. */ let gridWidth = $state(0); @@ -458,7 +458,7 @@ {/if} - {#if preferences.viewMode === 'list'} + {#if filesStore.viewMode === 'list'}
diff --git a/frontend/src/lib/components/SkeletonList.svelte b/frontend/src/lib/components/SkeletonList.svelte index 39bd5755..6eea2cb9 100644 --- a/frontend/src/lib/components/SkeletonList.svelte +++ b/frontend/src/lib/components/SkeletonList.svelte @@ -1,5 +1,5 @@
-
+
{#each placeholders as i (i)} - {#if preferences.viewMode === 'grid'} + {#if filesStore.viewMode === 'grid'}
diff --git a/frontend/src/lib/stores/files.svelte.ts b/frontend/src/lib/stores/files.svelte.ts index 3dc3db45..8ab82011 100644 --- a/frontend/src/lib/stores/files.svelte.ts +++ b/frontend/src/lib/stores/files.svelte.ts @@ -72,23 +72,35 @@ export type Section = | 'photos' | 'music'; -// `viewMode` used to live here (localStorage `oxi-view-mode`), but -// moved to the server-side `ui_preferences` bag so the choice -// follows the user across devices. Read via -// `preferences.viewMode` and mutate via `preferences.setViewMode` -// (`lib/stores/preferences.svelte.ts`). Kept `ViewMode` as an -// exported type because template code still needs it for prop -// annotations without pulling in the whole preferences module. +const VIEW_KEY = 'oxi-view-mode'; + +function readViewMode(): ViewMode { + if (typeof localStorage === 'undefined') return 'grid'; + return localStorage.getItem(VIEW_KEY) === 'list' ? 'list' : 'grid'; +} class FilesStore { currentFolder = $state(null); currentFolderInfo = $state(null); breadcrumbPath = $state>([]); + // View mode INTENTIONALLY lives here (localStorage) rather than in + // the server-side `preferences` bag. See the note in + // `preferences.svelte.ts::UiPreferences` for the full rationale — + // short version: server persistence broke Playwright test + // isolation (favorites.spec's list-view click leaked into every + // downstream test's context), and view mode isn't a preference + // users have asked to sync across devices. + viewMode = $state(readViewMode()); section = $state
('files'); isSearchMode = $state(false); // Reactive set: in-place mutations below drive template/$derived reads. selection = new SvelteSet(); + setViewMode(mode: ViewMode): void { + this.viewMode = mode; + if (typeof localStorage !== 'undefined') localStorage.setItem(VIEW_KEY, mode); + } + clearSelection(): void { this.selection.clear(); } diff --git a/frontend/src/lib/stores/files.test.ts b/frontend/src/lib/stores/files.test.ts index bd757415..661e4f0b 100644 --- a/frontend/src/lib/stores/files.test.ts +++ b/frontend/src/lib/stores/files.test.ts @@ -27,11 +27,13 @@ it('shows the owner as "Me" for the current user and a short id otherwise', () = expect(ownerLabel('abcdef123456', 'someone-else')).toBe('abcdef12'); }); -// View-mode assertions moved to `preferences.svelte.test.ts` — the -// setting now lives on the server-side `ui_preferences` bag via the -// `preferences` store, not on `FilesStore`. What remains of `FilesStore` -// is navigation + selection state, exercised below. -it('toggles selection', () => { +it('persists the view mode and toggles selection', () => { + files.setViewMode('list'); + expect(files.viewMode).toBe('list'); + expect(localStorage.getItem('oxi-view-mode')).toBe('list'); + files.setViewMode('grid'); + expect(files.viewMode).toBe('grid'); + files.clearSelection(); expect(files.selection.size).toBe(0); files.toggleSelected('a'); diff --git a/frontend/src/lib/stores/preferences.svelte.ts b/frontend/src/lib/stores/preferences.svelte.ts index 2ab3e26a..e9e141d0 100644 --- a/frontend/src/lib/stores/preferences.svelte.ts +++ b/frontend/src/lib/stores/preferences.svelte.ts @@ -41,22 +41,23 @@ export interface UiPreferences { * preserved on upload, matching Nextcloud / ownCloud / Seafile. */ hide_dotfiles?: boolean; - /** - * App-wide file list view: grid tiles or list rows. Default - * `'grid'`. Migrated from the localStorage `oxi-view-mode` key - * so the choice follows the user across devices — muscle memory - * for "I use list on my laptop, grid on my tablet" is rare; - * consistency across devices is the common case. Public-share - * viewers still use `oxi-share-view` (localStorage) because - * anonymous consumers have no server preferences. - */ - view_mode?: 'grid' | 'list'; + // NOTE: view_mode (grid/list) DELIBERATELY stays in localStorage + // (`oxi-view-mode` on `filesStore`). Making it server-persistent + // caused a real Playwright regression: `favorites.spec.ts` clicks + // the list-view toggle, and on the server-backed store that + // preference would then leak into every downstream test's fresh + // browser context — Playwright's default context isolation + // relies on localStorage being fresh per test, which the server + // bag can't provide. Result: files-extra's `Zip-*` folder fell + // outside list view's smaller virtualisation window (~25 vs ~75 + // grid items) and `getByTestId` timed out. Google Drive / Finder + // / Dropbox also keep view mode per-device — the sync-across- + // devices UX isn't a strongly-requested pattern. } /** Reasonable default for an empty bag or a missing key. */ const DEFAULTS: Required = { - hide_dotfiles: false, - view_mode: 'grid' + hide_dotfiles: false }; /** @@ -84,8 +85,6 @@ class PreferencesStore { : DEFAULTS.hide_dotfiles ); - viewMode = $derived<'grid' | 'list'>(this.bag.view_mode === 'list' ? 'list' : DEFAULTS.view_mode); - // ── Mutations ───────────────────────────────────────────────── private patchTimer: ReturnType | null = null; @@ -158,10 +157,6 @@ class PreferencesStore { toggleHideDotfiles(): void { this.setHideDotfiles(!this.hideDotfiles); } - - setViewMode(mode: 'grid' | 'list'): void { - this.set({ view_mode: mode }); - } } export const preferences = new PreferencesStore(); diff --git a/frontend/src/routes/files/[...path]/+page.svelte b/frontend/src/routes/files/[...path]/+page.svelte index ad56c7c6..51cdeb6f 100644 --- a/frontend/src/routes/files/[...path]/+page.svelte +++ b/frontend/src/routes/files/[...path]/+page.svelte @@ -1366,7 +1366,7 @@ // hiding N items" hint so users aren't confused. const isEmpty = $derived(visibleFolders.length === 0 && visibleFiles.length === 0); const viewClass = $derived( - preferences.viewMode === 'grid' ? 'files-grid-view' : 'files-list-view' + filesStore.viewMode === 'grid' ? 'files-grid-view' : 'files-list-view' ); // Client-side sort (flat, Drive-style). The listing endpoint returns the @@ -1806,7 +1806,7 @@ {/each} {/each}
- {:else if preferences.viewMode === 'list'} + {:else if filesStore.viewMode === 'list'}
{@render fileListHeader()} diff --git a/frontend/src/routes/files/page.test.ts b/frontend/src/routes/files/page.test.ts index e2d01bb3..bde2b0d1 100644 --- a/frontend/src/routes/files/page.test.ts +++ b/frontend/src/routes/files/page.test.ts @@ -62,7 +62,7 @@ vi.mock('$lib/api/endpoints/folders', () => ({ import { fetchFolderListing, createFolder, deleteFolder } from '$lib/api/endpoints/folders'; import { deleteFile } from '$lib/api/endpoints/files'; import { apiFetch } from '$lib/api/client'; -import { preferences } from '$lib/stores/preferences.svelte'; +import { files as filesStore } from '$lib/stores/files.svelte'; import FilesPage from './[...path]/+page.svelte'; const m = (fn: unknown) => fn as ReturnType; @@ -126,12 +126,7 @@ beforeEach(() => { // listing-oriented tests target a folder directly. pageState.params.path = 'home'; // List view renders the select-all header + per-row checkboxes; grid hides them. - // The store's `setViewMode` writes through to the server bag via a - // debounced PATCH; in the test harness there's no session so the - // PATCH silently no-ops on the network side but the optimistic local - // mutation (session.user.ui_preferences.view_mode) still lands and - // downstream `preferences.viewMode` re-derives to 'list'. - preferences.setViewMode('list'); + filesStore.viewMode = 'list'; }); it('loads the home folder listing on mount and renders its contents', async () => {