refactor: remove Trash system — delete is now permanent with confirm modal
Remove all vestiges of the abandoned Trash system: trashed/trashedAt fields from types, frontmatter parsing, sidebar filtering, editor banners, inspector components, mock data, and all related tests. Delete is already permanent via useDeleteActions with a confirmation dialog. Notes with trashed:true in existing vault frontmatter are now treated as normal notes (the flag is ignored by the parser). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -11,7 +11,6 @@ interface FilterPillsProps {
|
||||
const PILLS: { value: NoteListFilter; label: string }[] = [
|
||||
{ value: 'open', label: 'Open' },
|
||||
{ value: 'archived', label: 'Archived' },
|
||||
{ value: 'trashed', label: 'Trashed' },
|
||||
]
|
||||
|
||||
const BOTTOM_GRADIENT = 'linear-gradient(to bottom, transparent 0%, var(--card, #fff) 30%, var(--card, #fff) 100%)'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { MagnifyingGlass, Plus, Trash } from '@phosphor-icons/react'
|
||||
import { MagnifyingGlass, Plus } from '@phosphor-icons/react'
|
||||
import type { VaultEntry } from '../../types'
|
||||
import type { SortOption, SortDirection } from '../../utils/noteListHelpers'
|
||||
import { Input } from '@/components/ui/input'
|
||||
@@ -6,12 +6,10 @@ import { useDragRegion } from '../../hooks/useDragRegion'
|
||||
import { SortDropdown } from '../SortDropdown'
|
||||
import { ListPropertiesPopover } from './ListPropertiesPopover'
|
||||
|
||||
export function NoteListHeader({ title, typeDocument, isEntityView, isTrashView, trashCount, listSort, listDirection, customProperties, sidebarCollapsed, searchVisible, search, isSectionGroup, entries, onSortChange, onCreateNote, onOpenType, onToggleSearch, onSearchChange, onEmptyTrash, onUpdateTypeProperty }: {
|
||||
export function NoteListHeader({ title, typeDocument, isEntityView, listSort, listDirection, customProperties, sidebarCollapsed, searchVisible, search, isSectionGroup, entries, onSortChange, onCreateNote, onOpenType, onToggleSearch, onSearchChange, onUpdateTypeProperty }: {
|
||||
title: string
|
||||
typeDocument: VaultEntry | null
|
||||
isEntityView: boolean
|
||||
isTrashView: boolean
|
||||
trashCount: number
|
||||
listSort: SortOption
|
||||
listDirection: SortDirection
|
||||
customProperties: string[]
|
||||
@@ -25,7 +23,6 @@ export function NoteListHeader({ title, typeDocument, isEntityView, isTrashView,
|
||||
onOpenType: (entry: VaultEntry) => void
|
||||
onToggleSearch: () => void
|
||||
onSearchChange: (value: string) => void
|
||||
onEmptyTrash?: () => void
|
||||
onUpdateTypeProperty?: (path: string, key: string, value: string | number | boolean | string[] | null) => void
|
||||
}) {
|
||||
const { onMouseDown: onDragMouseDown } = useDragRegion()
|
||||
@@ -48,21 +45,9 @@ export function NoteListHeader({ title, typeDocument, isEntityView, isTrashView,
|
||||
{isSectionGroup && typeDocument && entries && onUpdateTypeProperty && (
|
||||
<ListPropertiesPopover typeDocument={typeDocument} entries={entries} onSave={onUpdateTypeProperty} />
|
||||
)}
|
||||
{isTrashView && trashCount > 0 && (
|
||||
<button
|
||||
className="flex items-center text-destructive transition-colors hover:text-destructive/80"
|
||||
onClick={onEmptyTrash}
|
||||
title="Empty Trash"
|
||||
data-testid="empty-trash-btn"
|
||||
>
|
||||
<Trash size={16} />
|
||||
</button>
|
||||
)}
|
||||
{!isTrashView && (
|
||||
<button className="flex items-center text-muted-foreground transition-colors hover:text-foreground" onClick={() => onCreateNote()} title="Create new note">
|
||||
<Plus size={16} />
|
||||
</button>
|
||||
)}
|
||||
<button className="flex items-center text-muted-foreground transition-colors hover:text-foreground" onClick={() => onCreateNote()} title="Create new note">
|
||||
<Plus size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{searchVisible && (
|
||||
|
||||
@@ -3,18 +3,11 @@ import type { VaultEntry } from '../../types'
|
||||
import type { SortOption, SortDirection, SortConfig, RelationshipGroup } from '../../utils/noteListHelpers'
|
||||
import { PinnedCard } from './PinnedCard'
|
||||
import { RelationshipGroupSection } from './RelationshipGroupSection'
|
||||
import { TrashWarningBanner, EmptyMessage } from './TrashWarningBanner'
|
||||
import { EmptyMessage } from './TrashWarningBanner'
|
||||
|
||||
function ListViewHeader({ isTrashView, expiredTrashCount }: {
|
||||
isTrashView: boolean; expiredTrashCount: number
|
||||
}) {
|
||||
return <TrashWarningBanner expiredCount={isTrashView ? expiredTrashCount : 0} />
|
||||
}
|
||||
|
||||
function resolveEmptyText(isChangesView: boolean, changesError: string | null | undefined, isTrashView: boolean, isArchivedView: boolean, isInboxView: boolean, query: string): string {
|
||||
function resolveEmptyText(isChangesView: boolean, changesError: string | null | undefined, isArchivedView: boolean, isInboxView: boolean, query: string): string {
|
||||
if (isChangesView && changesError) return `Failed to load changes: ${changesError}`
|
||||
if (isChangesView) return 'No pending changes'
|
||||
if (isTrashView) return 'Trash is empty'
|
||||
if (isArchivedView) return 'No archived notes'
|
||||
if (isInboxView) return query ? 'No matching notes' : 'All notes are organized'
|
||||
return query ? 'No matching notes' : 'No notes found'
|
||||
@@ -40,20 +33,18 @@ export function EntityView({ entity, groups, query, collapsedGroups, sortPrefs,
|
||||
)
|
||||
}
|
||||
|
||||
export function ListView({ isTrashView, isArchivedView, isChangesView, isInboxView, changesError, expiredTrashCount, deletedCount = 0, searched, query, renderItem, virtuosoRef }: {
|
||||
isTrashView: boolean; isArchivedView?: boolean; isChangesView?: boolean; isInboxView?: boolean; changesError?: string | null; expiredTrashCount: number
|
||||
export function ListView({ isArchivedView, isChangesView, isInboxView, changesError, deletedCount = 0, searched, query, renderItem, virtuosoRef }: {
|
||||
isArchivedView?: boolean; isChangesView?: boolean; isInboxView?: boolean; changesError?: string | null
|
||||
deletedCount?: number; searched: VaultEntry[]; query: string
|
||||
renderItem: (entry: VaultEntry) => React.ReactNode
|
||||
virtuosoRef?: React.RefObject<VirtuosoHandle | null>
|
||||
}) {
|
||||
const emptyText = resolveEmptyText(!!isChangesView, changesError ?? null, isTrashView, !!isArchivedView, !!isInboxView, query)
|
||||
const hasHeader = isTrashView && expiredTrashCount > 0
|
||||
const emptyText = resolveEmptyText(!!isChangesView, changesError ?? null, !!isArchivedView, !!isInboxView, query)
|
||||
const hasDeletedOnly = !!isChangesView && deletedCount > 0 && searched.length === 0
|
||||
|
||||
if (searched.length === 0 && !hasDeletedOnly) {
|
||||
return (
|
||||
<div className="h-full overflow-y-auto">
|
||||
{hasHeader && <ListViewHeader isTrashView={isTrashView} expiredTrashCount={expiredTrashCount} />}
|
||||
<EmptyMessage text={emptyText} />
|
||||
</div>
|
||||
)
|
||||
@@ -69,9 +60,6 @@ export function ListView({ isTrashView, isArchivedView, isChangesView, isInboxVi
|
||||
style={{ height: '100%' }}
|
||||
data={searched}
|
||||
overscan={200}
|
||||
components={{
|
||||
Header: hasHeader ? () => <ListViewHeader isTrashView={isTrashView} expiredTrashCount={expiredTrashCount} /> : undefined,
|
||||
}}
|
||||
itemContent={(_index, entry) => renderItem(entry)}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -1,17 +1,4 @@
|
||||
import { Warning, TrashSimple } from '@phosphor-icons/react'
|
||||
|
||||
export function TrashWarningBanner({ expiredCount }: { expiredCount: number }) {
|
||||
if (expiredCount === 0) return null
|
||||
return (
|
||||
<div className="flex items-start gap-2 border-b border-[var(--border)]" style={{ padding: '10px 12px', background: 'color-mix(in srgb, var(--destructive) 6%, transparent)' }}>
|
||||
<Warning size={16} className="shrink-0" style={{ color: 'var(--destructive)', marginTop: 1 }} />
|
||||
<div>
|
||||
<div style={{ fontSize: 12, fontWeight: 600, color: 'var(--destructive)' }}>Notes in trash for 30+ days will be permanently deleted</div>
|
||||
<div className="text-muted-foreground" style={{ fontSize: 11 }}>{expiredCount} {expiredCount === 1 ? 'note is' : 'notes are'} past the 30-day retention period</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
import { TrashSimple } from '@phosphor-icons/react'
|
||||
|
||||
export function EmptyMessage({ text }: { text: string }) {
|
||||
return <div className="px-4 py-8 text-center text-[13px] text-muted-foreground">{text}</div>
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
} from '../../utils/noteListHelpers'
|
||||
import type { InboxPeriod } from '../../types'
|
||||
import { buildTypeEntryMap } from '../../utils/typeColors'
|
||||
import { filterByQuery, filterGroupsByQuery, countExpiredTrash, createNoteStatusResolver, isModifiedEntry } from './noteListUtils'
|
||||
import { filterByQuery, filterGroupsByQuery, createNoteStatusResolver, isModifiedEntry } from './noteListUtils'
|
||||
import type { MultiSelectState } from '../../hooks/useMultiSelect'
|
||||
|
||||
// --- useTypeEntryMap ---
|
||||
@@ -45,7 +45,6 @@ interface NoteListDataParams {
|
||||
|
||||
export function useNoteListData({ entries, selection, query, listSort, listDirection, modifiedPathSet, modifiedSuffixes, subFilter, inboxPeriod, views }: NoteListDataParams) {
|
||||
const isEntityView = selection.kind === 'entity'
|
||||
const isTrashView = (selection.kind === 'filter' && selection.filter === 'trash') || subFilter === 'trashed'
|
||||
const isArchivedView = (selection.kind === 'filter' && selection.filter === 'archived') || subFilter === 'archived'
|
||||
|
||||
const filteredEntries = useFilteredEntries(entries, selection, modifiedPathSet, modifiedSuffixes, subFilter, inboxPeriod, views)
|
||||
@@ -64,12 +63,7 @@ export function useNoteListData({ entries, selection, query, listSort, listDirec
|
||||
return filterGroupsByQuery(groups, query)
|
||||
}, [isEntityView, selection, entries, query])
|
||||
|
||||
const expiredTrashCount = useMemo(
|
||||
() => isTrashView ? countExpiredTrash(searched) : 0,
|
||||
[isTrashView, searched],
|
||||
)
|
||||
|
||||
return { isEntityView, isTrashView, isArchivedView, searched, searchedGroups, expiredTrashCount }
|
||||
return { isEntityView, isArchivedView, searched, searchedGroups }
|
||||
}
|
||||
|
||||
// --- useNoteListSearch ---
|
||||
@@ -193,22 +187,22 @@ function handleSelectAllKey(e: KeyboardEvent, multiSelect: MultiSelectState, isE
|
||||
multiSelect.selectAll()
|
||||
}
|
||||
|
||||
function handleBulkActionKey(e: KeyboardEvent, multiSelect: MultiSelectState, onArchive: () => void, onTrash: () => void) {
|
||||
function handleBulkActionKey(e: KeyboardEvent, multiSelect: MultiSelectState, onArchive: () => void, onDelete: () => void) {
|
||||
if (!multiSelect.isMultiSelecting || !(e.metaKey || e.ctrlKey)) return
|
||||
if (e.key === 'e') { e.preventDefault(); e.stopPropagation(); onArchive() }
|
||||
if (e.key === 'Backspace' || e.key === 'Delete') { e.preventDefault(); e.stopPropagation(); onTrash() }
|
||||
if (e.key === 'Backspace' || e.key === 'Delete') { e.preventDefault(); e.stopPropagation(); onDelete() }
|
||||
}
|
||||
|
||||
export function useMultiSelectKeyboard(multiSelect: MultiSelectState, isEntityView: boolean, onBulkArchive: () => void, onBulkTrash: () => void) {
|
||||
export function useMultiSelectKeyboard(multiSelect: MultiSelectState, isEntityView: boolean, onBulkArchive: () => void, onBulkDelete: () => void) {
|
||||
useEffect(() => {
|
||||
function handleKeyDown(e: KeyboardEvent) {
|
||||
handleEscapeKey(e, multiSelect)
|
||||
handleSelectAllKey(e, multiSelect, isEntityView)
|
||||
handleBulkActionKey(e, multiSelect, onBulkArchive, onBulkTrash)
|
||||
handleBulkActionKey(e, multiSelect, onBulkArchive, onBulkDelete)
|
||||
}
|
||||
window.addEventListener('keydown', handleKeyDown, true)
|
||||
return () => window.removeEventListener('keydown', handleKeyDown, true)
|
||||
}, [multiSelect, isEntityView, onBulkArchive, onBulkTrash])
|
||||
}, [multiSelect, isEntityView, onBulkArchive, onBulkDelete])
|
||||
}
|
||||
|
||||
// --- useModifiedFilesState ---
|
||||
|
||||
@@ -6,7 +6,7 @@ function makeEntry(path = '/test.md'): VaultEntry {
|
||||
return {
|
||||
path, filename: 'test.md', title: 'Test', isA: null,
|
||||
aliases: [], belongsTo: [], relatedTo: [], status: null,
|
||||
archived: false, trashed: false, trashedAt: null,
|
||||
archived: false,
|
||||
modifiedAt: null, createdAt: null, fileSize: 0,
|
||||
snippet: '', wordCount: 0, relationships: {},
|
||||
icon: null, color: null, order: null, sidebarLabel: null,
|
||||
|
||||
@@ -24,11 +24,6 @@ export function filterGroupsByQuery(groups: RelationshipGroup[], query: string):
|
||||
return groups.map((g) => ({ ...g, entries: filterByQuery(g.entries, query) })).filter((g) => g.entries.length > 0)
|
||||
}
|
||||
|
||||
export function countExpiredTrash(entries: VaultEntry[]): number {
|
||||
const now = Date.now() / 1000
|
||||
return entries.filter((e) => e.trashedAt && (now - e.trashedAt) >= 86400 * 30).length
|
||||
}
|
||||
|
||||
export interface ClickActions {
|
||||
onReplace: (entry: VaultEntry) => void
|
||||
onSelect: (entry: VaultEntry) => void
|
||||
|
||||
Reference in New Issue
Block a user