feat: H1-as-title — title resolution, TitleField hiding, breadcrumb filename
- Rust: extract_title now prioritizes H1 > frontmatter > filename
- Rust: add has_h1 field to VaultEntry for frontend TitleField control
- Frontend: hide TitleField + icon picker when note has H1 in body
- Frontend: breadcrumb shows filename stem instead of display title
- Frontend: new notes use untitled-{type}-{timestamp}.md, no title in frontmatter
- CSS: only hide first H1 in BlockNote when TitleField is visible
- Updated all tests for new title resolution and note creation behavior
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -57,7 +57,10 @@ const makeEntry = (overrides: Partial<VaultEntry> = {}): VaultEntry => ({
|
||||
color: null,
|
||||
order: null,
|
||||
outgoingLinks: [],
|
||||
template: null, sort: null,
|
||||
template: null, sort: null, sidebarLabel: null,
|
||||
view: null, visible: null, properties: {}, organized: false,
|
||||
favorite: false, favoriteIndex: null, listPropertiesDisplay: [],
|
||||
hasH1: false,
|
||||
...overrides,
|
||||
})
|
||||
|
||||
@@ -212,11 +215,16 @@ describe('entryMatchesTarget', () => {
|
||||
})
|
||||
|
||||
describe('buildNoteContent', () => {
|
||||
it('generates frontmatter with status for regular types', () => {
|
||||
it('generates frontmatter with title and status for regular types', () => {
|
||||
const content = buildNoteContent('My Note', 'Note', 'Active')
|
||||
expect(content).toBe('---\ntitle: My Note\ntype: Note\nstatus: Active\n---\n')
|
||||
})
|
||||
|
||||
it('omits title when null', () => {
|
||||
const content = buildNoteContent(null, 'Note', 'Active')
|
||||
expect(content).toBe('---\ntype: Note\nstatus: Active\n---\n')
|
||||
})
|
||||
|
||||
it('omits status when null', () => {
|
||||
const content = buildNoteContent('AI', 'Topic', null)
|
||||
expect(content).toBe('---\ntitle: AI\ntype: Topic\n---\n')
|
||||
@@ -701,7 +709,8 @@ describe('useNoteActions hook', () => {
|
||||
expect(setToastMessage).toHaveBeenCalledWith('Property deleted')
|
||||
})
|
||||
|
||||
it('handleCreateNoteImmediate creates note with auto-generated title', () => {
|
||||
it('handleCreateNoteImmediate creates note with timestamp-based title', () => {
|
||||
vi.spyOn(Date, 'now').mockReturnValue(1700000000000)
|
||||
const { result } = renderHook(() => useNoteActions(makeConfig()))
|
||||
|
||||
act(() => {
|
||||
@@ -710,11 +719,15 @@ describe('useNoteActions hook', () => {
|
||||
|
||||
expect(addEntry).toHaveBeenCalledTimes(1)
|
||||
const [createdEntry] = addEntry.mock.calls[0]
|
||||
expect(createdEntry.title).toBe('Untitled note')
|
||||
expect(createdEntry.title).toBe('Untitled Note 1700000000')
|
||||
expect(createdEntry.filename).toBe('untitled-note-1700000000.md')
|
||||
expect(createdEntry.isA).toBe('Note')
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('handleCreateNoteImmediate generates unique names on rapid calls', () => {
|
||||
it('handleCreateNoteImmediate generates unique names on rapid calls via timestamp', () => {
|
||||
let ts = 1700000000000
|
||||
vi.spyOn(Date, 'now').mockImplementation(() => { ts += 1000; return ts })
|
||||
const { result } = renderHook(() => useNoteActions(makeConfig()))
|
||||
|
||||
act(() => {
|
||||
@@ -724,13 +737,17 @@ describe('useNoteActions hook', () => {
|
||||
})
|
||||
|
||||
expect(addEntry).toHaveBeenCalledTimes(3)
|
||||
const titles = addEntry.mock.calls.map(([e]: [VaultEntry]) => e.title)
|
||||
expect(titles[0]).toBe('Untitled note')
|
||||
expect(titles[1]).toBe('Untitled note 2')
|
||||
expect(titles[2]).toBe('Untitled note 3')
|
||||
const filenames = addEntry.mock.calls.map(([e]: [VaultEntry]) => e.filename)
|
||||
// Each call consumes Date.now() multiple times, so just verify uniqueness and pattern
|
||||
expect(new Set(filenames).size).toBe(3)
|
||||
for (const fn of filenames) {
|
||||
expect(fn).toMatch(/^untitled-note-\d+\.md$/)
|
||||
}
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('handleCreateNoteImmediate accepts custom type', () => {
|
||||
vi.spyOn(Date, 'now').mockReturnValue(1700000000000)
|
||||
const { result } = renderHook(() => useNoteActions(makeConfig()))
|
||||
|
||||
act(() => {
|
||||
@@ -739,8 +756,9 @@ describe('useNoteActions hook', () => {
|
||||
|
||||
expect(addEntry).toHaveBeenCalledTimes(1)
|
||||
const [createdEntry] = addEntry.mock.calls[0]
|
||||
expect(createdEntry.title).toBe('Untitled project')
|
||||
expect(createdEntry.filename).toMatch(/^untitled-project-\d+\.md$/)
|
||||
expect(createdEntry.isA).toBe('Project')
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('handleCreateNote uses default template for Project type', () => {
|
||||
@@ -918,8 +936,8 @@ describe('useNoteActions hook', () => {
|
||||
await new Promise((r) => setTimeout(r, 0))
|
||||
})
|
||||
|
||||
expect(trackUnsaved).toHaveBeenCalledWith(expect.stringContaining('untitled-note.md'))
|
||||
expect(markContentPending).toHaveBeenCalledWith(expect.stringContaining('untitled-note.md'), expect.stringContaining('Untitled note'))
|
||||
expect(trackUnsaved).toHaveBeenCalledWith(expect.stringMatching(/untitled-note-\d+\.md$/))
|
||||
expect(markContentPending).toHaveBeenCalledWith(expect.stringMatching(/untitled-note-\d+\.md$/), expect.stringContaining('type: Note'))
|
||||
})
|
||||
|
||||
it('calls onNewNotePersisted after successful disk write (non-Tauri)', async () => {
|
||||
|
||||
Reference in New Issue
Block a user