fix: improve note icon property editing

This commit is contained in:
Test
2026-04-08 20:40:14 +02:00
parent 16e5dd466a
commit f8112e9385
6 changed files with 608 additions and 57 deletions

View File

@@ -0,0 +1,120 @@
import { createRef } from 'react'
import { render, screen } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
import { EditorContentLayout } from './EditorContentLayout'
vi.mock('../BreadcrumbBar', () => ({
BreadcrumbBar: () => <div data-testid="breadcrumb-bar" />,
}))
vi.mock('../TitleField', () => ({
TitleField: () => <div data-testid="title-field-input" />,
}))
vi.mock('../NoteIcon', () => ({
NoteIcon: ({ icon }: { icon: string | null }) => (
icon
? <button type="button" data-testid="note-icon-display" />
: <button type="button" data-testid="note-icon-add" />
),
}))
vi.mock('../ArchivedNoteBanner', () => ({
ArchivedNoteBanner: () => <div data-testid="archived-banner" />,
}))
vi.mock('../ConflictNoteBanner', () => ({
ConflictNoteBanner: () => <div data-testid="conflict-banner" />,
}))
vi.mock('../RawEditorView', () => ({
RawEditorView: () => <div data-testid="raw-editor-view" />,
}))
vi.mock('../SingleEditorView', () => ({
SingleEditorView: () => <div data-testid="single-editor-view" />,
}))
vi.mock('../DiffView', () => ({
DiffView: () => <div data-testid="diff-view" />,
}))
function createModel(overrides: Record<string, unknown> = {}) {
return {
activeTab: {
entry: {
path: '/vault/project/demo.md',
filename: 'demo.md',
title: 'Demo Note',
},
content: 'Body',
},
isLoadingNewTab: false,
entries: [],
editor: {},
diffMode: false,
diffContent: null,
diffLoading: false,
onToggleDiff: vi.fn(),
effectiveRawMode: false,
onToggleRaw: vi.fn(),
onRawContentChange: vi.fn(),
onSave: vi.fn(),
showEditor: true,
isArchived: false,
onUnarchiveNote: undefined,
path: '/vault/project/demo.md',
isConflicted: false,
onKeepMine: vi.fn(),
onKeepTheirs: vi.fn(),
breadcrumbBarRef: createRef<HTMLDivElement>(),
wordCount: 12,
titleSectionRef: createRef<HTMLDivElement>(),
showTitleSection: true,
hasDisplayIcon: false,
entryIcon: null,
vaultPath: '/vault',
onTitleChange: vi.fn(),
cssVars: {},
onNavigateWikilink: vi.fn(),
onEditorChange: vi.fn(),
isDeletedPreview: false,
rawLatestContentRef: { current: null },
forceRawMode: false,
showAIChat: false,
onToggleAIChat: vi.fn(),
inspectorCollapsed: true,
onToggleInspector: vi.fn(),
showDiffToggle: false,
onToggleFavorite: vi.fn(),
onToggleOrganized: vi.fn(),
onDeleteNote: vi.fn(),
onArchiveNote: vi.fn(),
...overrides,
} as never
}
describe('EditorContentLayout', () => {
it('does not render a standalone add-icon row when the note has no icon', () => {
const { container } = render(<EditorContentLayout {...createModel()} />)
expect(container.querySelector('.title-section__add-icon')).toBeNull()
expect(container.querySelector('.title-section__inline-add-icon')).not.toBeNull()
expect(screen.getByTestId('note-icon-add')).toBeInTheDocument()
})
it('keeps the existing icon and title inside the same title row', () => {
const { container } = render(
<EditorContentLayout
{...createModel({
hasDisplayIcon: true,
entryIcon: 'rocket',
})}
/>,
)
const titleRow = container.querySelector('.title-section__row')
expect(titleRow?.querySelector('[data-testid="note-icon-display"]')).not.toBeNull()
expect(titleRow?.querySelector('[data-testid="title-field-input"]')).not.toBeNull()
})
})

View File

@@ -150,21 +150,23 @@ function TitleSection({
<div ref={titleSectionRef} className="title-section" data-title-ui-visible={showTitleSection || undefined}>
{showTitleSection && (
<>
{!hasDisplayIcon && (
<div className="title-section__add-icon">
<NoteIcon icon={null} editable />
<div className="title-section__heading">
{!hasDisplayIcon && (
<div className="title-section__inline-add-icon">
<NoteIcon icon={null} editable />
</div>
)}
<div className={`title-section__row${hasDisplayIcon ? '' : ' title-section__row--no-icon'}`}>
{hasDisplayIcon && <NoteIcon icon={entryIcon} editable />}
<TitleField
title={activeTab.entry.title}
filename={activeTab.entry.filename}
editable
notePath={path}
vaultPath={vaultPath}
onTitleChange={(newTitle) => onTitleChange?.(path, newTitle)}
/>
</div>
)}
<div className={`title-section__row${hasDisplayIcon ? '' : ' title-section__row--no-icon'}`}>
{hasDisplayIcon && <NoteIcon icon={entryIcon} editable />}
<TitleField
title={activeTab.entry.title}
filename={activeTab.entry.filename}
editable
notePath={path}
vaultPath={vaultPath}
onTitleChange={(newTitle) => onTitleChange?.(path, newTitle)}
/>
</div>
<div className="title-section__separator" />
</>
@@ -173,6 +175,98 @@ function TitleSection({
)
}
function EditorChrome({
isArchived,
onUnarchiveNote,
path,
isConflicted,
onKeepMine,
onKeepTheirs,
diffMode,
diffContent,
onToggleDiff,
}: Pick<
EditorContentModel,
'isArchived' | 'onUnarchiveNote' | 'path' | 'isConflicted' | 'onKeepMine' | 'onKeepTheirs' | 'diffMode' | 'diffContent' | 'onToggleDiff'
>) {
return (
<>
{isArchived && onUnarchiveNote && (
<ArchivedNoteBanner onUnarchive={() => onUnarchiveNote(path)} />
)}
{isConflicted && (
<ConflictNoteBanner
onKeepMine={() => onKeepMine?.(path)}
onKeepTheirs={() => onKeepTheirs?.(path)}
/>
)}
{diffMode && <DiffModeView diffContent={diffContent} onToggleDiff={onToggleDiff} />}
</>
)
}
function EditorCanvas({
showEditor,
cssVars,
activeTab,
entryIcon,
hasDisplayIcon,
path,
showTitleSection,
titleSectionRef,
vaultPath,
onTitleChange,
editor,
entries,
onNavigateWikilink,
onEditorChange,
isDeletedPreview,
}: Pick<
EditorContentModel,
| 'showEditor'
| 'cssVars'
| 'activeTab'
| 'entryIcon'
| 'hasDisplayIcon'
| 'path'
| 'showTitleSection'
| 'titleSectionRef'
| 'vaultPath'
| 'onTitleChange'
| 'editor'
| 'entries'
| 'onNavigateWikilink'
| 'onEditorChange'
| 'isDeletedPreview'
>) {
if (!showEditor) return null
return (
<div className="editor-scroll-area" style={cssVars as React.CSSProperties}>
<div className="editor-content-wrapper">
<TitleSection
activeTab={activeTab}
entryIcon={entryIcon}
hasDisplayIcon={hasDisplayIcon}
path={path}
showTitleSection={showTitleSection}
titleSectionRef={titleSectionRef}
vaultPath={vaultPath}
onTitleChange={onTitleChange}
/>
<SingleEditorView
editor={editor}
entries={entries}
onNavigateWikilink={onNavigateWikilink}
onChange={onEditorChange}
vaultPath={vaultPath}
editable={!isDeletedPreview}
/>
</div>
</div>
)
}
export function EditorContentLayout(model: EditorContentModel) {
const {
activeTab,
@@ -237,16 +331,17 @@ export function EditorContentLayout(model: EditorContentModel) {
onUnarchiveNote: model.onUnarchiveNote,
}}
/>
{isArchived && onUnarchiveNote && (
<ArchivedNoteBanner onUnarchive={() => onUnarchiveNote(path)} />
)}
{isConflicted && (
<ConflictNoteBanner
onKeepMine={() => onKeepMine?.(path)}
onKeepTheirs={() => onKeepTheirs?.(path)}
/>
)}
{diffMode && <DiffModeView diffContent={diffContent} onToggleDiff={onToggleDiff} />}
<EditorChrome
isArchived={isArchived}
onUnarchiveNote={onUnarchiveNote}
path={path}
isConflicted={isConflicted}
onKeepMine={onKeepMine}
onKeepTheirs={onKeepTheirs}
diffMode={diffMode}
diffContent={diffContent}
onToggleDiff={onToggleDiff}
/>
<RawModeEditorSection
activeTab={activeTab}
entries={entries}
@@ -256,30 +351,23 @@ export function EditorContentLayout(model: EditorContentModel) {
rawLatestContentRef={rawLatestContentRef}
vaultPath={vaultPath}
/>
{showEditor && (
<div className="editor-scroll-area" style={cssVars as React.CSSProperties}>
<div className="editor-content-wrapper">
<TitleSection
activeTab={activeTab}
entryIcon={entryIcon}
hasDisplayIcon={hasDisplayIcon}
path={path}
showTitleSection={showTitleSection}
titleSectionRef={titleSectionRef}
vaultPath={vaultPath}
onTitleChange={onTitleChange}
/>
<SingleEditorView
editor={editor}
entries={entries}
onNavigateWikilink={onNavigateWikilink}
onChange={onEditorChange}
vaultPath={vaultPath}
editable={!isDeletedPreview}
/>
</div>
</div>
)}
<EditorCanvas
showEditor={showEditor}
cssVars={cssVars}
activeTab={activeTab}
entryIcon={entryIcon}
hasDisplayIcon={hasDisplayIcon}
path={path}
showTitleSection={showTitleSection}
titleSectionRef={titleSectionRef}
vaultPath={vaultPath}
onTitleChange={onTitleChange}
editor={editor}
entries={entries}
onNavigateWikilink={onNavigateWikilink}
onEditorChange={onEditorChange}
isDeletedPreview={isDeletedPreview}
/>
{isLoadingNewTab && showEditor && <EditorLoadingSkeleton />}
</div>
)