From 61f8c27e6c4a68f89f4ace88045200fc9027d6db Mon Sep 17 00:00:00 2001 From: bboysoul Date: Tue, 8 Sep 2026 12:50:36 +0800 Subject: [PATCH] fix: restore current page after refresh --- frontend/src/App.vue | 31 +++++++++++++++++++++++------- frontend/src/lib/mvp-utils.test.ts | 15 ++++++++++++++- frontend/src/lib/mvp-utils.ts | 17 ++++++++++++++++ frontend/src/style.test.ts | 8 +++++++- 4 files changed, 62 insertions(+), 9 deletions(-) diff --git a/frontend/src/App.vue b/frontend/src/App.vue index d1e5cb1..8c42f8c 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -6,7 +6,7 @@ import { Settings, Trash2, X, Repeat2, } from 'lucide-vue-next' import { buildTaskRrule, defaultTaskDueAt, filterTasks, fromDateTimeLocal, groupTaskTree, moveItemWithinScope, parseTaskRrule, renderMarkdown, toDateTimeLocal, type TaskRepeatConfig } from './lib/task-utils' -import { defaultView, isTaskView, nextTotalAfterLocalTaskAdd, readStoredBoolean, shouldToggleRowSwipe, writeCountdownCache, writeStoredBoolean } from './lib/mvp-utils' +import { isTaskView, nextTotalAfterLocalTaskAdd, readStoredBoolean, readStoredNavigation, shouldToggleRowSwipe, writeCountdownCache, writeStoredBoolean, writeStoredNavigation } from './lib/mvp-utils' import { csrfHeader } from './lib/csrf' import MvpPanel from './MvpPanel.vue' import CountdownPanel from './CountdownPanel.vue' @@ -30,8 +30,10 @@ const archivedLists = ref([]) const tasks = ref([]) const overdueTasks = ref([]) const trash = ref([]) -const activeList = ref('') -const activeView = ref(defaultView()) +const NAVIGATION_STORAGE_KEY = 'dodo.navigation' +const restoredNavigation = readStoredNavigation(window.localStorage, NAVIGATION_STORAGE_KEY) +const activeList = ref(restoredNavigation.listId) +const activeView = ref(restoredNavigation.view) const selectedTask = ref(null) const query = ref('') const error = ref('') @@ -274,6 +276,20 @@ function toast(message: string) { } function fail(reason: unknown) { error.value = reason instanceof Error ? reason.message : '请求失败' } +function restoreNavigation(inboxId: string) { + if (restoredNavigation.view === 'tasks' && restoredNavigation.listId) { + activeList.value = lists.value.some((item) => item.id === restoredNavigation.listId) ? restoredNavigation.listId : inboxId + } else { + activeList.value = inboxId + } +} + +async function loadRestoredView() { + if (activeView.value === 'trash') await loadTrash() + else if (isTaskView(activeView.value)) await loadAll() + else { tasks.value = []; totalTasks.value = 0 } +} + async function bootstrap() { authReady.value = false try { @@ -285,11 +301,11 @@ async function bootstrap() { folders.value = data.folders ?? [] lists.value = data.lists ?? [] navigationLoaded.value = true - activeList.value = data.inbox_id || lists.value.find((item: TaskList) => item.is_inbox)?.id || lists.value[0]?.id || '' + restoreNavigation(data.inbox_id || lists.value.find((item: TaskList) => item.is_inbox)?.id || lists.value[0]?.id || '') expandedFolders.value = new Set(folders.value.map((folder) => folder.id)) await loadArchivedLists() void preloadCountdowns() - await loadAll() + await loadRestoredView() } } catch { authenticated.value = false @@ -308,10 +324,10 @@ async function submitAuth() { folders.value = data.folders ?? [] lists.value = data.lists ?? [] navigationLoaded.value = true - activeList.value = data.inbox_id || lists.value.find((item: TaskList) => item.is_inbox)?.id || lists.value[0]?.id || '' + restoreNavigation(data.inbox_id || lists.value.find((item: TaskList) => item.is_inbox)?.id || lists.value[0]?.id || '') expandedFolders.value = new Set(folders.value.map((folder) => folder.id)) void preloadCountdowns() - await loadAll() + await loadRestoredView() } catch (reason) { fail(reason) } } async function loadTaskPages(path: string) { @@ -393,6 +409,7 @@ async function loadTrash() { async function switchView(view: View, listId?: string) { activeView.value = view if (listId) activeList.value = listId + writeStoredNavigation(window.localStorage, NAVIGATION_STORAGE_KEY, view, activeList.value) page.value = 1 selectedTask.value = null; mobileSidebar.value = false; mobileDetail.value = false; mobileMore.value = false; taskComposeOpen.value = false if (view === 'trash') await loadTrash() diff --git a/frontend/src/lib/mvp-utils.test.ts b/frontend/src/lib/mvp-utils.test.ts index 875d938..056cef7 100644 --- a/frontend/src/lib/mvp-utils.test.ts +++ b/frontend/src/lib/mvp-utils.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import { calendarModeLabel, clampFabPosition, countdownDayText, countdownKindLabel, dateKey, defaultView, habitButtonNotice, habitButtonValue, habitWeek, isFabDrag, isHabitComplete, isHabitScheduledToday, isTaskView, nextHabitSwipeValue, nextTotalAfterLocalTaskAdd, numericHabitInputValue, previousHabitSwipeValue, quickTaskFields, readCountdownCache, readHabitGridCache, readStoredBoolean, shouldToggleRowSwipe, writeCountdownCache, writeHabitGridCache, writeStoredBoolean } from './mvp-utils' +import { calendarModeLabel, clampFabPosition, countdownDayText, countdownKindLabel, dateKey, defaultView, habitButtonNotice, habitButtonValue, habitWeek, isFabDrag, isHabitComplete, isHabitScheduledToday, isTaskView, nextHabitSwipeValue, nextTotalAfterLocalTaskAdd, numericHabitInputValue, previousHabitSwipeValue, quickTaskFields, readCountdownCache, readHabitGridCache, readStoredBoolean, readStoredNavigation, shouldToggleRowSwipe, writeCountdownCache, writeHabitGridCache, writeStoredBoolean, writeStoredNavigation } from './mvp-utils' describe('MVP view utilities', () => { it('formats a local date as YYYY-MM-DD', () => { @@ -17,6 +17,19 @@ describe('MVP view utilities', () => { expect(['habits', 'settings', 'trash'].filter(isTaskView)).toEqual([]) }) + it('restores the last page and selected task list after refresh', () => { + const storage = new Map() + const fakeStorage = { + getItem: (key: string) => storage.get(key) ?? null, + setItem: (key: string, value: string) => storage.set(key, value), + } + expect(readStoredNavigation(fakeStorage, 'dodo.navigation')).toEqual({ view: 'today', listId: '' }) + writeStoredNavigation(fakeStorage, 'dodo.navigation', 'tasks', 'list-2') + expect(readStoredNavigation(fakeStorage, 'dodo.navigation')).toEqual({ view: 'tasks', listId: 'list-2' }) + storage.set('dodo.navigation', JSON.stringify({ view: 'invalid', listId: 'list-2' })) + expect(readStoredNavigation(fakeStorage, 'dodo.navigation')).toEqual({ view: 'today', listId: '' }) + }) + it('only completes numeric habits when progress reaches the target', () => { expect(isHabitComplete('numeric', 3, 5)).toBe(false) expect(isHabitComplete('numeric', 5, 5)).toBe(true) diff --git a/frontend/src/lib/mvp-utils.ts b/frontend/src/lib/mvp-utils.ts index 3053e7d..936a0ed 100644 --- a/frontend/src/lib/mvp-utils.ts +++ b/frontend/src/lib/mvp-utils.ts @@ -1,4 +1,21 @@ type BooleanStorage = Pick +type NavigationView = 'tasks' | 'today' | 'upcoming' | 'trash' | 'habits' | 'countdowns' | 'settings' +type StoredNavigation = { view: NavigationView; listId: string } +const NAVIGATION_VIEWS = new Set(['tasks', 'today', 'upcoming', 'trash', 'habits', 'countdowns', 'settings']) + +export function readStoredNavigation(storage: BooleanStorage, key: string): StoredNavigation { + try { + const value = JSON.parse(storage.getItem(key) ?? 'null') + if (value && NAVIGATION_VIEWS.has(value.view) && typeof value.listId === 'string') { + return { view: value.view, listId: value.listId } + } + } catch { /* storage may be unavailable or invalid */ } + return { view: defaultView(), listId: '' } +} + +export function writeStoredNavigation(storage: BooleanStorage, key: string, view: NavigationView, listId: string) { + try { storage.setItem(key, JSON.stringify({ view, listId })) } catch { /* storage may be unavailable */ } +} export function readStoredBoolean(storage: BooleanStorage, key: string, fallback: boolean) { try { diff --git a/frontend/src/style.test.ts b/frontend/src/style.test.ts index 8ec1620..46acc34 100644 --- a/frontend/src/style.test.ts +++ b/frontend/src/style.test.ts @@ -120,6 +120,12 @@ describe('task and habit row decoration', () => { expect(css).toContain('.habit-progress{margin-left:auto;') }) + it('restores the current page and list from local storage', () => { + expect(app).toContain("readStoredNavigation(window.localStorage, NAVIGATION_STORAGE_KEY)") + expect(app).toContain("writeStoredNavigation(window.localStorage, NAVIGATION_STORAGE_KEY, view, activeList.value)") + expect(app).toContain('if (restoredNavigation.view === \'tasks\' && restoredNavigation.listId)') + }) + it('restores the completed-item visibility choices from local storage', () => { expect(app).toContain("readStoredBoolean(window.localStorage, SHOW_COMPLETED_STORAGE_KEY, true)") expect(app).toContain("writeStoredBoolean(window.localStorage, SHOW_COMPLETED_STORAGE_KEY, value)") @@ -366,7 +372,7 @@ describe('sidebar layout', () => { }) it('loads archived lists during bootstrap so archived rows are visible after refresh', () => { - expect(app).toContain('expandedFolders.value = new Set(folders.value.map((folder) => folder.id))\n await loadArchivedLists()\n void preloadCountdowns()\n await loadAll()') + expect(app).toContain('expandedFolders.value = new Set(folders.value.map((folder) => folder.id))\n await loadArchivedLists()\n void preloadCountdowns()\n await loadRestoredView()') }) it('styles archived rows through the new list-row-main structure', () => {