Add trust features: history, trash, export, and collection sharing
release gate / check (push) Failing after 6m36s

Implement Notion-like upgrade phase 4: append-only page revisions with restore-as-a-new-version; recoverable bookmark/page/collection deletions whose restores never resurrect public flags or shares; owner-scoped JSON export; and explicit, revocable read-only collection sharing with owner-only mutations and no transitive third-party bookmark leaks. Add migration 009, API/docs, integration coverage, and page/collections/trash UI.
This commit is contained in:
Andrew S Erwin
2026-10-19 23:07:38 -05:00
parent 9db69efe4b
commit 5eab58f99c
22 changed files with 1496 additions and 76 deletions
+3 -3
View File
@@ -141,10 +141,10 @@ export function BookmarkDetailSheet({ bookmark, onClose, onUpdate }: Props) {
}, [bookmark, notes, title, url, description, tagIds, columnId, contactId, kanban, dueDate, alertAt, isPublic, imapEnabled, imapQuery, onUpdate])
const handleDelete = useCallback(async () => {
if (!bookmark || !confirm('Delete this bookmark?')) return
if (!bookmark || !confirm('Move this bookmark to the trash? It can be restored.')) return
const response = await fetch(`/api/bookmarks/${bookmark.id}`, { method: 'DELETE' })
if (!response.ok) { toast.error('Could not delete bookmark'); return }
toast.success('Bookmark deleted')
if (!response.ok) { toast.error('Could not move bookmark to trash'); return }
toast.success('Bookmark moved to trash')
onClose()
onUpdate()
}, [bookmark, onClose, onUpdate])
+2 -1
View File
@@ -26,6 +26,7 @@ const viewItems = [
{ href: '/kanban', label: 'Kanban', icon: Kanban },
{ href: '/collections', label: 'Collections', icon: Layers },
{ href: '/contacts', label: 'Contacts', icon: Users },
{ href: '/trash', label: 'Trash', icon: Trash2 },
]
// Section collapse state persists per browser. useSyncExternalStore keeps it
@@ -205,7 +206,7 @@ function SidebarNav({ open, pathname, isAdmin = false }: { open: boolean; pathna
}
const removePage = async (p: PageSummary) => {
if (!confirm(`Delete page "${p.title}"? Bookmarks are kept.`)) return
if (!confirm(`Move page "${p.title}" to the trash? Bookmarks are kept and it can be restored.`)) return
try {
const res = await fetch(`/api/pages/${p.id}`, { method: 'DELETE' })
if (!res.ok) { toast.error('Could not delete page'); return }