diff --git a/src/components/Editor.test.tsx b/src/components/Editor.test.tsx index e5254ba5..c4275751 100644 --- a/src/components/Editor.test.tsx +++ b/src/components/Editor.test.tsx @@ -45,6 +45,8 @@ const mockEntry: VaultEntry = { owner: 'Luca', cadence: null, archived: false, + trashed: false, + trashedAt: null, modifiedAt: 1700000000, createdAt: null, fileSize: 1024, diff --git a/src/components/Inspector.test.tsx b/src/components/Inspector.test.tsx index 8bb351f6..26e95101 100644 --- a/src/components/Inspector.test.tsx +++ b/src/components/Inspector.test.tsx @@ -15,6 +15,8 @@ const mockEntry: VaultEntry = { owner: 'Luca Rossi', cadence: null, archived: false, + trashed: false, + trashedAt: null, modifiedAt: 1707900000, createdAt: null, fileSize: 1024, @@ -54,6 +56,8 @@ const referrerEntry: VaultEntry = { owner: null, cadence: null, archived: false, + trashed: false, + trashedAt: null, modifiedAt: 1707900000, createdAt: null, fileSize: 200, diff --git a/src/components/NoteList.test.tsx b/src/components/NoteList.test.tsx index 101fc87d..d1e1b3a8 100644 --- a/src/components/NoteList.test.tsx +++ b/src/components/NoteList.test.tsx @@ -1,6 +1,6 @@ import { render, screen, fireEvent } from '@testing-library/react' import { describe, it, expect, vi } from 'vitest' -import { NoteList } from './NoteList' +import { NoteList, filterEntries } from './NoteList' import type { VaultEntry, SidebarSelection } from '../types' const allSelection: SidebarSelection = { kind: 'filter', filter: 'all' } @@ -19,6 +19,8 @@ const mockEntries: VaultEntry[] = [ owner: 'Luca', cadence: null, archived: false, + trashed: false, + trashedAt: null, modifiedAt: 1700000000, createdAt: null, fileSize: 1024, @@ -41,6 +43,8 @@ const mockEntries: VaultEntry[] = [ owner: null, cadence: null, archived: false, + trashed: false, + trashedAt: null, modifiedAt: 1700000000, createdAt: null, fileSize: 847, @@ -64,6 +68,8 @@ const mockEntries: VaultEntry[] = [ owner: null, cadence: null, archived: false, + trashed: false, + trashedAt: null, modifiedAt: 1700000000, createdAt: null, fileSize: 320, @@ -84,6 +90,8 @@ const mockEntries: VaultEntry[] = [ owner: null, cadence: null, archived: false, + trashed: false, + trashedAt: null, modifiedAt: 1700000000, createdAt: null, fileSize: 512, @@ -104,6 +112,8 @@ const mockEntries: VaultEntry[] = [ owner: null, cadence: null, archived: false, + trashed: false, + trashedAt: null, modifiedAt: 1700000000, createdAt: null, fileSize: 256, @@ -247,3 +257,129 @@ describe('NoteList', () => { expect(screen.getByText('Build a personal knowledge management app.')).toBeInTheDocument() }) }) + +// --- Trash feature tests --- + +const trashedEntry: VaultEntry = { + path: '/vault/note/old-draft.md', + filename: 'old-draft.md', + title: 'Old Draft Notes', + isA: 'Note', + aliases: [], + belongsTo: [], + relatedTo: [], + status: null, + owner: null, + cadence: null, + archived: false, + trashed: true, + trashedAt: Date.now() / 1000 - 86400 * 5, + modifiedAt: 1700000000, + createdAt: null, + fileSize: 280, + snippet: 'Some draft content that is no longer needed.', + relationships: {}, + icon: null, + color: null, +} + +const expiredTrashedEntry: VaultEntry = { + path: '/vault/note/deprecated-api.md', + filename: 'deprecated-api.md', + title: 'Deprecated API Notes', + isA: 'Note', + aliases: [], + belongsTo: [], + relatedTo: [], + status: null, + owner: null, + cadence: null, + archived: false, + trashed: true, + trashedAt: Date.now() / 1000 - 86400 * 35, + modifiedAt: 1700000000, + createdAt: null, + fileSize: 190, + snippet: 'Old API docs replaced by v2.', + relationships: {}, + icon: null, + color: null, +} + +const entriesWithTrashed = [...mockEntries, trashedEntry, expiredTrashedEntry] + +describe('filterEntries — trash', () => { + it('excludes trashed entries from "all" filter', () => { + const result = filterEntries(entriesWithTrashed, { kind: 'filter', filter: 'all' }) + expect(result.find((e) => e.title === 'Old Draft Notes')).toBeUndefined() + expect(result.find((e) => e.title === 'Build Laputa App')).toBeDefined() + }) + + it('excludes trashed entries from section group', () => { + const result = filterEntries(entriesWithTrashed, { kind: 'sectionGroup', type: 'Note' }) + expect(result.find((e) => e.title === 'Old Draft Notes')).toBeUndefined() + }) + + it('trash filter returns only trashed entries', () => { + const result = filterEntries(entriesWithTrashed, { kind: 'filter', filter: 'trash' }) + expect(result).toHaveLength(2) + expect(result.every((e) => e.trashed)).toBe(true) + }) + + it('archived filter excludes trashed entries', () => { + const archivedAndTrashed = [ + ...mockEntries, + { ...mockEntries[0], path: '/archived.md', archived: true, trashed: false, trashedAt: null, title: 'Archived Note' }, + trashedEntry, + ] + const result = filterEntries(archivedAndTrashed, { kind: 'filter', filter: 'archived' }) + expect(result.find((e) => e.title === 'Archived Note')).toBeDefined() + expect(result.find((e) => e.title === 'Old Draft Notes')).toBeUndefined() + }) + + it('topic filter excludes trashed entries', () => { + const topicEntry: VaultEntry = { ...mockEntries[4] } // Software Development topic + const trashedWithTopic: VaultEntry = { + ...trashedEntry, + relatedTo: ['[[topic/software-development]]'], + } + const all = [...mockEntries, trashedWithTopic] + const result = filterEntries(all, { kind: 'topic', entry: topicEntry }) + expect(result.find((e) => e.title === 'Old Draft Notes')).toBeUndefined() + // Normal entry with that topic should still appear + expect(result.find((e) => e.title === 'Build Laputa App')).toBeDefined() + }) +}) + +describe('NoteList — trash view', () => { + const trashSelection: SidebarSelection = { kind: 'filter', filter: 'trash' } + + it('shows "Trash" header when trash filter is active', () => { + render() + expect(screen.getByText('Trash')).toBeInTheDocument() + }) + + it('shows only trashed entries in trash view', () => { + render() + expect(screen.getByText('Old Draft Notes')).toBeInTheDocument() + expect(screen.getByText('Deprecated API Notes')).toBeInTheDocument() + expect(screen.queryByText('Build Laputa App')).not.toBeInTheDocument() + }) + + it('shows TRASHED badge on trashed entries', () => { + render() + const badges = screen.getAllByText('TRASHED') + expect(badges.length).toBeGreaterThanOrEqual(1) + }) + + it('shows 30-day warning banner when expired notes exist', () => { + render() + expect(screen.getByText('Notes in trash for 30+ days will be permanently deleted')).toBeInTheDocument() + expect(screen.getByText(/1 note is past the 30-day retention period/)).toBeInTheDocument() + }) + + it('shows "Trash is empty" when no trashed entries', () => { + render() + expect(screen.getByText('Trash is empty')).toBeInTheDocument() + }) +}) diff --git a/src/components/Sidebar.test.tsx b/src/components/Sidebar.test.tsx index dfc6d417..834b7ff3 100644 --- a/src/components/Sidebar.test.tsx +++ b/src/components/Sidebar.test.tsx @@ -28,6 +28,8 @@ const mockEntries: VaultEntry[] = [ owner: 'Luca', cadence: null, archived: false, + trashed: false, + trashedAt: null, modifiedAt: 1700000000, createdAt: null, fileSize: 1024, @@ -48,6 +50,8 @@ const mockEntries: VaultEntry[] = [ owner: 'Luca', cadence: null, archived: false, + trashed: false, + trashedAt: null, modifiedAt: 1700000000, createdAt: null, fileSize: 512, @@ -68,6 +72,8 @@ const mockEntries: VaultEntry[] = [ owner: 'Luca', cadence: null, archived: false, + trashed: false, + trashedAt: null, modifiedAt: 1700000000, createdAt: null, fileSize: 256, @@ -88,6 +94,8 @@ const mockEntries: VaultEntry[] = [ owner: 'Luca', cadence: 'Weekly', archived: false, + trashed: false, + trashedAt: null, modifiedAt: 1700000000, createdAt: null, fileSize: 128, @@ -108,6 +116,8 @@ const mockEntries: VaultEntry[] = [ owner: null, cadence: null, archived: false, + trashed: false, + trashedAt: null, modifiedAt: 1700000000, createdAt: null, fileSize: 256, @@ -128,6 +138,8 @@ const mockEntries: VaultEntry[] = [ owner: null, cadence: null, archived: false, + trashed: false, + trashedAt: null, modifiedAt: 1700000000, createdAt: null, fileSize: 180, @@ -148,6 +160,8 @@ const mockEntries: VaultEntry[] = [ owner: null, cadence: null, archived: false, + trashed: false, + trashedAt: null, modifiedAt: 1700000000, createdAt: null, fileSize: 100, @@ -168,6 +182,8 @@ const mockEntries: VaultEntry[] = [ owner: null, cadence: null, archived: false, + trashed: false, + trashedAt: null, modifiedAt: 1700000000, createdAt: null, fileSize: 200, @@ -315,6 +331,8 @@ describe('Sidebar', () => { owner: null, cadence: null, archived: false, + trashed: false, + trashedAt: null, modifiedAt: 1700000000, createdAt: null, fileSize: 200, @@ -335,6 +353,8 @@ describe('Sidebar', () => { owner: null, cadence: null, archived: false, + trashed: false, + trashedAt: null, modifiedAt: 1700000000, createdAt: null, fileSize: 200, @@ -355,6 +375,8 @@ describe('Sidebar', () => { owner: null, cadence: null, archived: false, + trashed: false, + trashedAt: null, modifiedAt: 1700000000, createdAt: null, fileSize: 300, @@ -403,6 +425,8 @@ describe('Sidebar', () => { owner: null, cadence: null, archived: false, + trashed: false, + trashedAt: null, modifiedAt: 1700000000, createdAt: null, fileSize: 200, diff --git a/src/hooks/useNoteActions.ts b/src/hooks/useNoteActions.ts index 30ede554..cdd17628 100644 --- a/src/hooks/useNoteActions.ts +++ b/src/hooks/useNoteActions.ts @@ -213,7 +213,8 @@ export function useNoteActions( path, filename: `${slug}.md`, title, isA: type, aliases: [], belongsTo: [], relatedTo: [], status: noStatusTypes.has(type) ? null : 'Active', - owner: null, cadence: null, archived: false, modifiedAt: now, createdAt: now, fileSize: 0, + owner: null, cadence: null, archived: false, trashed: false, trashedAt: null, + modifiedAt: now, createdAt: now, fileSize: 0, snippet: '', relationships: {}, icon: null, color: null, } @@ -240,7 +241,7 @@ export function useNoteActions( const newEntry: VaultEntry = { path, filename: `${slug}.md`, title: typeName, isA: 'Type', aliases: [], belongsTo: [], relatedTo: [], - status: null, owner: null, cadence: null, archived: false, + status: null, owner: null, cadence: null, archived: false, trashed: false, trashedAt: null, modifiedAt: now, createdAt: now, fileSize: 0, snippet: '', relationships: {}, icon: null, color: null, }