diff --git a/src/components/NoteList.tsx b/src/components/NoteList.tsx index 4ed2c785..c1d8e161 100644 --- a/src/components/NoteList.tsx +++ b/src/components/NoteList.tsx @@ -1,4 +1,5 @@ import { useState, useMemo, useCallback, memo } from 'react' +import { Virtuoso } from 'react-virtuoso' import type { VaultEntry, SidebarSelection, ModifiedFile } from '../types' import { Input } from '@/components/ui/input' import { @@ -133,6 +134,18 @@ function EntityView({ entity, groups, query, collapsedGroups, sortPrefs, onToggl ) } +function ListViewHeader({ typeDocument, isTrashView, expiredTrashCount, typeEntryMap, onClickNote }: { + typeDocument: VaultEntry | null; isTrashView: boolean; expiredTrashCount: number + typeEntryMap: Record; onClickNote: (entry: VaultEntry, e: React.MouseEvent) => void +}) { + return ( + <> + {typeDocument && } + + + ) +} + function ListView({ typeDocument, isTrashView, expiredTrashCount, searched, query, renderItem, typeEntryMap, onClickNote }: { typeDocument: VaultEntry | null; isTrashView: boolean; expiredTrashCount: number searched: VaultEntry[]; query: string @@ -140,15 +153,27 @@ function ListView({ typeDocument, isTrashView, expiredTrashCount, searched, quer typeEntryMap: Record; onClickNote: (entry: VaultEntry, e: React.MouseEvent) => void }) { const emptyText = isTrashView ? 'Trash is empty' : (query ? 'No matching notes' : 'No notes found') + const hasHeader = typeDocument || (isTrashView && expiredTrashCount > 0) + + if (searched.length === 0) { + return ( +
+ {hasHeader && } + +
+ ) + } + return ( -
- {typeDocument && } - - {searched.length === 0 - ? - : searched.map((entry) => renderItem(entry)) - } -
+ : undefined, + }} + itemContent={(_index, entry) => renderItem(entry)} + /> ) } diff --git a/src/mock-tauri.ts b/src/mock-tauri.ts index 27097cca..5f3dd467 100644 --- a/src/mock-tauri.ts +++ b/src/mock-tauri.ts @@ -1550,6 +1550,62 @@ const MOCK_ENTRIES: VaultEntry[] = [ }, ] +// --- Bulk entry generator for large vault testing --- + +const BULK_TYPES = ['Note', 'Project', 'Experiment', 'Responsibility', 'Procedure', 'Person', 'Event', 'Essay', 'Topic'] +const BULK_ADJECTIVES = ['Quick', 'Advanced', 'Daily', 'Weekly', 'Annual', 'Draft', 'Final', 'Revised', 'Archived', 'New'] +const BULK_NOUNS = ['Meeting', 'Strategy', 'Review', 'Plan', 'Analysis', 'Summary', 'Report', 'Guide', 'Checklist', 'Template', 'Framework', 'Workflow', 'Retrospective', 'Brainstorm', 'Proposal'] +const BULK_SNIPPETS = [ + 'Key findings from the latest analysis session.', + 'Notes on process improvements and next steps.', + 'Summary of decisions made during the review.', + 'Action items and follow-ups from discussion.', + 'Draft outline for upcoming deliverable.', + 'Reference material for the ongoing initiative.', + 'Tracking progress on quarterly objectives.', + 'Comparison of different approaches considered.', +] + +function generateBulkEntries(count: number): VaultEntry[] { + const now = Date.now() / 1000 + const entries: VaultEntry[] = [] + for (let i = 0; i < count; i++) { + const type = BULK_TYPES[i % BULK_TYPES.length] + const adj = BULK_ADJECTIVES[i % BULK_ADJECTIVES.length] + const noun = BULK_NOUNS[i % BULK_NOUNS.length] + const title = `${adj} ${noun} ${i + 1}` + const slug = title.toLowerCase().replace(/\s+/g, '-') + const folder = type.toLowerCase() + entries.push({ + path: `/Users/luca/Laputa/${folder}/${slug}.md`, + filename: `${slug}.md`, + title, + isA: type, + aliases: [], + belongsTo: [], + relatedTo: [], + status: i % 4 === 0 ? 'Active' : i % 4 === 1 ? 'Paused' : i % 4 === 2 ? 'Done' : null, + owner: i % 3 === 0 ? 'Luca Rossi' : null, + cadence: null, + archived: false, + trashed: false, + trashedAt: null, + modifiedAt: now - i * 600, + createdAt: now - 86400 * 90 - i * 3600, + fileSize: 500 + (i % 2000), + snippet: BULK_SNIPPETS[i % BULK_SNIPPETS.length], + relationships: {}, + icon: null, + color: null, + order: null, + }) + } + return entries +} + +// Append 9000 generated entries for realistic large-vault testing +MOCK_ENTRIES.push(...generateBulkEntries(9000)) + function mockFileHistory(path: string): GitCommit[] { const filename = path.split('/').pop()?.replace('.md', '') ?? 'unknown' const now = Math.floor(Date.now() / 1000) diff --git a/src/test/setup.ts b/src/test/setup.ts index a9d0dd31..705a15e4 100644 --- a/src/test/setup.ts +++ b/src/test/setup.ts @@ -1 +1,43 @@ import '@testing-library/jest-dom/vitest' +import { vi } from 'vitest' + +// Mock react-virtuoso: JSDOM has no real viewport, so render all items directly +vi.mock('react-virtuoso', () => { + const React = require('react') + return { + Virtuoso: ({ data, itemContent, components }: { + data?: unknown[] + itemContent?: (index: number, item: unknown) => React.ReactNode + components?: { Header?: React.ComponentType } + }) => { + const Header = components?.Header + return React.createElement('div', { 'data-testid': 'virtuoso-mock' }, + Header ? React.createElement(Header) : null, + data?.map((item: unknown, index: number) => + React.createElement('div', { key: index }, itemContent?.(index, item)) + ) + ) + }, + GroupedVirtuoso: ({ groupCounts, groupContent, itemContent }: { + groupCounts: number[] + groupContent: (index: number) => React.ReactNode + itemContent: (index: number, groupIndex: number) => React.ReactNode + }) => { + const React = require('react') + let globalIndex = 0 + return React.createElement('div', { 'data-testid': 'grouped-virtuoso-mock' }, + groupCounts?.map((count: number, groupIndex: number) => { + const items = [] + for (let i = 0; i < count; i++) { + items.push(React.createElement('div', { key: globalIndex }, itemContent(globalIndex, groupIndex))) + globalIndex++ + } + return React.createElement('div', { key: `group-${groupIndex}` }, + groupContent(groupIndex), + ...items + ) + }) + ) + }, + } +})