Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 109 additions & 0 deletions packages/app-core/src/store.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// @vitest-environment jsdom

import { beforeEach, describe, expect, it, vi } from 'vitest'
import { TASKS_TAB_PATH, type VaultTask } from '@shared/tasks'

function makeTask(content: string, taskIndex = 0): VaultTask {
return {
id: `inbox/Note.md#${taskIndex}`,
sourcePath: 'inbox/Note.md',
noteTitle: 'Note',
noteFolder: 'inbox',
lineNumber: taskIndex,
taskIndex,
rawText: `- [ ] ${content}`,
content,
checked: false,
waiting: false,
tags: []
}
}

function makeNote(body: string) {
return {
path: 'inbox/Note.md',
title: 'Note',
folder: 'inbox' as const,
siblingOrder: 0,
createdAt: 0,
updatedAt: 1,
size: body.length,
tags: [],
wikilinks: [],
hasAttachments: false,
excerpt: body,
body
}
}

function installZen(overrides: Record<string, unknown> = {}): void {
Object.defineProperty(window, 'zen', {
configurable: true,
value: {
scanTasks: vi.fn().mockResolvedValue([]),
scanTasksForPath: vi.fn().mockResolvedValue([]),
listNotes: vi.fn().mockResolvedValue([makeNote('- [ ] old task')]),
listFolders: vi.fn().mockResolvedValue([]),
listAssets: vi.fn().mockResolvedValue([]),
hasAssetsDir: vi.fn().mockResolvedValue(false),
readNote: vi.fn().mockResolvedValue(makeNote('- [ ] old task')),
...overrides
}
})
}

async function loadStore() {
vi.resetModules()
localStorage.clear()
return import('./store')
}

async function flushAsyncWork(): Promise<void> {
await new Promise((resolve) => window.setTimeout(resolve, 0))
}

beforeEach(() => {
vi.restoreAllMocks()
})

describe('tasks cache freshness', () => {
it('refreshes tasks when focusing an existing Tasks tab', async () => {
const freshTasks = [makeTask('new task')]
const scanTasks = vi.fn().mockResolvedValue(freshTasks)
installZen({ scanTasks })

const { useStore } = await loadStore()
const paneId = useStore.getState().activePaneId
await useStore.getState().openNoteInPane(paneId, TASKS_TAB_PATH)
await useStore.getState().openNoteInPane(paneId, 'inbox/Note.md')
useStore.setState({ vaultTasks: [makeTask('stale task')] })

await useStore.getState().focusTabInPane(paneId, TASKS_TAB_PATH)
await flushAsyncWork()

expect(scanTasks).toHaveBeenCalledTimes(1)
expect(useStore.getState().vaultTasks).toEqual(freshTasks)
})

it('rescans changed notes while the Tasks tab is open but inactive', async () => {
const freshTasks = [makeTask('new task')]
const scanTasksForPath = vi.fn().mockResolvedValue(freshTasks)
installZen({ scanTasksForPath })

const { useStore } = await loadStore()
const paneId = useStore.getState().activePaneId
await useStore.getState().openNoteInPane(paneId, TASKS_TAB_PATH)
await useStore.getState().openNoteInPane(paneId, 'inbox/Note.md')
useStore.setState({ vaultTasks: [makeTask('stale task')] })

await useStore.getState().applyChange({
kind: 'change',
path: 'inbox/Note.md',
folder: 'inbox',
scope: 'content'
})

expect(scanTasksForPath).toHaveBeenCalledWith('inbox/Note.md')
expect(useStore.getState().vaultTasks).toEqual(freshTasks)
})
})
15 changes: 11 additions & 4 deletions packages/app-core/src/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1149,6 +1149,10 @@ export function isTasksViewActive(state: {
return leaf?.activeTab === TASKS_TAB_PATH
}

function hasTasksViewOpen(state: { paneLayout: PaneLayout }): boolean {
return allLeaves(state.paneLayout).some((leaf) => leaf.tabs.includes(TASKS_TAB_PATH))
}

/** True when the active pane's active tab is the vault-wide Tags view. */
export function isTagsViewActive(state: {
paneLayout: PaneLayout
Expand Down Expand Up @@ -2641,15 +2645,17 @@ export const useStore = create<Store>((set, get) => {

if (ev.scope === 'vault-settings') return

// Keep the Tasks view in sync as files change externally or via our own
// writes — cheap per-path rescans instead of walking the whole vault.
if (isTasksViewActive(state)) {
// Keep an open Tasks tab in sync as files change externally or via our own
// writes — cheap per-path rescans instead of walking the whole vault. This
// also covers inactive Tasks tabs so returning to Kanban doesn't show stale
// cards from the last time the tab was focused.
if (hasTasksViewOpen(state)) {
if (ev.kind === 'unlink') {
set((s) => ({
vaultTasks: s.vaultTasks.filter((t) => t.sourcePath !== ev.path)
}))
} else {
void get().rescanTasksForPath(ev.path)
await get().rescanTasksForPath(ev.path)
}
}

Expand Down Expand Up @@ -3516,6 +3522,7 @@ export const useStore = create<Store>((set, get) => {
...activeFieldsFrom(nextLayout, paneId, cur.noteContents, cur.noteDirty)
}
})
if (!get().tasksLoading) void get().refreshTasks()
return
}

Expand Down
Loading