Skip to content

Commit 2f0f08c

Browse files
committed
Add daily note stacked block action
1 parent 00bb9d2 commit 2f0f08c

3 files changed

Lines changed: 198 additions & 7 deletions

File tree

src/plugins/daily-notes/actions.ts

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
* Global keyboard actions for navigating daily notes:
33
*
44
* - `open_today` (cmd+shift+`) — today's note
5+
* - `append_today_daily_block` (ctrl+shift+n) — new block in today's note,
6+
* opened in a stacked panel
57
* - `open_previous_daily_note` (cmd+shift+[) — yesterday relative to
68
* the currently viewed daily note (or to today if not on one)
79
* - `open_next_daily_note` (cmd+shift+]) — tomorrow relative
@@ -21,16 +23,25 @@
2123
*/
2224
import type { Block } from '@/data/block'
2325
import type { Repo } from '@/data/repo'
24-
import { aliasesProp } from '@/data/properties.ts'
26+
import { ChangeScope } from '@/data/api'
27+
import { getLayoutSessionBlock } from '@/data/globalState.ts'
28+
import {
29+
activePanelIdProp,
30+
aliasesProp,
31+
editorSelection,
32+
isEditingProp,
33+
} from '@/data/properties.ts'
2534
import {
2635
ActionConfig,
2736
ActionContextTypes,
2837
} from '@/shortcuts/types.ts'
38+
import { getLayoutSessionId } from '@/utils/layoutSessionId.ts'
2939
import { parseAppHash } from '@/utils/routing.ts'
3040
import {
3141
navigateFromGlobalCommand,
3242
resolveGlobalCommandTopLevelBlockId,
3343
} from '@/utils/navigation.ts'
44+
import { insertSidebarStackedPanel } from '@/utils/panelLayoutProjection.ts'
3445
import { addDaysIso, getOrCreateDailyNote, todayIso } from './dailyNotes.ts'
3546

3647
const ISO_ALIAS_RE = /^\d{4}-\d{2}-\d{2}$/
@@ -71,6 +82,30 @@ const openDailyNoteByOffset = async (repo: Repo, offsetDays: number) => {
7182
navigateFromGlobalCommand(repo, {blockId: note.id, workspaceId})
7283
}
7384

85+
const appendTodayDailyBlockInStack = async (
86+
repo: Repo,
87+
uiStateBlock: Block,
88+
): Promise<void> => {
89+
const workspaceId = repo.activeWorkspaceId
90+
if (!workspaceId || repo.isReadOnly) return
91+
92+
const note = await getOrCreateDailyNote(repo, workspaceId, todayIso())
93+
const blockId = await repo.mutate.createChild({
94+
parentId: note.id,
95+
position: {kind: 'last'},
96+
})
97+
98+
const layoutSessionBlock = await getLayoutSessionBlock(uiStateBlock, getLayoutSessionId())
99+
await layoutSessionBlock.load()
100+
const sourcePanelId = layoutSessionBlock.peekProperty(activePanelIdProp)
101+
const panelId = await insertSidebarStackedPanel(repo, layoutSessionBlock, blockId, {sourcePanelId})
102+
103+
await repo.tx(async tx => {
104+
await tx.setProperty(panelId, editorSelection, {blockId, start: 0})
105+
await tx.setProperty(panelId, isEditingProp, true)
106+
}, {scope: ChangeScope.UiState, description: 'edit new daily block'})
107+
}
108+
74109
export const dailyNotesActions = (
75110
{repo}: {repo: Repo},
76111
): readonly ActionConfig<typeof ActionContextTypes.GLOBAL>[] => [
@@ -92,6 +127,20 @@ export const dailyNotesActions = (
92127
keys: ['cmd+shift+`', 'ctrl+shift+`'],
93128
},
94129
},
130+
{
131+
id: 'append_today_daily_block',
132+
description: "Append a block to today's daily note",
133+
context: ActionContextTypes.GLOBAL,
134+
handler: async ({uiStateBlock}) => {
135+
await appendTodayDailyBlockInStack(repo, uiStateBlock)
136+
},
137+
defaultBinding: {
138+
keys: 'ctrl+shift+n',
139+
eventOptions: {
140+
preventDefault: true,
141+
},
142+
},
143+
},
95144
{
96145
id: 'open_previous_daily_note',
97146
description: 'Open previous daily note',
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
// @vitest-environment jsdom
2+
3+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
4+
import { ChangeScope, type User } from '@/data/api'
5+
import { BlockCache } from '@/data/blockCache'
6+
import { kernelDataExtension } from '@/data/kernelDataExtension'
7+
import { getLayoutSessionBlock, getUIStateBlock } from '@/data/globalState'
8+
import {
9+
activePanelIdProp,
10+
editorSelection,
11+
focusedBlockIdProp,
12+
isEditingProp,
13+
topLevelBlockIdProp,
14+
} from '@/data/properties'
15+
import { createTestDb, type TestDb } from '@/data/test/createTestDb'
16+
import { Repo } from '@/data/repo'
17+
import { resolveFacetRuntimeSync } from '@/extensions/facet'
18+
import {
19+
__resetLayoutSessionIdForTesting,
20+
getLayoutSessionId,
21+
} from '@/utils/layoutSessionId'
22+
import {
23+
insertPanelRow,
24+
layoutSlotsFromRows,
25+
panelBlockId,
26+
panelRowsInLayoutOrder,
27+
} from '@/utils/panelLayoutProjection'
28+
import {
29+
dailyNotesActions,
30+
} from '../actions.ts'
31+
import {
32+
dailyNotesDataExtension,
33+
getOrCreateDailyNote,
34+
} from '../index.ts'
35+
36+
const WS = 'ws-1'
37+
const USER: User = {id: 'user-1', name: 'Alice'}
38+
39+
interface Harness {
40+
h: TestDb
41+
repo: Repo
42+
}
43+
44+
const setup = async (): Promise<Harness> => {
45+
const h = await createTestDb()
46+
let id = 0
47+
const repo = new Repo({
48+
db: h.db,
49+
cache: new BlockCache(),
50+
user: USER,
51+
newId: () => `gen-${++id}`,
52+
registerKernelProcessors: false,
53+
})
54+
repo.setFacetRuntime(resolveFacetRuntimeSync([
55+
kernelDataExtension,
56+
dailyNotesDataExtension,
57+
]))
58+
repo.setActiveWorkspaceId(WS)
59+
return {h, repo}
60+
}
61+
62+
let env: Harness
63+
64+
beforeEach(async () => {
65+
__resetLayoutSessionIdForTesting()
66+
vi.useFakeTimers()
67+
vi.setSystemTime(new Date(2026, 4, 13, 12))
68+
env = await setup()
69+
})
70+
71+
afterEach(async () => {
72+
vi.useRealTimers()
73+
await env.h.cleanup()
74+
})
75+
76+
describe('dailyNotesActions', () => {
77+
it('appends an empty block to today and opens it in an editing stacked panel', async () => {
78+
const daily = await getOrCreateDailyNote(env.repo, WS, '2026-05-13')
79+
await env.repo.mutate.createChild({
80+
id: 'existing-daily-child',
81+
parentId: daily.id,
82+
content: 'existing',
83+
})
84+
await env.repo.tx(async tx => {
85+
await tx.create({
86+
id: 'main-block',
87+
workspaceId: WS,
88+
parentId: null,
89+
orderKey: 'm0',
90+
content: 'Main',
91+
})
92+
}, {scope: ChangeScope.BlockDefault})
93+
94+
const rootUiState = await getUIStateBlock(env.repo, WS, USER, {})
95+
const layoutSession = await getLayoutSessionBlock(rootUiState, getLayoutSessionId())
96+
await insertPanelRow(env.repo, layoutSession, 'main-block')
97+
98+
const action = dailyNotesActions({repo: env.repo})
99+
.find(candidate => candidate.id === 'append_today_daily_block')
100+
expect(action?.defaultBinding?.keys).toBe('ctrl+shift+n')
101+
102+
await action?.handler(
103+
{uiStateBlock: rootUiState},
104+
{preventDefault: vi.fn()} as unknown as KeyboardEvent,
105+
)
106+
107+
const dailyChildren = await env.repo.block(daily.id).childIds.load()
108+
expect(dailyChildren[0]).toBe('existing-daily-child')
109+
expect(dailyChildren).toHaveLength(2)
110+
const newBlockId = dailyChildren[1]
111+
expect(env.repo.block(newBlockId).peek()?.content).toBe('')
112+
113+
const layoutRows = await env.repo.query.subtree({id: layoutSession.id}).load()
114+
expect(layoutSlotsFromRows(layoutSession.id, layoutRows)).toEqual([
115+
{kind: 'leaf', blockId: 'main-block'},
116+
{
117+
kind: 'stack',
118+
children: [
119+
{kind: 'leaf', blockId: newBlockId},
120+
],
121+
},
122+
])
123+
124+
const newPanel = panelRowsInLayoutOrder(layoutSession.id, layoutRows)
125+
.find(row => panelBlockId(row) === newBlockId)
126+
expect(newPanel).toBeTruthy()
127+
await env.repo.block(newPanel!.id).load()
128+
await layoutSession.load()
129+
130+
expect(env.repo.block(newPanel!.id).peekProperty(topLevelBlockIdProp)).toBe(newBlockId)
131+
expect(env.repo.block(newPanel!.id).peekProperty(focusedBlockIdProp)).toBe(newBlockId)
132+
expect(env.repo.block(newPanel!.id).peekProperty(editorSelection)).toEqual({
133+
blockId: newBlockId,
134+
start: 0,
135+
})
136+
expect(env.repo.block(newPanel!.id).peekProperty(isEditingProp)).toBe(true)
137+
expect(layoutSession.peekProperty(activePanelIdProp)).toBe(newPanel!.id)
138+
})
139+
})

src/plugins/quick-find/QuickFind.tsx

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,10 @@ import {
99
} from '@/components/ui/command'
1010
import { Kbd } from '@/components/ui/kbd'
1111
import { useRepo } from '@/context/repo.tsx'
12-
import { useUserPrefsBlock, useUserPrefsProperty } from '@/data/globalState.ts'
12+
import { useLayoutSessionBlock, useUserPrefsBlock, useUserPrefsProperty } from '@/data/globalState.ts'
1313
import { ChangeScope } from '@/data/api'
14-
import { aliasesProp } from '@/data/properties.ts'
14+
import { activePanelIdProp, aliasesProp } from '@/data/properties.ts'
15+
import { usePropertyValue } from '@/hooks/block.ts'
1516
import { PAGE_TYPE } from '@/data/blockTypes.ts'
1617
import { v4 as uuidv4 } from 'uuid'
1718
import { useNavigate, useNavigateFromGlobalCommand } from '@/utils/navigation.ts'
@@ -55,6 +56,8 @@ export function QuickFind() {
5556
const userPrefsBlock = useUserPrefsBlock()
5657
const navigate = useNavigate()
5758
const navigateFromGlobalCommand = useNavigateFromGlobalCommand()
59+
const layoutSessionBlock = useLayoutSessionBlock()
60+
const [activePanelId] = usePropertyValue(layoutSessionBlock, activePanelIdProp)
5861
const [recentIds] = useUserPrefsProperty(recentBlockIdsProp)
5962

6063
const [open, setOpen] = useState(false)
@@ -159,9 +162,9 @@ export function QuickFind() {
159162
setOpen(false)
160163
}
161164

162-
const openInNewPanel = (blockId: string) => {
165+
const openInStackedPanel = (blockId: string) => {
163166
pushRecentBlockId(userPrefsBlock, blockId)
164-
navigate({blockId, target: 'new-panel'})
167+
navigate({blockId, target: 'sidebar-stack', sourcePanelId: activePanelId})
165168
setOpen(false)
166169
}
167170

@@ -215,7 +218,7 @@ export function QuickFind() {
215218
}
216219
const blockId = payload.split(':')[0]
217220
if (!blockId) return
218-
if (openInPanel) openInNewPanel(blockId)
221+
if (openInPanel) openInStackedPanel(blockId)
219222
else jumpToBlock(blockId)
220223
}
221224

@@ -343,7 +346,7 @@ export function QuickFind() {
343346
<Kbd></Kbd> jump
344347
</span>
345348
<span className="flex items-center gap-1">
346-
<Kbd>⇧↵</Kbd> open in panel
349+
<Kbd>⇧↵</Kbd> open in stack
347350
</span>
348351
</div>
349352
</CommandDialog>

0 commit comments

Comments
 (0)