双向删除同步
This commit is contained in:
@@ -22,6 +22,7 @@ interface FileTreeProps {
|
||||
disableSelection?: boolean;
|
||||
onDropFiles?: (docId: string, files: FileList) => void;
|
||||
onAssetContextMenu?: (event: React.MouseEvent, asset: MediaAsset) => void;
|
||||
onAssetClick?: (asset: MediaAsset, event: React.MouseEvent) => void;
|
||||
}
|
||||
|
||||
const INDENT = 16;
|
||||
@@ -42,6 +43,7 @@ export function FileTree({
|
||||
disableSelection = false,
|
||||
onDropFiles,
|
||||
onAssetContextMenu,
|
||||
onAssetClick,
|
||||
}: FileTreeProps) {
|
||||
if (nodes.length === 0) {
|
||||
return <div className="px-4 py-6 text-sm text-gray-400">暂无页面,点击下方按钮创建</div>;
|
||||
@@ -83,6 +85,7 @@ export function FileTree({
|
||||
disableSelection={disableSelection}
|
||||
onDropFiles={onDropFiles}
|
||||
onAssetContextMenu={onAssetContextMenu}
|
||||
onAssetClick={onAssetClick}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -106,6 +109,7 @@ interface FileTreeNodeProps {
|
||||
disableSelection: boolean;
|
||||
onDropFiles?: (docId: string, files: FileList) => void;
|
||||
onAssetContextMenu?: (event: React.MouseEvent, asset: MediaAsset) => void;
|
||||
onAssetClick?: (asset: MediaAsset, event: React.MouseEvent) => void;
|
||||
}
|
||||
|
||||
function FileTreeNode({
|
||||
@@ -125,6 +129,7 @@ function FileTreeNode({
|
||||
disableSelection,
|
||||
onDropFiles,
|
||||
onAssetContextMenu,
|
||||
onAssetClick,
|
||||
}: FileTreeNodeProps) {
|
||||
const isExpanded = expanded.has(node.id);
|
||||
const assets = useMemo(() => assetsByDoc[node.id] ?? [], [assetsByDoc, node.id]);
|
||||
@@ -202,13 +207,23 @@ function FileTreeNode({
|
||||
onToggleAssetSelect ? () => onToggleAssetSelect(asset.id) : undefined
|
||||
}
|
||||
onSelectOnly={onSelectOnlyAsset ? () => onSelectOnlyAsset(asset.id) : undefined}
|
||||
onClick={() => onOpenAsset(asset)}
|
||||
onClick={(e) => {
|
||||
if (onAssetClick) {
|
||||
onAssetClick(asset, e);
|
||||
} else {
|
||||
onOpenAsset(asset);
|
||||
}
|
||||
}}
|
||||
onDoubleClick={() => onOpenAsset(asset)}
|
||||
stopBubble
|
||||
onContextMenu={
|
||||
onAssetContextMenu
|
||||
? (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (onAssetClick) {
|
||||
onAssetClick(asset, e);
|
||||
}
|
||||
onAssetContextMenu(e, asset);
|
||||
}
|
||||
: undefined
|
||||
@@ -264,17 +279,23 @@ function FileLeafRow({
|
||||
onSelectOnly?: () => void;
|
||||
stopBubble?: boolean;
|
||||
onContextMenu?: (event: React.MouseEvent) => void;
|
||||
onDoubleClick?: () => void;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className="flex items-center gap-2 rounded-md px-2 py-1 text-sm text-gray-600 hover:bg-[#f8fafc]"
|
||||
className={cn(
|
||||
"flex items-center gap-2 rounded-md px-2 py-1 text-sm text-gray-600 hover:bg-[#f8fafc]",
|
||||
selected && "bg-[#e8f2ff] text-[#2563eb]",
|
||||
)}
|
||||
style={{ paddingLeft: depth * INDENT + 32 }}
|
||||
onClick={(e) => {
|
||||
if (stopBubble) e.stopPropagation();
|
||||
// 附件行默认只负责选中,不直接打开,避免误触下载
|
||||
if (selectable) return;
|
||||
onClick();
|
||||
}}
|
||||
onDoubleClick={(e) => {
|
||||
if (stopBubble) e.stopPropagation();
|
||||
onDoubleClick?.();
|
||||
}}
|
||||
onMouseDown={(e) => {
|
||||
if (stopBubble) e.stopPropagation();
|
||||
}}
|
||||
|
||||
@@ -44,6 +44,8 @@ import { supabaseBrowser } from "@/lib/supabase/client";
|
||||
import { useSearchPaletteStore } from "@/store/search-palette";
|
||||
import { FileTree } from "@/components/sidebar/file-tree";
|
||||
import type { MediaAsset } from "@/types/media";
|
||||
import { AssetContextMenu } from "@/components/sidebar/asset-context-menu";
|
||||
import { ASSETS_CHANGED_EVENT, DOCUMENTS_CHANGED_EVENT, emitAssetsChanged, emitDocumentsChanged } from "@/lib/events";
|
||||
|
||||
const TOP_BUTTONS = [
|
||||
{ id: "search", icon: SearchIcon, label: "搜索" },
|
||||
@@ -93,6 +95,13 @@ export function Sidebar({ initialData }: SidebarProps) {
|
||||
const [trashOpen, setTrashOpen] = useState(false);
|
||||
const [trashSearch, setTrashSearch] = useState("");
|
||||
const [emptyingTrash, setEmptyingTrash] = useState(false);
|
||||
const [mediaAssets, setMediaAssets] = useState<MediaAsset[]>(sidebarData.mediaAssets ?? []);
|
||||
const [mindmapDocs, setMindmapDocs] = useState<string[]>(sidebarData.mindmapDocs ?? []);
|
||||
const [assetMenu, setAssetMenu] = useState<{ asset: MediaAsset; x: number; y: number } | null>(
|
||||
null,
|
||||
);
|
||||
const [selectedAssetIds, setSelectedAssetIds] = useState<Set<string>>(new Set());
|
||||
const [lastSelectedAssetId, setLastSelectedAssetId] = useState<string | null>(null);
|
||||
|
||||
const workspaceMenuRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
@@ -104,10 +113,43 @@ export function Sidebar({ initialData }: SidebarProps) {
|
||||
});
|
||||
}, [sidebarData.documents]);
|
||||
|
||||
useEffect(() => {
|
||||
setMediaAssets(sidebarData.mediaAssets ?? []);
|
||||
}, [sidebarData.mediaAssets]);
|
||||
|
||||
useEffect(() => {
|
||||
setMindmapDocs(sidebarData.mindmapDocs ?? []);
|
||||
}, [sidebarData.mindmapDocs]);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedAssetIds(new Set());
|
||||
setLastSelectedAssetId(null);
|
||||
}, [mediaAssets, sidebarData.documents]);
|
||||
|
||||
useEffect(() => {
|
||||
setOpen(false);
|
||||
}, [activeId, setOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (event: Event) => {
|
||||
const custom = event as CustomEvent<{ docId?: string; asset?: MediaAsset }>;
|
||||
const asset = custom.detail?.asset as MediaAsset | undefined;
|
||||
if (asset?.id) {
|
||||
setMediaAssets((prev) => {
|
||||
if (prev.find((item) => item.id === asset.id)) return prev;
|
||||
return [asset, ...prev];
|
||||
});
|
||||
}
|
||||
void sidebarQuery.refetch();
|
||||
};
|
||||
window.addEventListener(ASSETS_CHANGED_EVENT, handler);
|
||||
window.addEventListener(DOCUMENTS_CHANGED_EVENT, handler);
|
||||
return () => {
|
||||
window.removeEventListener(ASSETS_CHANGED_EVENT, handler);
|
||||
window.removeEventListener(DOCUMENTS_CHANGED_EVENT, handler);
|
||||
};
|
||||
}, [sidebarQuery]);
|
||||
|
||||
const refreshTree = useCallback(async () => {
|
||||
await sidebarQuery.refetch();
|
||||
}, [sidebarQuery]);
|
||||
@@ -128,6 +170,27 @@ export function Sidebar({ initialData }: SidebarProps) {
|
||||
};
|
||||
}, [refreshTree]);
|
||||
|
||||
useEffect(() => {
|
||||
const channel = supabaseBrowser
|
||||
.channel("media-assets-feed")
|
||||
.on(
|
||||
"postgres_changes",
|
||||
{
|
||||
event: "*",
|
||||
schema: "public",
|
||||
table: "media_assets",
|
||||
filter: `workspace_id=eq.${sidebarData.activeWorkspaceId}`,
|
||||
},
|
||||
() => {
|
||||
void sidebarQuery.refetch();
|
||||
},
|
||||
)
|
||||
.subscribe();
|
||||
return () => {
|
||||
supabaseBrowser.removeChannel(channel);
|
||||
};
|
||||
}, [sidebarData.activeWorkspaceId, sidebarQuery]);
|
||||
|
||||
const sections = useMemo(() => buildSidebarSectionsFromTree(tree), [tree]);
|
||||
const starredNodes = useMemo(() => sections.find((section) => section.id === "starred")?.nodes ?? [], [sections]);
|
||||
const publicNodes = useMemo(() => sections.find((section) => section.id === "public")?.nodes ?? [], [sections]);
|
||||
@@ -154,15 +217,67 @@ export function Sidebar({ initialData }: SidebarProps) {
|
||||
|
||||
const assetsByDoc = useMemo(() => {
|
||||
const map: Record<string, MediaAsset[]> = {};
|
||||
const assets = sidebarData.mediaAssets ?? [];
|
||||
const assets = mediaAssets;
|
||||
|
||||
assets.forEach((asset) => {
|
||||
if (!map[asset.document_id]) {
|
||||
map[asset.document_id] = [];
|
||||
}
|
||||
map[asset.document_id].push(asset);
|
||||
});
|
||||
|
||||
if (mindmapDocs.length > 0) {
|
||||
mindmapDocs.forEach((docId) => {
|
||||
const doc = sidebarData.documents.find((d) => d.id === docId);
|
||||
const workspaceId = doc?.workspace_id ?? "";
|
||||
if (!map[docId]) {
|
||||
map[docId] = [];
|
||||
}
|
||||
// 避免重复插入
|
||||
const alreadyExists = map[docId].some(
|
||||
(asset) => asset.asset_type === "mindmap",
|
||||
);
|
||||
if (!alreadyExists) {
|
||||
map[docId].push({
|
||||
id: `mindmap-${docId}`,
|
||||
workspace_id: workspaceId,
|
||||
document_id: docId,
|
||||
asset_type: "mindmap",
|
||||
file_url: `/documents/${docId}`,
|
||||
thumbnail_url: null,
|
||||
bucket: null,
|
||||
storage_path: null,
|
||||
file_name: "mindmap.json",
|
||||
file_size: null,
|
||||
mime_type: "application/json",
|
||||
ocr_payload: undefined,
|
||||
ocr_strategy: null,
|
||||
ocr_text: null,
|
||||
ocr_status: null,
|
||||
signed_url: null,
|
||||
created_at: "",
|
||||
updated_at: "",
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return map;
|
||||
}, [sidebarData.mediaAssets]);
|
||||
}, [sidebarData.documents, mediaAssets, mindmapDocs]);
|
||||
|
||||
const assetOrder = useMemo(() => {
|
||||
const list: string[] = [];
|
||||
Object.values(assetsByDoc).forEach((arr) => {
|
||||
arr.forEach((asset) => list.push(asset.id));
|
||||
});
|
||||
return list;
|
||||
}, [assetsByDoc]);
|
||||
|
||||
const assetIndexMap = useMemo(() => {
|
||||
const map = new Map<string, number>();
|
||||
assetOrder.forEach((id, idx) => map.set(id, idx));
|
||||
return map;
|
||||
}, [assetOrder]);
|
||||
|
||||
const activeWorkspace =
|
||||
sidebarData.workspaces.find((workspace) => workspace.id === sidebarData.activeWorkspaceId) ??
|
||||
@@ -170,6 +285,8 @@ export function Sidebar({ initialData }: SidebarProps) {
|
||||
|
||||
const handleOpenDocument = useCallback(
|
||||
(documentId: string, mode: "main" | "sidebar") => {
|
||||
setSelectedAssetIds(new Set());
|
||||
setLastSelectedAssetId(null);
|
||||
const targetPath = `/documents/${documentId}`;
|
||||
if (mode === "main") {
|
||||
router.push(targetPath);
|
||||
@@ -248,6 +365,11 @@ export function Sidebar({ initialData }: SidebarProps) {
|
||||
}, []);
|
||||
|
||||
const handleOpenAsset = useCallback((asset: MediaAsset) => {
|
||||
if (asset.asset_type === "mindmap") {
|
||||
router.push(`/documents/${asset.document_id}`);
|
||||
setOpen(false);
|
||||
return;
|
||||
}
|
||||
const url = asset.signed_url ?? asset.file_url;
|
||||
if (!url) {
|
||||
window.alert("暂无可用的文件链接");
|
||||
@@ -256,8 +378,213 @@ export function Sidebar({ initialData }: SidebarProps) {
|
||||
if (typeof window !== "undefined") {
|
||||
window.open(url, "_blank", "noopener,noreferrer");
|
||||
}
|
||||
}, [router, setOpen]);
|
||||
|
||||
const handleAssetContextMenu = useCallback(
|
||||
(event: React.MouseEvent, asset: MediaAsset) => {
|
||||
event.preventDefault();
|
||||
if (!selectedAssetIds.has(asset.id)) {
|
||||
setSelectedAssetIds(new Set([asset.id]));
|
||||
setLastSelectedAssetId(asset.id);
|
||||
}
|
||||
setAssetMenu({ asset, x: event.clientX, y: event.clientY });
|
||||
},
|
||||
[selectedAssetIds],
|
||||
);
|
||||
|
||||
const handleSelectAsset = useCallback(
|
||||
(asset: MediaAsset, event?: { type?: string; shiftKey?: boolean; metaKey?: boolean; ctrlKey?: boolean }) => {
|
||||
const evtType = event?.type ?? "";
|
||||
if (evtType === "contextmenu" && selectedAssetIds.has(asset.id)) {
|
||||
setLastSelectedAssetId(asset.id);
|
||||
return;
|
||||
}
|
||||
setSelectedAssetIds((prev) => {
|
||||
let next = new Set(prev);
|
||||
const withShift = Boolean(event?.shiftKey) && lastSelectedAssetId && assetIndexMap.has(lastSelectedAssetId);
|
||||
if (withShift) {
|
||||
const start = assetIndexMap.get(lastSelectedAssetId!) ?? 0;
|
||||
const end = assetIndexMap.get(asset.id) ?? start;
|
||||
const [lo, hi] = start < end ? [start, end] : [end, start];
|
||||
const idsInRange = assetOrder.slice(lo, hi + 1);
|
||||
next = new Set([...prev, ...idsInRange]);
|
||||
} else if (event?.metaKey || event?.ctrlKey) {
|
||||
if (next.has(asset.id)) {
|
||||
next.delete(asset.id);
|
||||
} else {
|
||||
next.add(asset.id);
|
||||
}
|
||||
} else {
|
||||
next = new Set([asset.id]);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
setLastSelectedAssetId(asset.id);
|
||||
},
|
||||
[assetIndexMap, assetOrder, lastSelectedAssetId, selectedAssetIds],
|
||||
);
|
||||
|
||||
const toggleAssetCheckbox = useCallback((assetId: string) => {
|
||||
setSelectedAssetIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(assetId)) {
|
||||
next.delete(assetId);
|
||||
} else {
|
||||
next.add(assetId);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
setLastSelectedAssetId(assetId);
|
||||
}, []);
|
||||
|
||||
const selectOnlyAsset = useCallback((assetId: string) => {
|
||||
setSelectedAssetIds(new Set([assetId]));
|
||||
setLastSelectedAssetId(assetId);
|
||||
}, []);
|
||||
|
||||
const handleCopyAssetLink = useCallback(
|
||||
async (asset: MediaAsset) => {
|
||||
if (asset.asset_type === "mindmap") {
|
||||
await copyText(buildDocumentUrl(asset.document_id), "页面链接已复制");
|
||||
return;
|
||||
}
|
||||
const url = asset.signed_url ?? asset.file_url ?? "";
|
||||
if (!url) {
|
||||
window.alert("暂无可用的文件链接");
|
||||
return;
|
||||
}
|
||||
await copyText(url, "附件链接已复制");
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const handleCopyAssetPath = useCallback(async (asset: MediaAsset) => {
|
||||
const path =
|
||||
asset.asset_type === "mindmap"
|
||||
? `documents/${asset.document_id}/mindmap.json`
|
||||
: asset.storage_path || asset.file_url || asset.file_name || "附件";
|
||||
await copyText(path, "存储路径已复制");
|
||||
}, []);
|
||||
|
||||
const handleDownloadAsset = useCallback(async (asset: MediaAsset) => {
|
||||
if (asset.asset_type === "mindmap") {
|
||||
const resp = await fetch(`/api/mindmap/${asset.document_id}`);
|
||||
if (!resp.ok) {
|
||||
window.alert("下载失败");
|
||||
return;
|
||||
}
|
||||
const payload = await resp.json().catch(() => null);
|
||||
const data = payload?.data ?? {};
|
||||
const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = "mindmap.json";
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
return;
|
||||
}
|
||||
const url = asset.signed_url ?? asset.file_url;
|
||||
if (!url) {
|
||||
window.alert("暂无可用的下载链接");
|
||||
return;
|
||||
}
|
||||
if (typeof window !== "undefined") {
|
||||
window.open(url, "_blank", "noopener,noreferrer");
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleRenameAsset = useCallback(
|
||||
async (asset: MediaAsset) => {
|
||||
if (asset.asset_type === "mindmap") {
|
||||
window.alert("思维导图暂不支持重命名");
|
||||
return;
|
||||
}
|
||||
const input = window.prompt("输入新文件名", asset.file_name ?? "");
|
||||
if (!input || !input.trim()) return;
|
||||
const newName = input.trim();
|
||||
const resp = await fetch("/api/media/batch", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ action: "rename", assetIds: [asset.id], newName }),
|
||||
});
|
||||
if (!resp.ok) {
|
||||
const payload = await resp.json().catch(() => ({}));
|
||||
window.alert(payload?.error ?? "重命名失败");
|
||||
return;
|
||||
}
|
||||
await sidebarQuery.refetch();
|
||||
setAssetMenu(null);
|
||||
emitAssetsChanged(asset.document_id);
|
||||
},
|
||||
[sidebarQuery],
|
||||
);
|
||||
|
||||
const handleMoveAsset = useCallback(
|
||||
async (asset: MediaAsset) => {
|
||||
if (asset.asset_type === "mindmap") {
|
||||
window.alert("思维导图文件无需移动,请在页面中直接编辑");
|
||||
return;
|
||||
}
|
||||
const target = window.prompt("输入目标页面 ID", asset.document_id);
|
||||
if (!target || !target.trim()) return;
|
||||
const resp = await fetch("/api/media/batch", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
action: "move",
|
||||
assetIds: [asset.id],
|
||||
targetDocumentId: target.trim(),
|
||||
}),
|
||||
});
|
||||
if (!resp.ok) {
|
||||
const payload = await resp.json().catch(() => ({}));
|
||||
window.alert(payload?.error ?? "移动失败");
|
||||
return;
|
||||
}
|
||||
await sidebarQuery.refetch();
|
||||
setAssetMenu(null);
|
||||
emitAssetsChanged(asset.document_id);
|
||||
emitAssetsChanged(target.trim());
|
||||
},
|
||||
[sidebarQuery],
|
||||
);
|
||||
|
||||
const handleDeleteAssets = useCallback(
|
||||
async (assetIds: string[], assetHint?: MediaAsset) => {
|
||||
const target =
|
||||
assetHint ??
|
||||
mediaAssets.find((item) => assetIds.includes(item.id)) ??
|
||||
null;
|
||||
if (target?.asset_type === "mindmap") {
|
||||
const resp = await fetch(`/api/mindmap/${target.document_id}`, { method: "DELETE" });
|
||||
if (!resp.ok) {
|
||||
const payload = await resp.json().catch(() => ({}));
|
||||
window.alert(payload?.error ?? "删除思维导图失败");
|
||||
return;
|
||||
}
|
||||
setMindmapDocs((prev) => prev.filter((id) => id !== target.document_id));
|
||||
setAssetMenu(null);
|
||||
emitAssetsChanged(target.document_id, undefined, undefined, true);
|
||||
return;
|
||||
}
|
||||
const resp = await fetch("/api/media/batch", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ action: "delete", assetIds }),
|
||||
});
|
||||
if (!resp.ok) {
|
||||
const payload = await resp.json().catch(() => ({}));
|
||||
window.alert(payload?.error ?? "删除失败");
|
||||
return;
|
||||
}
|
||||
setMediaAssets((prev) => prev.filter((item) => !assetIds.includes(item.id)));
|
||||
setAssetMenu(null);
|
||||
emitAssetsChanged(target?.document_id, undefined, assetIds);
|
||||
},
|
||||
[mediaAssets, sidebarQuery],
|
||||
);
|
||||
|
||||
const handleResizeStart = useCallback(
|
||||
(event: React.MouseEvent) => {
|
||||
event.preventDefault();
|
||||
@@ -389,8 +716,12 @@ export function Sidebar({ initialData }: SidebarProps) {
|
||||
body: JSON.stringify({ documentId }),
|
||||
});
|
||||
await refreshTree();
|
||||
emitDocumentsChanged(documentId);
|
||||
if (activeId === documentId) {
|
||||
router.push("/");
|
||||
}
|
||||
},
|
||||
[refreshTree],
|
||||
[activeId, refreshTree, router],
|
||||
);
|
||||
|
||||
const handleConvertToChild = useCallback(
|
||||
@@ -713,6 +1044,11 @@ export function Sidebar({ initialData }: SidebarProps) {
|
||||
onOpenAsset={handleOpenAsset}
|
||||
onCreateChild={handleCreate}
|
||||
onContextMenu={openContextMenu}
|
||||
onAssetContextMenu={handleAssetContextMenu}
|
||||
selectedAssetIds={selectedAssetIds}
|
||||
onAssetClick={(asset, e) => handleSelectAsset(asset, e)}
|
||||
onToggleAssetSelect={toggleAssetCheckbox}
|
||||
onSelectOnlyAsset={selectOnlyAsset}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -772,6 +1108,25 @@ export function Sidebar({ initialData }: SidebarProps) {
|
||||
onDelete={() => void handleDelete(contextMenu.node.id)}
|
||||
/>
|
||||
)}
|
||||
{assetMenu && (
|
||||
<AssetContextMenu
|
||||
asset={assetMenu.asset}
|
||||
position={{ x: assetMenu.x, y: assetMenu.y }}
|
||||
onClose={() => setAssetMenu(null)}
|
||||
onOpen={handleOpenAsset}
|
||||
onCopyLink={handleCopyAssetLink}
|
||||
onCopyPath={handleCopyAssetPath}
|
||||
onRename={handleRenameAsset}
|
||||
onMove={handleMoveAsset}
|
||||
onDelete={(ids) =>
|
||||
void handleDeleteAssets(
|
||||
selectedAssetIds.size > 0 ? Array.from(selectedAssetIds) : ids,
|
||||
assetMenu.asset,
|
||||
)
|
||||
}
|
||||
onDownload={handleDownloadAsset}
|
||||
/>
|
||||
)}
|
||||
<Drawer open={trashOpen} onOpenChange={setTrashOpen}>
|
||||
<DrawerContent className="max-h-[90vh]">
|
||||
<DrawerHeader className="text-left">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { DocumentRecord } from "@/lib/documents";
|
||||
import type { WorkspaceSummary } from "@/lib/workspaces";
|
||||
import type { MediaAsset } from "@/types/media";
|
||||
|
||||
export type SidebarSectionId = "starred" | "public" | "shared" | "private" | "templates";
|
||||
|
||||
@@ -16,4 +17,12 @@ export interface SidebarInitialData {
|
||||
workspaces: WorkspaceSummary[];
|
||||
documents: DocumentRecord[];
|
||||
trashedDocuments: TrashRecord[];
|
||||
/**
|
||||
* 已存在思维导图文件(本地或 supabase)对应的页面 id 列表
|
||||
*/
|
||||
mindmapDocs?: string[];
|
||||
/**
|
||||
* 可选的媒体资源列表(旧字段向后兼容)
|
||||
*/
|
||||
mediaAssets?: MediaAsset[];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user