fix(types): normalize built-in notes type creation

This commit is contained in:
lucaronin
2026-05-05 03:41:41 +02:00
parent ccfa148014
commit 0df9c1d707
4 changed files with 102 additions and 4 deletions

View File

@@ -319,6 +319,15 @@ describe('resolveNewType', () => {
expect(entry.path).toBe('/other/vault/responsibility.md')
expect(entry.path).not.toContain('/Users/luca/Laputa')
})
it('normalizes the built-in Notes label to the Note type definition', () => {
const { entry, content } = resolveNewType({ typeName: 'Notes', vaultPath: '/my/vault' })
expect(entry.path).toBe('/my/vault/note.md')
expect(entry.filename).toBe('note.md')
expect(entry.title).toBe('Note')
expect(content).toContain('# Note')
})
})
describe('planNewTypeCreation', () => {
@@ -344,4 +353,24 @@ describe('planNewTypeCreation', () => {
expect(plan.status).toBe('blocked')
})
it('does not treat an existing notes.md note as a collision for the built-in Notes label', () => {
const plan = planNewTypeCreation({
entries: [makeEntry({ path: '/my/vault/notes.md', filename: 'notes.md', title: 'Meeting Notes', isA: 'Note' })],
typeName: 'Notes',
vaultPath: '/my/vault',
})
expect(plan).toEqual({
status: 'create',
resolved: expect.objectContaining({
entry: expect.objectContaining({
path: '/my/vault/note.md',
filename: 'note.md',
title: 'Note',
isA: 'Type',
}),
}),
})
})
})

View File

@@ -624,6 +624,46 @@ describe('useNoteCreation hook', () => {
expect(setToastMessage).toHaveBeenCalledWith('Cannot create type "Note" because note.md already exists')
})
it('handleCreateType creates the built-in Note type when notes.md is an existing ordinary note', async () => {
vi.mocked(isTauri).mockReturnValue(true)
vi.mocked(invoke)
.mockRejectedValueOnce(new Error('not found'))
.mockResolvedValueOnce(undefined)
const existingNotes = makeEntry({
path: '/test/vault/notes.md',
filename: 'notes.md',
title: 'Meeting Notes',
isA: 'Note',
})
const { result } = renderHook(() => useNoteCreation(makeConfig([existingNotes]), tabDeps))
let created = false
await act(async () => {
created = await result.current.handleCreateType('Notes')
})
expect(created).toBe(true)
expect(vi.mocked(invoke)).toHaveBeenCalledWith('get_note_content', {
path: '/test/vault/note.md',
})
expect(vi.mocked(invoke)).toHaveBeenCalledWith('create_note_content', {
path: '/test/vault/note.md',
content: '---\ntype: Type\n---\n\n# Note\n',
})
expect(addEntry).toHaveBeenCalledWith(expect.objectContaining({
path: '/test/vault/note.md',
filename: 'note.md',
title: 'Note',
isA: 'Type',
}))
expect(openTabWithContent).toHaveBeenCalledWith(expect.objectContaining({
path: '/test/vault/note.md',
title: 'Note',
isA: 'Type',
}), expect.stringContaining('# Note'))
expect(setToastMessage).not.toHaveBeenCalled()
})
it('handleCreateType blocks when a loaded non-Type entry collides with the target type path', async () => {
vi.mocked(isTauri).mockReturnValue(true)
const staleEntry = makeEntry({

View File

@@ -8,6 +8,7 @@ import { trackEvent } from '../lib/telemetry'
import { cacheNoteContent } from './useTabManagement'
import { findByCollidingNotePath, joinVaultPath, notePathFilename } from '../utils/notePathIdentity'
import { canonicalFrontmatterKey } from '../utils/systemMetadata'
import { canonicalizeTypeName } from '../utils/vaultTypes'
export interface NewEntryParams {
path: string
@@ -233,10 +234,20 @@ export interface NewTypeParams {
vaultPath: string
}
const TYPE_CREATION_ALIASES = new Map<string, string>([
['notes', 'Note'],
])
export function normalizeTypeCreationName(typeName: string): string {
const trimmed = typeName.trim()
return TYPE_CREATION_ALIASES.get(trimmed.toLowerCase()) ?? canonicalizeTypeName(trimmed) ?? trimmed
}
export function resolveNewType({ typeName, vaultPath }: NewTypeParams): { entry: VaultEntry; content: string } {
const slug = slugify(typeName)
const entry = buildNewEntry({ path: joinVaultPath(vaultPath, `${slug}.md`), slug, title: typeName, type: 'Type', status: null })
return { entry, content: `---\ntype: Type\n---\n\n# ${typeName}\n` }
const normalizedTypeName = normalizeTypeCreationName(typeName)
const slug = slugify(normalizedTypeName)
const entry = buildNewEntry({ path: joinVaultPath(vaultPath, `${slug}.md`), slug, title: normalizedTypeName, type: 'Type', status: null })
return { entry, content: `---\ntype: Type\n---\n\n# ${normalizedTypeName}\n` }
}
type ResolvedEntry = { entry: VaultEntry; content: string }
@@ -300,7 +311,7 @@ function buildCreationCollisionMessage({ noun, title, path }: { noun: 'note' | '
}
function findEquivalentTypeEntry(entries: VaultEntry[], typeName: string): VaultEntry | undefined {
const trimmed = typeName.trim()
const trimmed = normalizeTypeCreationName(typeName)
const targetSlug = slugify(trimmed)
return entries.find((entry) =>
entry.isA === 'Type' && (entry.title === trimmed || slugify(entry.title) === targetSlug)

View File

@@ -120,6 +120,24 @@ test.describe('Collision-safe create flows', () => {
expect(fs.readFileSync(defaultNoteTypePath, 'utf8')).toContain('# Note')
})
test('command palette type creation maps the built-in Notes label away from notes.md collisions', async ({ page }) => {
const existingNotesPath = writeFixtureNote(
tempVaultDir,
'notes.md',
'---\ntype: Note\n---\n# Meeting Notes\n',
)
fs.rmSync(path.join(tempVaultDir, 'type', 'note.md'), { force: true })
const defaultNoteTypePath = path.join(tempVaultDir, 'note.md')
await openFixtureVaultDesktopHarness(page, tempVaultDir)
await createTypeFromCommandPalette(page, 'Notes')
await expect.poll(() => fs.existsSync(defaultNoteTypePath)).toBe(true)
await expect(toast(page)).toContainText('Type "Notes" created')
expect(fs.readFileSync(defaultNoteTypePath, 'utf8')).toContain('# Note')
expect(fs.readFileSync(existingNotesPath, 'utf8')).toContain('# Meeting Notes')
})
test('unicode type creation ignores an unrelated untitled draft filename', async ({ page }) => {
const untitledPath = writeFixtureNote(
tempVaultDir,