fix: hide legacy title section for H1 notes
This commit is contained in:
90
src/components/editor-content/editorContentState.test.ts
Normal file
90
src/components/editor-content/editorContentState.test.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
75
src/components/editor-content/editorContentState.ts
Normal file
75
src/components/editor-content/editorContentState.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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 })
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user