feat(workspace): trash for notes
Deleting a note (or its folder) moves it to the trash instead of erasing it. Trashed notes leave the tree, search, export and stats and are hidden from everyone they were shared with; the owner sees them read-only with a restore banner. New endpoints list, restore, purge and empty the trash; restoring recreates a deleted folder. Sidebar entry and mobile menu item with counter, list view with two-tap permanent delete. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
344f5e4ab2
commit
f69f74fdc7
15 files changed
+649
-40
No files matched your search
@@ -240,6 +240,9 @@ export const HelpView: React.FC<HelpViewProps> = ({ onBack }) => {
|
||||
<li>
|
||||
<strong className="text-main">Ansicht „Tags & Filter“:</strong> Im oberen Workspace-Reiter findest du die Tag-Übersicht. Hier siehst du alle verwendeten Hashtags, kannst Fundstellen aus allen Dokumenten gebündelt durchsuchen und sogar eigene freie Notizen auf Tag-Seiten hinterlegen.
|
||||
</li>
|
||||
<li>
|
||||
<strong className="text-main">Papierkorb:</strong> Gelöschte Notizen (auch aus gelöschten Ordnern) landen im „Papierkorb“ unten in der Ordnerleiste bzw. im mobilen Menü. Dort kannst du sie schreibgeschützt ansehen, wiederherstellen oder endgültig löschen. Solange eine Notiz im Papierkorb liegt, sehen sie auch die Personen nicht, mit denen sie geteilt war.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
),
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
Pin,
|
||||
Users,
|
||||
Trash2,
|
||||
RotateCcw,
|
||||
Download,
|
||||
FileText,
|
||||
Loader2,
|
||||
@@ -36,6 +37,7 @@ export interface DocumentEditorProps {
|
||||
initialFocusedBlockId?: string | null;
|
||||
onDocMetadataChange: (updated: Document) => void;
|
||||
onDeleteDoc: (docId: string) => void;
|
||||
onRestoreDoc?: (docId: string) => void;
|
||||
onOpenShareModal: (doc: Document) => void;
|
||||
onSelectTag: (tag: string) => void;
|
||||
onWikiLinkClick?: (pageTitle: string) => void;
|
||||
@@ -179,6 +181,7 @@ export const DocumentEditor: React.FC<DocumentEditorProps> = ({
|
||||
initialFocusedBlockId,
|
||||
onDocMetadataChange,
|
||||
onDeleteDoc,
|
||||
onRestoreDoc,
|
||||
onOpenShareModal,
|
||||
onSelectTag,
|
||||
onWikiLinkClick,
|
||||
@@ -244,7 +247,9 @@ export const DocumentEditor: React.FC<DocumentEditorProps> = ({
|
||||
}, [title, doc?.title, blocks, zoomedBlockId]);
|
||||
|
||||
// Owner and editors may edit; the role comes from the server via the sync engine
|
||||
const inTrash = Boolean(doc?.isArchived);
|
||||
const canEdit = useMemo(() => {
|
||||
if (doc?.isArchived) return false;
|
||||
if (engineState?.role) return engineState.role === 'owner' || engineState.role === 'editor';
|
||||
if (!doc) return false;
|
||||
return Boolean(doc.isOwner || doc.permission === 'owner' || doc.permission === 'write');
|
||||
@@ -770,7 +775,7 @@ export const DocumentEditor: React.FC<DocumentEditorProps> = ({
|
||||
{/* Who else has this note open */}
|
||||
<PresenceAvatars people={otherPeople} avatars={avatarsById} />
|
||||
{/* Sharing Section */}
|
||||
{doc && (() => {
|
||||
{doc && !inTrash && (() => {
|
||||
const isOwner = doc.isOwner !== false && (doc.userId === currentUser?.id || !doc.userId || currentUser?.role === 'admin');
|
||||
const sharesCount = doc.sharedWith?.length || 0;
|
||||
const isShared = sharesCount > 0 || !isOwner || Boolean(doc.sharedFolderId);
|
||||
@@ -814,7 +819,7 @@ export const DocumentEditor: React.FC<DocumentEditorProps> = ({
|
||||
);
|
||||
})()}
|
||||
|
||||
<button
|
||||
{!inTrash && <button
|
||||
type="button"
|
||||
onClick={handleTogglePin}
|
||||
className={`btn-icon ${
|
||||
@@ -825,7 +830,7 @@ export const DocumentEditor: React.FC<DocumentEditorProps> = ({
|
||||
title={doc?.isPinned ? 'Anheftung lösen' : 'Notiz anheften'}
|
||||
>
|
||||
<Pin className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</button>}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
@@ -849,8 +854,26 @@ export const DocumentEditor: React.FC<DocumentEditorProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* In the trash: read-only with a way back */}
|
||||
{inTrash && (
|
||||
<div className="flex flex-wrap items-center gap-2 px-4 sm:px-6 py-2 bg-status-err/10 border-b border-status-err/25 text-status-err text-xs">
|
||||
<Trash2 className="w-3.5 h-3.5 shrink-0" />
|
||||
<span className="flex-1 min-w-0">Diese Notiz liegt im Papierkorb und ist schreibgeschützt.</span>
|
||||
{onRestoreDoc && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onRestoreDoc(docId)}
|
||||
className="btn-action h-11 sm:h-7 px-3 text-xs text-main inline-flex items-center gap-1.5"
|
||||
>
|
||||
<RotateCcw className="w-3.5 h-3.5 text-accent" />
|
||||
Wiederherstellen
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Read-only notification banner */}
|
||||
{!canEdit && (
|
||||
{!canEdit && !inTrash && (
|
||||
<div className="flex items-center gap-2 px-4 sm:px-6 py-2 bg-status-warn/10 border-b border-status-warn/25 text-status-warn text-xs">
|
||||
<Lock className="w-3.5 h-3.5 shrink-0" />
|
||||
<span>Dieses Dokument wurde schreibgeschützt mit dir geteilt (Nur Lesen).</span>
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
import React, { useState } from 'react';
|
||||
import { ArrowLeft, Folder, RotateCcw, Trash2 } from 'lucide-react';
|
||||
import { TrashedDocument } from '../../lib/types';
|
||||
import { CountBadge } from '../../components/CountBadge';
|
||||
|
||||
interface TrashViewProps {
|
||||
items: TrashedDocument[];
|
||||
onOpen: (docId: string) => void;
|
||||
onRestore: (docId: string) => Promise<void>;
|
||||
onPurge: (docId: string) => Promise<void>;
|
||||
onEmpty: () => Promise<void>;
|
||||
onBackMobile: () => void;
|
||||
}
|
||||
|
||||
const formatDeletedAt = (iso: string) => {
|
||||
const d = new Date(iso);
|
||||
if (Number.isNaN(d.getTime())) return '';
|
||||
return d.toLocaleString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' });
|
||||
};
|
||||
|
||||
// Deleted notes: open read-only, restore, or delete for good. Deleting for good always takes a second
|
||||
// tap on the same button ("Wirklich löschen?"), which works the same with mouse and touch.
|
||||
export const TrashView: React.FC<TrashViewProps> = ({ items, onOpen, onRestore, onPurge, onEmpty, onBackMobile }) => {
|
||||
const [confirmId, setConfirmId] = useState<string | null>(null);
|
||||
const [busyId, setBusyId] = useState<string | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const run = async (id: string, action: () => Promise<void>) => {
|
||||
setBusyId(id);
|
||||
setError(null);
|
||||
try {
|
||||
await action();
|
||||
setConfirmId(null);
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Aktion fehlgeschlagen');
|
||||
} finally {
|
||||
setBusyId(null);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex-1 overflow-y-auto p-4 sm:p-6 lg:p-8 bg-canvas">
|
||||
<div className="w-full max-w-3xl space-y-4">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onBackMobile}
|
||||
className="lg:hidden btn-action h-11 w-11 flex items-center justify-center"
|
||||
aria-label="Zurück zu den Notizen"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
</button>
|
||||
<Trash2 className="w-5 h-5 text-accent" />
|
||||
<h1 className="text-xl font-bold text-main">Papierkorb</h1>
|
||||
<CountBadge count={items.length} />
|
||||
{items.length > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
disabled={busyId !== null}
|
||||
onClick={() =>
|
||||
confirmId === '__all__' ? run('__all__', onEmpty) : setConfirmId('__all__')
|
||||
}
|
||||
className={`ml-auto h-11 sm:h-8 px-3 text-xs font-semibold rounded-btn border transition-colors disabled:opacity-50 ${
|
||||
confirmId === '__all__'
|
||||
? 'bg-status-err/20 text-status-err border-status-err'
|
||||
: 'btn-action text-status-err border-status-err/40'
|
||||
}`}
|
||||
>
|
||||
{confirmId === '__all__' ? `Wirklich ${items.length} endgültig löschen?` : 'Papierkorb leeren'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-muted">
|
||||
Gelöschte Notizen bleiben hier, bis du sie wiederherstellst oder endgültig löschst. Für alle, mit denen sie geteilt
|
||||
waren, sind sie bis dahin ausgeblendet.
|
||||
</p>
|
||||
{error && <p className="text-xs text-status-err">{error}</p>}
|
||||
|
||||
{items.length === 0 ? (
|
||||
<div className="p-8 text-center text-sm text-faint border border-dashed border-border rounded-card">
|
||||
Der Papierkorb ist leer.
|
||||
</div>
|
||||
) : (
|
||||
<ul className="space-y-2">
|
||||
{items.map((t) => (
|
||||
<li
|
||||
key={t.id}
|
||||
className="p-3 bg-panel border border-border rounded-card flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-3"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onOpen(t.id)}
|
||||
className="flex items-center gap-2.5 flex-1 min-w-0 text-left cursor-pointer group"
|
||||
title="Schreibgeschützt ansehen"
|
||||
>
|
||||
<span className="text-lg shrink-0">{t.icon || '📝'}</span>
|
||||
<span className="min-w-0">
|
||||
<span className="block text-sm font-semibold text-main truncate group-hover:text-accent">
|
||||
{t.title || 'Unbenannte Notiz'}
|
||||
</span>
|
||||
<span className="flex flex-wrap items-center gap-x-2 text-[11px] text-muted">
|
||||
<span className="inline-flex items-center gap-1 min-w-0">
|
||||
<Folder className="w-3 h-3 shrink-0" />
|
||||
<span className="truncate">{t.folderPath || 'Hauptebene'}</span>
|
||||
</span>
|
||||
<span>gelöscht {formatDeletedAt(t.deletedAt)}</span>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
<div className="grid grid-cols-2 sm:flex gap-2 shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
disabled={busyId !== null}
|
||||
onClick={() => run(t.id, () => onRestore(t.id))}
|
||||
className="btn-action h-11 sm:h-8 px-3 text-xs inline-flex items-center justify-center gap-1.5 disabled:opacity-50"
|
||||
>
|
||||
<RotateCcw className="w-3.5 h-3.5 text-accent" />
|
||||
Wiederherstellen
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={busyId !== null}
|
||||
onClick={() => (confirmId === t.id ? run(t.id, () => onPurge(t.id)) : setConfirmId(t.id))}
|
||||
className={`h-11 sm:h-8 px-3 text-xs rounded-btn border inline-flex items-center justify-center gap-1.5 transition-colors disabled:opacity-50 ${
|
||||
confirmId === t.id
|
||||
? 'bg-status-err/20 text-status-err border-status-err font-semibold'
|
||||
: 'btn-action text-status-err border-status-err/40'
|
||||
}`}
|
||||
>
|
||||
<Trash2 className="w-3.5 h-3.5" />
|
||||
{confirmId === t.id ? 'Wirklich löschen?' : 'Endgültig löschen'}
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
TagDetails,
|
||||
TagSummary,
|
||||
Team,
|
||||
TrashedDocument,
|
||||
UnifiedShare,
|
||||
User,
|
||||
} from '../../lib/types';
|
||||
@@ -53,6 +54,8 @@ import { FormSheet } from '../../components/FormSheet';
|
||||
import { sendDocOps } from '../../lib/docSync/oneoff';
|
||||
import { useMobileActions } from '../../context/MobileActionContext';
|
||||
import { DocumentEditor, DocSaveStatus } from './DocumentEditor';
|
||||
import { TrashView } from './TrashView';
|
||||
import { CountBadge } from '../../components/CountBadge';
|
||||
import { extractHashtags } from '../../lib/hashtags';
|
||||
|
||||
interface WorkspaceViewProps {
|
||||
@@ -63,7 +66,7 @@ interface WorkspaceViewProps {
|
||||
onOpenTask?: (taskId: string) => void;
|
||||
}
|
||||
|
||||
type WorkspaceSubView = 'editor' | 'tag_detail' | 'todos' | 'tags';
|
||||
type WorkspaceSubView = 'editor' | 'tag_detail' | 'todos' | 'tags' | 'trash';
|
||||
|
||||
interface FolderTreeNode {
|
||||
name: string;
|
||||
@@ -307,6 +310,15 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
const [targetBlockId, setTargetBlockId] = useState<string | null>(initialBlockId || null);
|
||||
const [newlyCreatedDocId, setNewlyCreatedDocId] = useState<string | null>(null);
|
||||
const [subView, setSubView] = useState<WorkspaceSubView>('editor');
|
||||
const [trash, setTrash] = useState<TrashedDocument[]>([]);
|
||||
// Bumped after a restore so the open editor reloads the note without the trash banner
|
||||
const [editorNonce, setEditorNonce] = useState(0);
|
||||
const reloadTrash = useCallback(() => {
|
||||
api.getTrash().then(setTrash).catch(() => {});
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
reloadTrash();
|
||||
}, [reloadTrash]);
|
||||
const [selectedTag, setSelectedTag] = useState<string | null>(null);
|
||||
const [tagDetails, setTagDetails] = useState<TagDetails | null>(null);
|
||||
const [tagNoteDraft, setTagNoteDraft] = useState<string>('');
|
||||
@@ -466,14 +478,16 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
const delId = msg.payload.id as string;
|
||||
setDocs((prev) => prev.filter((d) => d.id !== delId));
|
||||
setSelectedDocId((prev) => (prev === delId ? null : prev));
|
||||
reloadTrash();
|
||||
} else if (msg.event === 'WORKSPACE_FOLDERS_UPDATED') {
|
||||
api.getFolders().then(setFolders).catch(() => {});
|
||||
api.getSharedFolders().then(setSharedFolders).catch(() => {});
|
||||
api.getDocuments().then(setDocs).catch(() => {});
|
||||
reloadTrash();
|
||||
}
|
||||
});
|
||||
return unsub;
|
||||
}, [currentUser]);
|
||||
}, [currentUser, reloadTrash]);
|
||||
|
||||
const handleCreateDocument = useCallback(
|
||||
async (folderPath = '') => {
|
||||
@@ -524,6 +538,12 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
setMobileShowDetail(true);
|
||||
}, []);
|
||||
|
||||
const openTrash = useCallback(() => {
|
||||
reloadTrash();
|
||||
setSubView('trash');
|
||||
setMobileShowDetail(true);
|
||||
}, [reloadTrash]);
|
||||
|
||||
// Register mobile FAB actions
|
||||
useEffect(() => {
|
||||
setPrimaryAction(() => handleCreateDocument(''));
|
||||
@@ -545,6 +565,13 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
icon: <FolderPlus className="w-4 h-4" />,
|
||||
onClick: () => setShowNewFolderModal(true),
|
||||
},
|
||||
{
|
||||
id: 'trash',
|
||||
label: 'Papierkorb',
|
||||
icon: <Trash2 className="w-4 h-4" />,
|
||||
badge: trash.length,
|
||||
onClick: openTrash,
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
@@ -552,7 +579,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
setGroups([]);
|
||||
setPrimaryAction(undefined);
|
||||
};
|
||||
}, [handleCreateDocument, setGroups, setPrimaryAction]);
|
||||
}, [handleCreateDocument, setGroups, setPrimaryAction, trash.length, openTrash]);
|
||||
|
||||
const handleDeleteDocument = useCallback(
|
||||
async (docId: string) => {
|
||||
@@ -567,13 +594,47 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
});
|
||||
setMobileShowDetail(false);
|
||||
onStatsChange();
|
||||
reloadTrash();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
},
|
||||
[onStatsChange, selectedDocId]
|
||||
[onStatsChange, selectedDocId, reloadTrash]
|
||||
);
|
||||
|
||||
const handleOpenTrashedDoc = (docId: string) => {
|
||||
setSelectedDocId(docId);
|
||||
setTargetBlockId(null);
|
||||
setNewlyCreatedDocId(null);
|
||||
setSubView('editor');
|
||||
setMobileShowDetail(true);
|
||||
};
|
||||
|
||||
const handleRestoreDocument = useCallback(
|
||||
async (docId: string) => {
|
||||
await api.restoreDocument(docId);
|
||||
const [freshDocs, freshFolders] = await Promise.all([api.getDocuments(), api.getFolders()]);
|
||||
setDocs(freshDocs);
|
||||
setFolders(freshFolders);
|
||||
setEditorNonce((n) => n + 1);
|
||||
reloadTrash();
|
||||
onStatsChange();
|
||||
},
|
||||
[reloadTrash, onStatsChange]
|
||||
);
|
||||
|
||||
const handlePurgeDocument = async (docId: string) => {
|
||||
await api.purgeDocument(docId);
|
||||
if (selectedDocId === docId) setSelectedDocId(null);
|
||||
reloadTrash();
|
||||
};
|
||||
|
||||
const handleEmptyTrash = async () => {
|
||||
await api.emptyTrash();
|
||||
if (selectedDocId && trash.some((t) => t.id === selectedDocId)) setSelectedDocId(null);
|
||||
reloadTrash();
|
||||
};
|
||||
|
||||
const handleDocMetadataChange = useCallback((updated: Document) => {
|
||||
setDocs((prev) => prev.map((d) => (d.id === updated.id ? { ...d, ...updated } : d)));
|
||||
}, []);
|
||||
@@ -615,6 +676,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
}
|
||||
}
|
||||
onStatsChange();
|
||||
reloadTrash();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
@@ -1257,7 +1319,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
{/* LEFT MASTER SIDEBAR */}
|
||||
<aside
|
||||
className={`${
|
||||
subView !== 'editor' || isSidebarCollapsed
|
||||
(subView !== 'editor' && subView !== 'trash') || isSidebarCollapsed
|
||||
? 'hidden'
|
||||
: mobileShowDetail
|
||||
? 'hidden lg:flex'
|
||||
@@ -1469,6 +1531,23 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Trash at the bottom of the tree */}
|
||||
<div className="p-2 border-t border-border shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
onClick={openTrash}
|
||||
className={`w-full flex items-center gap-2 px-2 h-11 sm:h-8 rounded-btn text-xs border transition-colors cursor-pointer ${
|
||||
subView === 'trash'
|
||||
? 'bg-panel-elevated text-main font-semibold border-border/80'
|
||||
: 'border-transparent text-muted hover:text-main hover:bg-panel-elevated'
|
||||
}`}
|
||||
>
|
||||
<Trash2 className="w-3.5 h-3.5" />
|
||||
<span className="flex-1 text-left">Papierkorb</span>
|
||||
<CountBadge count={trash.length} />
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* RIGHT DETAIL AREA (STRICTLY ISOLATED BY KEY) */}
|
||||
@@ -1477,9 +1556,21 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
subView !== 'editor' || mobileShowDetail || isSidebarCollapsed ? 'flex' : 'hidden lg:flex'
|
||||
} flex-1 flex-col min-w-0 h-full overflow-hidden bg-canvas`}
|
||||
>
|
||||
{subView === 'editor' && selectedDocId ? (
|
||||
{subView === 'trash' ? (
|
||||
<TrashView
|
||||
items={trash}
|
||||
onOpen={handleOpenTrashedDoc}
|
||||
onRestore={handleRestoreDocument}
|
||||
onPurge={handlePurgeDocument}
|
||||
onEmpty={handleEmptyTrash}
|
||||
onBackMobile={() => {
|
||||
setSubView('editor');
|
||||
setMobileShowDetail(false);
|
||||
}}
|
||||
/>
|
||||
) : subView === 'editor' && selectedDocId ? (
|
||||
<DocumentEditor
|
||||
key={selectedDocId}
|
||||
key={`${selectedDocId}:${editorNonce}`}
|
||||
docId={selectedDocId}
|
||||
currentUser={currentUser}
|
||||
initialDoc={selectedDocObj}
|
||||
@@ -1488,6 +1579,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
initialFocusedBlockId={targetBlockId}
|
||||
onDocMetadataChange={handleDocMetadataChange}
|
||||
onDeleteDoc={handleDeleteDocument}
|
||||
onRestoreDoc={handleRestoreDocument}
|
||||
onOpenShareModal={openDocShareModal}
|
||||
onSelectTag={handleSelectTag}
|
||||
onWikiLinkClick={handleWikiLinkClick}
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
AuthStatus,
|
||||
DashboardStats,
|
||||
Document,
|
||||
TrashedDocument,
|
||||
DocumentBlock,
|
||||
DocShare,
|
||||
KanbanColumn,
|
||||
@@ -647,10 +648,27 @@ export const api = {
|
||||
});
|
||||
},
|
||||
|
||||
/** Moves the note to the trash (owner) or leaves a shared note (others). */
|
||||
async deleteDocument(id: string): Promise<void> {
|
||||
await request(`/api/v1/documents/${id}`, { method: 'DELETE' });
|
||||
},
|
||||
|
||||
async getTrash(): Promise<TrashedDocument[]> {
|
||||
return request<TrashedDocument[]>('/api/v1/documents/trash');
|
||||
},
|
||||
|
||||
async restoreDocument(id: string): Promise<void> {
|
||||
await request(`/api/v1/documents/${id}/restore`, { method: 'POST' });
|
||||
},
|
||||
|
||||
async purgeDocument(id: string): Promise<void> {
|
||||
await request(`/api/v1/documents/${id}/permanent`, { method: 'DELETE' });
|
||||
},
|
||||
|
||||
async emptyTrash(): Promise<{ deleted: number }> {
|
||||
return request<{ deleted: number }>('/api/v1/documents/trash', { method: 'DELETE' });
|
||||
},
|
||||
|
||||
async moveDocument(id: string, folderPath: string): Promise<Document> {
|
||||
const d = await request<Document>(`/api/v1/documents/${id}/move`, {
|
||||
method: 'PUT',
|
||||
|
||||
@@ -232,6 +232,16 @@ export interface DocShare {
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
/** A note in the trash (GET /documents/trash). */
|
||||
export interface TrashedDocument {
|
||||
id: string;
|
||||
title: string;
|
||||
icon: string;
|
||||
folderPath: string;
|
||||
deletedAt: string;
|
||||
isOwner: boolean;
|
||||
}
|
||||
|
||||
export interface Document {
|
||||
id: string;
|
||||
userId: string;
|
||||
|
||||
Reference in new issue
Block a user