fix: hide type note from note list, make header clickable to access it
When browsing a type section (e.g. "Book"), the type-defining note (types/book.md) no longer appears as a PinnedCard in the note list. Instead, the header title becomes clickable to navigate to the type note. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1107,6 +1107,72 @@ describe('NoteList — multi-select', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// --- Type note filtering tests ---
|
||||
|
||||
const typeEntry: VaultEntry = {
|
||||
path: '/Users/luca/Laputa/types/project.md',
|
||||
filename: 'project.md',
|
||||
title: 'Project',
|
||||
isA: 'Type',
|
||||
aliases: [],
|
||||
belongsTo: [],
|
||||
relatedTo: [],
|
||||
status: null,
|
||||
owner: null,
|
||||
cadence: null,
|
||||
archived: false,
|
||||
trashed: false,
|
||||
trashedAt: null,
|
||||
modifiedAt: 1700000000,
|
||||
createdAt: null,
|
||||
fileSize: 200,
|
||||
snippet: 'Defines the Project type.',
|
||||
wordCount: 50,
|
||||
relationships: {},
|
||||
icon: null,
|
||||
color: null,
|
||||
order: null,
|
||||
outgoingLinks: [],
|
||||
}
|
||||
|
||||
const entriesWithType = [...mockEntries, typeEntry]
|
||||
|
||||
describe('NoteList — type note filtering', () => {
|
||||
beforeEach(() => {
|
||||
noopSelect.mockClear()
|
||||
noopReplace.mockClear()
|
||||
})
|
||||
|
||||
it('does not show type note PinnedCard when browsing a sectionGroup', () => {
|
||||
render(
|
||||
<NoteList entries={entriesWithType} selection={{ kind: 'sectionGroup', type: 'Project' }} selectedNote={null} onSelectNote={noopSelect} onReplaceActiveTab={noopReplace} allContent={{}} onCreateNote={vi.fn()} />
|
||||
)
|
||||
// The type note snippet should NOT be visible (PinnedCard was removed)
|
||||
expect(screen.queryByText('Defines the Project type.')).not.toBeInTheDocument()
|
||||
// But the Project instance should still be in the list
|
||||
expect(screen.getByText('Build Laputa App')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows clickable header title that navigates to type note', () => {
|
||||
render(
|
||||
<NoteList entries={entriesWithType} selection={{ kind: 'sectionGroup', type: 'Project' }} selectedNote={null} onSelectNote={noopSelect} onReplaceActiveTab={noopReplace} allContent={{}} onCreateNote={vi.fn()} />
|
||||
)
|
||||
const headerLink = screen.getByTestId('type-header-link')
|
||||
expect(headerLink).toBeInTheDocument()
|
||||
expect(headerLink.textContent).toBe('Project')
|
||||
expect(headerLink.style.cursor).toBe('pointer')
|
||||
fireEvent.click(headerLink)
|
||||
expect(noopReplace).toHaveBeenCalledWith(typeEntry)
|
||||
})
|
||||
|
||||
it('header is not clickable when not viewing a type section', () => {
|
||||
render(
|
||||
<NoteList entries={entriesWithType} selection={allSelection} selectedNote={null} onSelectNote={noopSelect} onReplaceActiveTab={noopReplace} allContent={{}} onCreateNote={vi.fn()} />
|
||||
)
|
||||
expect(screen.queryByTestId('type-header-link')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('NoteList — traffic light padding when sidebar collapsed', () => {
|
||||
it('adds left padding to header when sidebarCollapsed is true', () => {
|
||||
const { container } = render(
|
||||
|
||||
@@ -136,31 +136,24 @@ function EntityView({ entity, groups, query, collapsedGroups, sortPrefs, onToggl
|
||||
)
|
||||
}
|
||||
|
||||
function ListViewHeader({ typeDocument, isTrashView, expiredTrashCount, typeEntryMap, onClickNote }: {
|
||||
typeDocument: VaultEntry | null; isTrashView: boolean; expiredTrashCount: number
|
||||
typeEntryMap: Record<string, VaultEntry>; onClickNote: (entry: VaultEntry, e: React.MouseEvent) => void
|
||||
function ListViewHeader({ isTrashView, expiredTrashCount }: {
|
||||
isTrashView: boolean; expiredTrashCount: number
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
{typeDocument && <PinnedCard entry={typeDocument} typeEntryMap={typeEntryMap} onClickNote={onClickNote} />}
|
||||
<TrashWarningBanner expiredCount={isTrashView ? expiredTrashCount : 0} />
|
||||
</>
|
||||
)
|
||||
return <TrashWarningBanner expiredCount={isTrashView ? expiredTrashCount : 0} />
|
||||
}
|
||||
|
||||
function ListView({ typeDocument, isTrashView, isChangesView, expiredTrashCount, searched, query, renderItem, typeEntryMap, onClickNote }: {
|
||||
typeDocument: VaultEntry | null; isTrashView: boolean; isChangesView?: boolean; expiredTrashCount: number
|
||||
function ListView({ isTrashView, isChangesView, expiredTrashCount, searched, query, renderItem }: {
|
||||
isTrashView: boolean; isChangesView?: boolean; expiredTrashCount: number
|
||||
searched: VaultEntry[]; query: string
|
||||
renderItem: (entry: VaultEntry) => React.ReactNode
|
||||
typeEntryMap: Record<string, VaultEntry>; onClickNote: (entry: VaultEntry, e: React.MouseEvent) => void
|
||||
}) {
|
||||
const emptyText = isChangesView ? 'No pending changes' : isTrashView ? 'Trash is empty' : (query ? 'No matching notes' : 'No notes found')
|
||||
const hasHeader = typeDocument || (isTrashView && expiredTrashCount > 0)
|
||||
const hasHeader = isTrashView && expiredTrashCount > 0
|
||||
|
||||
if (searched.length === 0) {
|
||||
return (
|
||||
<div className="h-full overflow-y-auto">
|
||||
{hasHeader && <ListViewHeader typeDocument={typeDocument} isTrashView={isTrashView} expiredTrashCount={expiredTrashCount} typeEntryMap={typeEntryMap} onClickNote={onClickNote} />}
|
||||
{hasHeader && <ListViewHeader isTrashView={isTrashView} expiredTrashCount={expiredTrashCount} />}
|
||||
<EmptyMessage text={emptyText} />
|
||||
</div>
|
||||
)
|
||||
@@ -172,7 +165,7 @@ function ListView({ typeDocument, isTrashView, isChangesView, expiredTrashCount,
|
||||
data={searched}
|
||||
overscan={200}
|
||||
components={{
|
||||
Header: hasHeader ? () => <ListViewHeader typeDocument={typeDocument} isTrashView={isTrashView} expiredTrashCount={expiredTrashCount} typeEntryMap={typeEntryMap} onClickNote={onClickNote} /> : undefined,
|
||||
Header: hasHeader ? () => <ListViewHeader isTrashView={isTrashView} expiredTrashCount={expiredTrashCount} /> : undefined,
|
||||
}}
|
||||
itemContent={(_index, entry) => renderItem(entry)}
|
||||
/>
|
||||
@@ -369,7 +362,14 @@ function NoteListInner({ entries, selection, selectedNote, allContent, modifiedF
|
||||
return (
|
||||
<div className="flex flex-col select-none overflow-hidden border-r border-border bg-card text-foreground" style={{ height: '100%' }}>
|
||||
<div className="flex h-[52px] shrink-0 items-center justify-between border-b border-border px-4" onMouseDown={onDragMouseDown} style={{ cursor: 'default', paddingLeft: sidebarCollapsed ? 80 : undefined }}>
|
||||
<h3 className="m-0 min-w-0 flex-1 truncate text-[14px] font-semibold">{resolveHeaderTitle(selection, typeDocument)}</h3>
|
||||
<h3
|
||||
className="m-0 min-w-0 flex-1 truncate text-[14px] font-semibold"
|
||||
style={typeDocument ? { cursor: 'pointer' } : undefined}
|
||||
onClick={typeDocument ? () => onReplaceActiveTab(typeDocument) : undefined}
|
||||
data-testid={typeDocument ? 'type-header-link' : undefined}
|
||||
>
|
||||
{resolveHeaderTitle(selection, typeDocument)}
|
||||
</h3>
|
||||
<div className="flex items-center gap-3" style={{ WebkitAppRegion: 'no-drag' } as React.CSSProperties}>
|
||||
{!isEntityView && <SortDropdown groupLabel="__list__" current={listSort} direction={listDirection} onChange={handleSortChange} />}
|
||||
<button className="flex items-center text-muted-foreground transition-colors hover:text-foreground" onClick={() => { setSearchVisible(!searchVisible); if (searchVisible) setSearch('') }} title="Search notes">
|
||||
@@ -391,7 +391,7 @@ function NoteListInner({ entries, selection, selectedNote, allContent, modifiedF
|
||||
{isEntityView && selection.kind === 'entity' ? (
|
||||
<EntityView entity={selection.entry} groups={searchedGroups} query={query} collapsedGroups={collapsedGroups} sortPrefs={sortPrefs} onToggleGroup={toggleGroup} onSortChange={handleSortChange} renderItem={renderItem} typeEntryMap={typeEntryMap} onClickNote={handleClickNote} />
|
||||
) : (
|
||||
<ListView typeDocument={typeDocument} isTrashView={isTrashView} isChangesView={isChangesView} expiredTrashCount={expiredTrashCount} searched={searched} query={query} renderItem={renderItem} typeEntryMap={typeEntryMap} onClickNote={handleClickNote} />
|
||||
<ListView isTrashView={isTrashView} isChangesView={isChangesView} expiredTrashCount={expiredTrashCount} searched={searched} query={query} renderItem={renderItem} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user