fix: persist completed item visibility
This commit is contained in:
@@ -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, shouldToggleRowSwipe, writeCountdownCache } from './lib/mvp-utils'
|
import { defaultView, isTaskView, nextTotalAfterLocalTaskAdd, readStoredBoolean, shouldToggleRowSwipe, writeCountdownCache, writeStoredBoolean } 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'
|
||||||
@@ -43,7 +43,8 @@ const mobileDetail = ref(false)
|
|||||||
const mobileMore = ref(false)
|
const mobileMore = ref(false)
|
||||||
const moreSettingsOpen = ref(false)
|
const moreSettingsOpen = ref(false)
|
||||||
const markdownPreview = ref(false)
|
const markdownPreview = ref(false)
|
||||||
const showCompleted = ref(true)
|
const SHOW_COMPLETED_STORAGE_KEY = 'dodo.show-completed'
|
||||||
|
const showCompleted = ref(readStoredBoolean(window.localStorage, SHOW_COMPLETED_STORAGE_KEY, true))
|
||||||
const page = ref(1)
|
const page = ref(1)
|
||||||
const pageSize = 50
|
const pageSize = 50
|
||||||
const totalTasks = ref(0)
|
const totalTasks = ref(0)
|
||||||
@@ -242,7 +243,8 @@ watch(query, () => {
|
|||||||
page.value = 1
|
page.value = 1
|
||||||
searchTimer = window.setTimeout(() => loadAll(), 250)
|
searchTimer = window.setTimeout(() => loadAll(), 250)
|
||||||
})
|
})
|
||||||
watch(showCompleted, () => {
|
watch(showCompleted, (value) => {
|
||||||
|
writeStoredBoolean(window.localStorage, SHOW_COMPLETED_STORAGE_KEY, value)
|
||||||
if (isTaskView(activeView.value)) { page.value = 1; loadAll() }
|
if (isTaskView(activeView.value)) { page.value = 1; loadAll() }
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
|
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||||
import { Activity, ArchiveRestore, Check, Download, FileJson, GripVertical, LogOut, Trash2, X } from 'lucide-vue-next'
|
import { Activity, ArchiveRestore, Check, Download, FileJson, GripVertical, LogOut, Trash2, X } from 'lucide-vue-next'
|
||||||
import { moveItemWithinScope } from './lib/task-utils'
|
import { moveItemWithinScope } from './lib/task-utils'
|
||||||
import { dateKey, habitButtonNotice, habitButtonValue, isHabitComplete, isHabitScheduledToday, mergePage, nextHabitSwipeValue, previousHabitSwipeValue, readHabitGridCache, shouldToggleRowSwipe, writeHabitGridCache } from './lib/mvp-utils'
|
import { dateKey, habitButtonNotice, habitButtonValue, isHabitComplete, isHabitScheduledToday, mergePage, nextHabitSwipeValue, previousHabitSwipeValue, readHabitGridCache, readStoredBoolean, shouldToggleRowSwipe, writeHabitGridCache, writeStoredBoolean } from './lib/mvp-utils'
|
||||||
import { csrfHeader } from './lib/csrf'
|
import { csrfHeader } from './lib/csrf'
|
||||||
|
|
||||||
type View = 'habits' | 'today-habits' | 'settings'
|
type View = 'habits' | 'today-habits' | 'settings'
|
||||||
@@ -34,10 +34,12 @@ const habitPointerStart = ref<{ id: string; x: number; y: number } | null>(null)
|
|||||||
const habitSwipeOffsets = ref<Record<string, number>>({})
|
const habitSwipeOffsets = ref<Record<string, number>>({})
|
||||||
const habitReorder = ref<{ id: string; startY: number; offsetY: number } | null>(null)
|
const habitReorder = ref<{ id: string; startY: number; offsetY: number } | null>(null)
|
||||||
const habitReorderTarget = ref('')
|
const habitReorderTarget = ref('')
|
||||||
const hideCompletedHabits = ref(false)
|
const HIDE_COMPLETED_HABITS_STORAGE_KEY = 'dodo.hide-completed-habits'
|
||||||
|
const hideCompletedHabits = ref(readStoredBoolean(window.localStorage, HIDE_COMPLETED_HABITS_STORAGE_KEY, false))
|
||||||
const todayHabits = computed(() => habits.value.filter((item) => isHabitScheduledToday(item, todayKey.value)))
|
const todayHabits = computed(() => habits.value.filter((item) => isHabitScheduledToday(item, todayKey.value)))
|
||||||
const visibleTodayHabits = computed(() => hideCompletedHabits.value ? todayHabits.value.filter((item) => !isDone(item, todayKey.value)) : todayHabits.value)
|
const visibleTodayHabits = computed(() => hideCompletedHabits.value ? todayHabits.value.filter((item) => !isDone(item, todayKey.value)) : todayHabits.value)
|
||||||
const visibleHabits = computed(() => hideCompletedHabits.value ? habits.value.filter((item) => !isDone(item, todayKey.value)) : habits.value)
|
const visibleHabits = computed(() => hideCompletedHabits.value ? habits.value.filter((item) => !isDone(item, todayKey.value)) : habits.value)
|
||||||
|
watch(hideCompletedHabits, (value) => writeStoredBoolean(window.localStorage, HIDE_COMPLETED_HABITS_STORAGE_KEY, value))
|
||||||
let dayRolloverTimer: ReturnType<typeof setInterval> | undefined
|
let dayRolloverTimer: ReturnType<typeof setInterval> | undefined
|
||||||
|
|
||||||
function formatErrorMessage(detail: unknown): string {
|
function formatErrorMessage(detail: unknown): string {
|
||||||
|
|||||||
@@ -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, shouldToggleRowSwipe, writeCountdownCache, writeHabitGridCache } 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, shouldToggleRowSwipe, writeCountdownCache, writeHabitGridCache, writeStoredBoolean } 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', () => {
|
||||||
@@ -77,6 +77,19 @@ describe('MVP view utilities', () => {
|
|||||||
expect(habitButtonNotice('boolean', 1, 0)).toBe('已取消完成')
|
expect(habitButtonNotice('boolean', 1, 0)).toBe('已取消完成')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('persists boolean display preferences across page refreshes', () => {
|
||||||
|
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(readStoredBoolean(fakeStorage, 'dodo.show-completed', true)).toBe(true)
|
||||||
|
writeStoredBoolean(fakeStorage, 'dodo.show-completed', false)
|
||||||
|
expect(readStoredBoolean(fakeStorage, 'dodo.show-completed', true)).toBe(false)
|
||||||
|
storage.set('dodo.show-completed', 'invalid')
|
||||||
|
expect(readStoredBoolean(fakeStorage, 'dodo.show-completed', true)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
it('reuses the current week habit grid while refreshing in the background', () => {
|
it('reuses the current week habit grid while refreshing in the background', () => {
|
||||||
const habits = [{ id: 'habit-1', name: '喝水' }]
|
const habits = [{ id: 'habit-1', name: '喝水' }]
|
||||||
writeHabitGridCache('2026-09-07', habits)
|
writeHabitGridCache('2026-09-07', habits)
|
||||||
|
|||||||
@@ -1,3 +1,18 @@
|
|||||||
|
type BooleanStorage = Pick<Storage, 'getItem' | 'setItem'>
|
||||||
|
|
||||||
|
export function readStoredBoolean(storage: BooleanStorage, key: string, fallback: boolean) {
|
||||||
|
try {
|
||||||
|
const value = storage.getItem(key)
|
||||||
|
if (value === 'true') return true
|
||||||
|
if (value === 'false') return false
|
||||||
|
} catch { /* storage may be unavailable */ }
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
export function writeStoredBoolean(storage: BooleanStorage, key: string, value: boolean) {
|
||||||
|
try { storage.setItem(key, String(value)) } catch { /* storage may be unavailable */ }
|
||||||
|
}
|
||||||
|
|
||||||
export function dateKey(date: Date) {
|
export function dateKey(date: Date) {
|
||||||
const y = date.getFullYear()
|
const y = date.getFullYear()
|
||||||
const m = `${date.getMonth() + 1}`.padStart(2, '0')
|
const m = `${date.getMonth() + 1}`.padStart(2, '0')
|
||||||
|
|||||||
@@ -120,8 +120,15 @@ 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 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)")
|
||||||
|
expect(mvpPanel).toContain("readStoredBoolean(window.localStorage, HIDE_COMPLETED_HABITS_STORAGE_KEY, false)")
|
||||||
|
expect(mvpPanel).toContain("writeStoredBoolean(window.localStorage, HIDE_COMPLETED_HABITS_STORAGE_KEY, value)")
|
||||||
|
})
|
||||||
|
|
||||||
it('can hide completed habits in both Today and full habit views', () => {
|
it('can hide completed habits in both Today and full habit views', () => {
|
||||||
expect(mvpPanel).toContain('const hideCompletedHabits = ref(false)')
|
expect(mvpPanel).toContain("const hideCompletedHabits = ref(readStoredBoolean(window.localStorage, HIDE_COMPLETED_HABITS_STORAGE_KEY, false))")
|
||||||
expect(mvpPanel).toContain('const visibleTodayHabits = computed')
|
expect(mvpPanel).toContain('const visibleTodayHabits = computed')
|
||||||
expect(mvpPanel).toContain('const visibleHabits = computed')
|
expect(mvpPanel).toContain('const visibleHabits = computed')
|
||||||
expect(mvpPanel).toContain('v-model="hideCompletedHabits"')
|
expect(mvpPanel).toContain('v-model="hideCompletedHabits"')
|
||||||
|
|||||||
Reference in New Issue
Block a user