This commit is contained in:
lucaronin
2026-03-08 20:19:15 +01:00
3 changed files with 48 additions and 29 deletions

View File

@@ -182,7 +182,7 @@ function App() {
// Read at callback time, so it's always current when user presses Cmd+N.
const contentChangeRef = useRef<(path: string, content: string) => void>(() => {})
const notes = useNoteActions({ addEntry: vault.addEntry, removeEntry: vault.removeEntry, updateContent: vault.updateContent, entries: vault.entries, setToastMessage, updateEntry: vault.updateEntry, addPendingSave: vault.addPendingSave, removePendingSave: vault.removePendingSave, trackUnsaved: vault.trackUnsaved, clearUnsaved: vault.clearUnsaved, unsavedPaths: vault.unsavedPaths, markContentPending: (path, content) => contentChangeRef.current(path, content), onNewNotePersisted: vault.loadModifiedFiles })
const notes = useNoteActions({ addEntry: vault.addEntry, removeEntry: vault.removeEntry, updateContent: vault.updateContent, entries: vault.entries, setToastMessage, updateEntry: vault.updateEntry, vaultPath: resolvedPath, addPendingSave: vault.addPendingSave, removePendingSave: vault.removePendingSave, trackUnsaved: vault.trackUnsaved, clearUnsaved: vault.clearUnsaved, unsavedPaths: vault.unsavedPaths, markContentPending: (path, content) => contentChangeRef.current(path, content), onNewNotePersisted: vault.loadModifiedFiles })
// Keep tab entries in sync with vault entries so banners (trash/archive)
// and read-only state react immediately without reopening the note.

View File

@@ -241,8 +241,8 @@ describe('DEFAULT_TEMPLATES', () => {
describe('resolveNewNote', () => {
it('uses TYPE_FOLDER_MAP for known types', () => {
const { entry, content } = resolveNewNote('My Project', 'Project')
expect(entry.path).toBe('/Users/luca/Laputa/project/my-project.md')
const { entry, content } = resolveNewNote('My Project', 'Project', '/my/vault')
expect(entry.path).toBe('/my/vault/project/my-project.md')
expect(entry.isA).toBe('Project')
expect(entry.status).toBe('Active')
expect(content).toContain('type: Project')
@@ -250,31 +250,43 @@ describe('resolveNewNote', () => {
})
it('falls back to slugified type for custom types', () => {
const { entry } = resolveNewNote('First Recipe', 'Recipe')
expect(entry.path).toBe('/Users/luca/Laputa/recipe/first-recipe.md')
const { entry } = resolveNewNote('First Recipe', 'Recipe', '/my/vault')
expect(entry.path).toBe('/my/vault/recipe/first-recipe.md')
})
it('omits status for Topic type', () => {
const { entry, content } = resolveNewNote('Machine Learning', 'Topic')
const { entry, content } = resolveNewNote('Machine Learning', 'Topic', '/my/vault')
expect(entry.status).toBeNull()
expect(content).not.toContain('status:')
})
it('omits status for Person type', () => {
const { entry } = resolveNewNote('John Doe', 'Person')
const { entry } = resolveNewNote('John Doe', 'Person', '/my/vault')
expect(entry.status).toBeNull()
})
it('uses provided vault path instead of hardcoded path', () => {
const { entry } = resolveNewNote('Test', 'Note', '/other/vault')
expect(entry.path).toBe('/other/vault/note/test.md')
expect(entry.path).not.toContain('/Users/luca/Laputa')
})
})
describe('resolveNewType', () => {
it('creates a type entry in the type folder', () => {
const { entry, content } = resolveNewType('Recipe')
expect(entry.path).toBe('/Users/luca/Laputa/type/recipe.md')
const { entry, content } = resolveNewType('Recipe', '/my/vault')
expect(entry.path).toBe('/my/vault/type/recipe.md')
expect(entry.isA).toBe('Type')
expect(entry.status).toBeNull()
expect(content).toContain('type: Type')
expect(content).toContain('# Recipe')
})
it('uses provided vault path instead of hardcoded path', () => {
const { entry } = resolveNewType('Responsibility', '/other/vault')
expect(entry.path).toBe('/other/vault/type/responsibility.md')
expect(entry.path).not.toContain('/Users/luca/Laputa')
})
})
describe('frontmatterToEntryPatch', () => {
@@ -365,8 +377,8 @@ describe('buildDailyNoteContent', () => {
describe('resolveDailyNote', () => {
it('creates entry in journal folder with date as filename', () => {
const { entry } = resolveDailyNote('2026-03-02')
expect(entry.path).toBe('/Users/luca/Laputa/journal/2026-03-02.md')
const { entry } = resolveDailyNote('2026-03-02', '/my/vault')
expect(entry.path).toBe('/my/vault/journal/2026-03-02.md')
expect(entry.filename).toBe('2026-03-02.md')
expect(entry.title).toBe('2026-03-02')
expect(entry.isA).toBe('Journal')
@@ -374,10 +386,16 @@ describe('resolveDailyNote', () => {
})
it('returns content with daily note template', () => {
const { content } = resolveDailyNote('2026-03-02')
const { content } = resolveDailyNote('2026-03-02', '/my/vault')
expect(content).toContain('type: Journal')
expect(content).toContain('## Intentions')
})
it('uses provided vault path instead of hardcoded path', () => {
const { entry } = resolveDailyNote('2026-03-02', '/other/vault')
expect(entry.path).toBe('/other/vault/journal/2026-03-02.md')
expect(entry.path).not.toContain('/Users/luca/Laputa')
})
})
describe('findDailyNote', () => {
@@ -413,7 +431,7 @@ describe('useNoteActions hook', () => {
const setToastMessage = vi.fn()
const makeConfig = (entries: VaultEntry[] = []): NoteActionsConfig => ({
addEntry, removeEntry, updateContent, entries, setToastMessage, updateEntry,
addEntry, removeEntry, updateContent, entries, setToastMessage, updateEntry, vaultPath: '/test/vault',
})
beforeEach(() => {

View File

@@ -26,6 +26,7 @@ export interface NoteActionsConfig {
entries: VaultEntry[]
setToastMessage: (msg: string | null) => void
updateEntry: (path: string, patch: Partial<VaultEntry>) => void
vaultPath: string
addPendingSave?: (path: string) => void
removePendingSave?: (path: string) => void
trackUnsaved?: (path: string) => void
@@ -188,17 +189,17 @@ export function buildNoteContent(title: string, type: string, status: string | n
return `${lines.join('\n')}\n\n# ${title}\n${body}`
}
export function resolveNewNote(title: string, type: string, template?: string | null): { entry: VaultEntry; content: string } {
export function resolveNewNote(title: string, type: string, vaultPath: string, template?: string | null): { entry: VaultEntry; content: string } {
const folder = TYPE_FOLDER_MAP[type] || slugify(type)
const slug = slugify(title)
const status = NO_STATUS_TYPES.has(type) ? null : 'Active'
const entry = buildNewEntry({ path: `/Users/luca/Laputa/${folder}/${slug}.md`, slug, title, type, status })
const entry = buildNewEntry({ path: `${vaultPath}/${folder}/${slug}.md`, slug, title, type, status })
return { entry, content: buildNoteContent(title, type, status, template) }
}
export function resolveNewType(typeName: string): { entry: VaultEntry; content: string } {
export function resolveNewType(typeName: string, vaultPath: string): { entry: VaultEntry; content: string } {
const slug = slugify(typeName)
const entry = buildNewEntry({ path: `/Users/luca/Laputa/type/${slug}.md`, slug, title: typeName, type: 'Type', status: null })
const entry = buildNewEntry({ path: `${vaultPath}/type/${slug}.md`, slug, title: typeName, type: 'Type', status: null })
return { entry, content: `---\ntype: Type\n---\n\n# ${typeName}\n\n` }
}
@@ -211,8 +212,8 @@ export function buildDailyNoteContent(date: string): string {
return `${lines.join('\n')}\n\n# ${date}\n\n## Intentions\n\n\n\n## Reflections\n\n`
}
export function resolveDailyNote(date: string): { entry: VaultEntry; content: string } {
const entry = buildNewEntry({ path: `/Users/luca/Laputa/journal/${date}.md`, slug: date, title: date, type: 'Journal', status: null })
export function resolveDailyNote(date: string, vaultPath: string): { entry: VaultEntry; content: string } {
const entry = buildNewEntry({ path: `${vaultPath}/journal/${date}.md`, slug: date, title: date, type: 'Journal', status: null })
return { entry, content: buildDailyNoteContent(date) }
}
@@ -224,11 +225,11 @@ export function findDailyNote(entries: VaultEntry[], date: string): VaultEntry |
type PersistFn = (resolved: { entry: VaultEntry; content: string }) => void
/** Open today's daily note: navigate to it if it exists, or create + persist a new one. */
function openDailyNote(entries: VaultEntry[], selectNote: (e: VaultEntry) => void, persist: PersistFn): void {
function openDailyNote(entries: VaultEntry[], selectNote: (e: VaultEntry) => void, persist: PersistFn, vaultPath: string): void {
const date = todayDateString()
const existing = findDailyNote(entries, date)
if (existing) selectNote(existing)
else persist(resolveDailyNote(date))
else persist(resolveDailyNote(date, vaultPath))
signalFocusEditor()
}
@@ -366,22 +367,22 @@ export function useNoteActions(config: NoteActionsConfig) {
const handleCreateNote = useCallback((title: string, type: string) => {
const template = resolveTemplate(entries, type)
persistNew(resolveNewNote(title, type, template))
}, [entries, persistNew])
persistNew(resolveNewNote(title, type, config.vaultPath, template))
}, [entries, persistNew, config.vaultPath])
const handleCreateNoteImmediate = useCallback((type?: string) => {
const noteType = type || 'Note'
const title = generateUntitledName(entries, noteType, pendingNamesRef.current)
pendingNamesRef.current.add(title)
const template = resolveTemplate(entries, noteType)
const resolved = resolveNewNote(title, noteType, template)
const resolved = resolveNewNote(title, noteType, config.vaultPath, template)
openTabWithContent(resolved.entry, resolved.content)
addEntryWithMock(resolved.entry, resolved.content, addEntry)
config.trackUnsaved?.(resolved.entry.path)
config.markContentPending?.(resolved.entry.path, resolved.content)
signalFocusEditor({ selectTitle: true })
setTimeout(() => pendingNamesRef.current.delete(title), 500)
}, [entries, openTabWithContent, addEntry, config.trackUnsaved, config.markContentPending]) // eslint-disable-line react-hooks/exhaustive-deps -- config callbacks are stable
}, [entries, openTabWithContent, addEntry, config.vaultPath, config.trackUnsaved, config.markContentPending]) // eslint-disable-line react-hooks/exhaustive-deps -- config callbacks are stable
/** Close tab and discard entry+unsaved state if the note was never persisted. */
const handleCloseTabWithCleanup = useCallback((path: string) => {
@@ -392,17 +393,17 @@ export function useNoteActions(config: NoteActionsConfig) {
// Keep handleCloseTabRef in sync so Cmd+W and menu events also clean up unsaved notes.
useEffect(() => { handleCloseTabRef.current = handleCloseTabWithCleanup })
const handleOpenDailyNote = useCallback(() => openDailyNote(entries, handleSelectNote, persistNew), [entries, handleSelectNote, persistNew])
const handleOpenDailyNote = useCallback(() => openDailyNote(entries, handleSelectNote, persistNew, config.vaultPath), [entries, handleSelectNote, persistNew, config.vaultPath])
const handleCreateType = useCallback((typeName: string) => persistNew(resolveNewType(typeName)), [persistNew])
const handleCreateType = useCallback((typeName: string) => persistNew(resolveNewType(typeName, config.vaultPath)), [persistNew, config.vaultPath])
/** Create a Type entry file silently (no tab opened). Adds to state and persists to disk. */
const createTypeEntrySilent = useCallback(async (typeName: string): Promise<VaultEntry> => {
const resolved = resolveNewType(typeName)
const resolved = resolveNewType(typeName, config.vaultPath)
addEntryWithMock(resolved.entry, resolved.content, addEntry)
await persistNewNote(resolved.entry.path, resolved.content)
return resolved.entry
}, [addEntry])
}, [addEntry, config.vaultPath])
const fmCallbacks = { updateTab: updateTabContent, updateEntry, toast: setToastMessage }