fix: hide legacy title section for H1 notes

This commit is contained in:
Test
2026-04-08 20:17:51 +02:00
parent 2c910779bf
commit 16e5dd466a
4 changed files with 190 additions and 16 deletions

View File

@@ -0,0 +1,90 @@
import { describe, expect, it } from 'vitest'
import type { VaultEntry } from '../../types'
import { deriveEditorContentState, type EditorContentTab } from './editorContentState'
const baseEntry: VaultEntry = {
path: '/vault/project/legacy-project.md',
filename: 'legacy-project.md',
title: 'Legacy Project',
isA: 'Project',
aliases: [],
belongsTo: [],
relatedTo: [],
status: 'Active',
archived: false,
modifiedAt: 1700000000,
createdAt: null,
fileSize: 1024,
snippet: '',
wordCount: 0,
relationships: {},
icon: null,
color: null,
order: null,
sidebarLabel: null,
template: null,
sort: null,
view: null,
visible: null,
organized: false,
favorite: false,
favoriteIndex: null,
listPropertiesDisplay: [],
outgoingLinks: [],
properties: {},
hasH1: false,
}
function deriveState(tab: EditorContentTab | null, overrides?: Partial<VaultEntry>) {
const entry = tab ? { ...baseEntry, ...overrides, ...tab.entry } : null
return deriveEditorContentState({
activeTab: entry ? { ...tab, entry } : null,
entries: entry ? [entry] : [],
rawMode: false,
activeStatus: 'clean',
})
}
describe('deriveEditorContentState', () => {
it('hides the legacy title section when loaded content contains a top-level H1', () => {
const state = deriveState({
entry: baseEntry,
content: '---\ntitle: Legacy Project\n---\n# Legacy Project\n\nBody',
})
expect(state.hasH1).toBe(true)
expect(state.showTitleSection).toBe(false)
})
it('keeps the title section for notes without an H1', () => {
const state = deriveState({
entry: baseEntry,
content: '---\ntitle: Legacy Project\n---\nBody without a heading',
})
expect(state.hasH1).toBe(false)
expect(state.showTitleSection).toBe(true)
})
it('hides the title section for untitled drafts before they get an H1', () => {
const draftEntry = {
...baseEntry,
path: '/vault/untitled-note-1700000000.md',
filename: 'untitled-note-1700000000.md',
title: 'Untitled Note 1700000000',
}
const state = deriveEditorContentState({
activeTab: {
entry: draftEntry,
content: '---\ntype: Note\n---\n',
},
entries: [draftEntry],
rawMode: false,
activeStatus: 'unsaved',
})
expect(state.hasH1).toBe(false)
expect(state.showTitleSection).toBe(false)
})
})

View File

@@ -0,0 +1,75 @@
import type { NoteStatus, VaultEntry } from '../../types'
import { extractH1TitleFromContent } from '../../utils/noteTitle'
import { countWords } from '../../utils/wikilinks'
export interface EditorContentTab {
entry: VaultEntry
content: string
}
interface EditorContentStateInput {
activeTab: EditorContentTab | null
entries: VaultEntry[]
rawMode: boolean
activeStatus: NoteStatus
}
export interface EditorContentState {
freshEntry: VaultEntry | undefined
isArchived: boolean
hasH1: boolean
isDeletedPreview: boolean
isNonMarkdownText: boolean
effectiveRawMode: boolean
showEditor: boolean
showTitleSection: boolean
path: string
wordCount: number
}
function findFreshEntry(activeTab: EditorContentTab | null, entries: VaultEntry[]): VaultEntry | undefined {
if (!activeTab) return undefined
return entries.find((entry) => entry.path === activeTab.entry.path)
}
function contentHasTopLevelH1(activeTab: EditorContentTab | null): boolean {
return activeTab ? extractH1TitleFromContent(activeTab.content) !== null : false
}
function resolveHasH1(activeTab: EditorContentTab | null, freshEntry: VaultEntry | undefined): boolean {
return contentHasTopLevelH1(activeTab) || freshEntry?.hasH1 === true || activeTab?.entry.hasH1 === true
}
function isUnsavedUntitledDraft(activeTab: EditorContentTab | null, activeStatus: NoteStatus): boolean {
if (!activeTab) return false
if (!activeTab.entry.filename.startsWith('untitled-')) return false
return activeStatus === 'new' || activeStatus === 'unsaved' || activeStatus === 'pendingSave'
}
export function deriveEditorContentState({
activeTab,
entries,
rawMode,
activeStatus,
}: EditorContentStateInput): EditorContentState {
const freshEntry = findFreshEntry(activeTab, entries)
const isDeletedPreview = !!activeTab && !freshEntry
const hasH1 = resolveHasH1(activeTab, freshEntry)
const isNonMarkdownText = activeTab?.entry.fileKind === 'text'
const effectiveRawMode = rawMode || isNonMarkdownText
const showEditor = !effectiveRawMode
const showTitleSection = !isDeletedPreview && !hasH1 && !isUnsavedUntitledDraft(activeTab, activeStatus)
return {
freshEntry,
isArchived: freshEntry?.archived ?? activeTab?.entry.archived ?? false,
hasH1,
isDeletedPreview,
isNonMarkdownText,
effectiveRawMode,
showEditor,
showTitleSection,
path: activeTab?.entry.path ?? '',
wordCount: activeTab ? countWords(activeTab.content) : 0,
}
}

View File

@@ -2,9 +2,9 @@ import type React from 'react'
import { useEffect, useRef } from 'react'
import type { useCreateBlockNote } from '@blocknote/react'
import type { NoteStatus, VaultEntry } from '../../types'
import { countWords } from '../../utils/wikilinks'
import { useEditorTheme } from '../../hooks/useTheme'
import { resolveNoteIcon } from '../../utils/noteIcon'
import { deriveEditorContentState } from './editorContentState'
export interface Tab {
entry: VaultEntry
@@ -55,21 +55,24 @@ export function useEditorContentModel(props: EditorContentProps) {
} = props
const { cssVars } = useEditorTheme()
const freshEntry = activeTab ? entries.find((entry) => entry.path === activeTab.entry.path) : undefined
const isArchived = freshEntry?.archived ?? activeTab?.entry.archived ?? false
const hasH1 = freshEntry?.hasH1 ?? activeTab?.entry.hasH1 ?? false
const isDeletedPreview = !!activeTab && !freshEntry
const isNonMarkdownText = activeTab?.entry.fileKind === 'text'
const effectiveRawMode = rawMode || isNonMarkdownText
const showEditor = !diffMode && !effectiveRawMode
const {
isArchived,
isDeletedPreview,
isNonMarkdownText,
effectiveRawMode,
showEditor: showContentEditor,
path,
showTitleSection,
wordCount,
} = deriveEditorContentState({
activeTab,
entries,
rawMode,
activeStatus,
})
const showEditor = !diffMode && showContentEditor
const entryIcon = activeTab?.entry.icon ?? null
const hasDisplayIcon = resolveNoteIcon(entryIcon).kind !== 'none'
const isUntitledDraft = !!activeTab
&& activeTab.entry.filename.startsWith('untitled-')
&& (activeStatus === 'new' || activeStatus === 'unsaved' || activeStatus === 'pendingSave')
const showTitleSection = !isDeletedPreview && !hasH1 && !isUntitledDraft
const path = activeTab?.entry.path ?? ''
const wordCount = activeTab ? countWords(activeTab.content) : 0
const titleSectionRef = useRef<HTMLDivElement | null>(null)
const breadcrumbBarRef = useRef<HTMLDivElement | null>(null)

View File

@@ -69,6 +69,14 @@ test('creating an untitled draft hides the legacy title section in the editor',
await expect(page.locator('.title-section[data-title-ui-visible]')).toHaveCount(0)
})
test('@smoke older notes with an H1 do not render the legacy title section', async ({ page }) => {
await openNote(page, 'Alpha Project')
await expect(page.locator('.bn-editor')).toBeVisible({ timeout: 5_000 })
await expect(page.getByTestId('title-field-input')).toHaveCount(0)
await expect(page.locator('.title-section[data-title-ui-visible]')).toHaveCount(0)
})
test('@smoke edited H1 titles drive note list, search, and wikilink autocomplete', async ({ page }) => {
const updatedTitle = 'Updated Display Title'
const noteList = page.locator('[data-testid="note-list-container"]')
@@ -104,6 +112,4 @@ test('@smoke edited H1 titles drive note list, search, and wikilink autocomplete
const suggestionMenu = page.locator('.wikilink-menu')
await expect(suggestionMenu).toContainText(updatedTitle, { timeout: 5_000 })
await page.keyboard.press('Enter')
await expect(page.locator('.wikilink').last()).toHaveText(updatedTitle, { timeout: 5_000 })
})