fix: improve note icon property editing
This commit is contained in:
120
src/components/editor-content/EditorContentLayout.test.tsx
Normal file
120
src/components/editor-content/EditorContentLayout.test.tsx
Normal 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()
|
||||
})
|
||||
})
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user