fix: restore current page after refresh
This commit is contained in:
+24
-7
@@ -6,7 +6,7 @@ import {
|
|||||||
Settings, Trash2, X, Repeat2,
|
Settings, Trash2, X, Repeat2,
|
||||||
} from 'lucide-vue-next'
|
} from 'lucide-vue-next'
|
||||||
import { buildTaskRrule, defaultTaskDueAt, filterTasks, fromDateTimeLocal, groupTaskTree, moveItemWithinScope, parseTaskRrule, renderMarkdown, toDateTimeLocal, type TaskRepeatConfig } from './lib/task-utils'
|
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 { csrfHeader } from './lib/csrf'
|
||||||
import MvpPanel from './MvpPanel.vue'
|
import MvpPanel from './MvpPanel.vue'
|
||||||
import CountdownPanel from './CountdownPanel.vue'
|
import CountdownPanel from './CountdownPanel.vue'
|
||||||
@@ -30,8 +30,10 @@ const archivedLists = ref<TaskList[]>([])
|
|||||||
const tasks = ref<Task[]>([])
|
const tasks = ref<Task[]>([])
|
||||||
const overdueTasks = ref<Task[]>([])
|
const overdueTasks = ref<Task[]>([])
|
||||||
const trash = ref<Task[]>([])
|
const trash = ref<Task[]>([])
|
||||||
const activeList = ref('')
|
const NAVIGATION_STORAGE_KEY = 'dodo.navigation'
|
||||||
const activeView = ref<View>(defaultView())
|
const restoredNavigation = readStoredNavigation(window.localStorage, NAVIGATION_STORAGE_KEY)
|
||||||
|
const activeList = ref(restoredNavigation.listId)
|
||||||
|
const activeView = ref<View>(restoredNavigation.view)
|
||||||
const selectedTask = ref<Task | null>(null)
|
const selectedTask = ref<Task | null>(null)
|
||||||
const query = ref('')
|
const query = ref('')
|
||||||
const error = ref('')
|
const error = ref('')
|
||||||
@@ -274,6 +276,20 @@ function toast(message: string) {
|
|||||||
}
|
}
|
||||||
function fail(reason: unknown) { error.value = reason instanceof Error ? reason.message : '请求失败' }
|
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() {
|
async function bootstrap() {
|
||||||
authReady.value = false
|
authReady.value = false
|
||||||
try {
|
try {
|
||||||
@@ -285,11 +301,11 @@ async function bootstrap() {
|
|||||||
folders.value = data.folders ?? []
|
folders.value = data.folders ?? []
|
||||||
lists.value = data.lists ?? []
|
lists.value = data.lists ?? []
|
||||||
navigationLoaded.value = true
|
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))
|
expandedFolders.value = new Set(folders.value.map((folder) => folder.id))
|
||||||
await loadArchivedLists()
|
await loadArchivedLists()
|
||||||
void preloadCountdowns()
|
void preloadCountdowns()
|
||||||
await loadAll()
|
await loadRestoredView()
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
authenticated.value = false
|
authenticated.value = false
|
||||||
@@ -308,10 +324,10 @@ async function submitAuth() {
|
|||||||
folders.value = data.folders ?? []
|
folders.value = data.folders ?? []
|
||||||
lists.value = data.lists ?? []
|
lists.value = data.lists ?? []
|
||||||
navigationLoaded.value = true
|
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))
|
expandedFolders.value = new Set(folders.value.map((folder) => folder.id))
|
||||||
void preloadCountdowns()
|
void preloadCountdowns()
|
||||||
await loadAll()
|
await loadRestoredView()
|
||||||
} catch (reason) { fail(reason) }
|
} catch (reason) { fail(reason) }
|
||||||
}
|
}
|
||||||
async function loadTaskPages(path: string) {
|
async function loadTaskPages(path: string) {
|
||||||
@@ -393,6 +409,7 @@ async function loadTrash() {
|
|||||||
async function switchView(view: View, listId?: string) {
|
async function switchView(view: View, listId?: string) {
|
||||||
activeView.value = view
|
activeView.value = view
|
||||||
if (listId) activeList.value = listId
|
if (listId) activeList.value = listId
|
||||||
|
writeStoredNavigation(window.localStorage, NAVIGATION_STORAGE_KEY, view, activeList.value)
|
||||||
page.value = 1
|
page.value = 1
|
||||||
selectedTask.value = null; mobileSidebar.value = false; mobileDetail.value = false; mobileMore.value = false; taskComposeOpen.value = false
|
selectedTask.value = null; mobileSidebar.value = false; mobileDetail.value = false; mobileMore.value = false; taskComposeOpen.value = false
|
||||||
if (view === 'trash') await loadTrash()
|
if (view === 'trash') await loadTrash()
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, expect, it } from 'vitest'
|
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', () => {
|
describe('MVP view utilities', () => {
|
||||||
it('formats a local date as YYYY-MM-DD', () => {
|
it('formats a local date as YYYY-MM-DD', () => {
|
||||||
@@ -17,6 +17,19 @@ describe('MVP view utilities', () => {
|
|||||||
expect(['habits', 'settings', 'trash'].filter(isTaskView)).toEqual([])
|
expect(['habits', 'settings', 'trash'].filter(isTaskView)).toEqual([])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('restores the last page and selected task list after refresh', () => {
|
||||||
|
const storage = new Map<string, string>()
|
||||||
|
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', () => {
|
it('only completes numeric habits when progress reaches the target', () => {
|
||||||
expect(isHabitComplete('numeric', 3, 5)).toBe(false)
|
expect(isHabitComplete('numeric', 3, 5)).toBe(false)
|
||||||
expect(isHabitComplete('numeric', 5, 5)).toBe(true)
|
expect(isHabitComplete('numeric', 5, 5)).toBe(true)
|
||||||
|
|||||||
@@ -1,4 +1,21 @@
|
|||||||
type BooleanStorage = Pick<Storage, 'getItem' | 'setItem'>
|
type BooleanStorage = Pick<Storage, 'getItem' | 'setItem'>
|
||||||
|
type NavigationView = 'tasks' | 'today' | 'upcoming' | 'trash' | 'habits' | 'countdowns' | 'settings'
|
||||||
|
type StoredNavigation = { view: NavigationView; listId: string }
|
||||||
|
const NAVIGATION_VIEWS = new Set<NavigationView>(['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) {
|
export function readStoredBoolean(storage: BooleanStorage, key: string, fallback: boolean) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -120,6 +120,12 @@ describe('task and habit row decoration', () => {
|
|||||||
expect(css).toContain('.habit-progress{margin-left:auto;')
|
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', () => {
|
it('restores the completed-item visibility choices from local storage', () => {
|
||||||
expect(app).toContain("readStoredBoolean(window.localStorage, SHOW_COMPLETED_STORAGE_KEY, true)")
|
expect(app).toContain("readStoredBoolean(window.localStorage, SHOW_COMPLETED_STORAGE_KEY, true)")
|
||||||
expect(app).toContain("writeStoredBoolean(window.localStorage, SHOW_COMPLETED_STORAGE_KEY, value)")
|
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', () => {
|
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', () => {
|
it('styles archived rows through the new list-row-main structure', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user