fix(types): normalize built-in notes type creation
This commit is contained in:
@@ -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',
|
||||
}),
|
||||
}),
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user