From 19b24113a4ab72408a67c87232e18552fdd7a70f Mon Sep 17 00:00:00 2001 From: tianma-if Date: Mon, 10 Aug 2026 01:26:38 +0800 Subject: [PATCH] fix: decouple attachment manager from selected note --- apps/web/src/components/AssetsPane.tsx | 177 +---------------------- apps/web/src/components/WorkspaceApp.tsx | 2 +- packages/shared/src/i18n/en-US.ts | 14 +- packages/shared/src/i18n/zh-CN.ts | 14 +- tests/e2e/assets-pane.spec.ts | 10 ++ 5 files changed, 17 insertions(+), 200 deletions(-) create mode 100644 tests/e2e/assets-pane.spec.ts diff --git a/apps/web/src/components/AssetsPane.tsx b/apps/web/src/components/AssetsPane.tsx index 188e0378..92e10275 100644 --- a/apps/web/src/components/AssetsPane.tsx +++ b/apps/web/src/components/AssetsPane.tsx @@ -1,7 +1,6 @@ -import { useState, useCallback, useMemo, useRef } from "react"; -import { useQuery, useQueryClient } from "@tanstack/react-query"; +import { useState, useMemo } from "react"; +import { useQuery } from "@tanstack/react-query"; import { useTranslation } from "react-i18next"; -import { useDropzone } from "react-dropzone"; import Lightbox from "yet-another-react-lightbox"; import Zoom from "yet-another-react-lightbox/plugins/zoom"; import Thumbnails from "yet-another-react-lightbox/plugins/thumbnails"; @@ -19,7 +18,6 @@ import { Search, Grid, List, - UploadCloud, FileText, FileSpreadsheet, FileArchive, @@ -27,15 +25,11 @@ import { Video, X, Loader2, - FileUp, } from "lucide-react"; import { Button } from "@/components/ui/button"; import { formatDateTime } from "@/lib/utils"; -import { compressImageForUpload } from "@/lib/image-compression"; -import { stageDesktopResource } from "@/lib/desktop-resources"; import { WORKSPACE_PAGE_TITLE_CLASSNAME } from "@/lib/workspace-ui"; -import type { MemoDetail } from "@edgeever/shared"; import type { EdgeEverRepository } from "@/lib/repository"; export const formatBytes = (bytes: number) => { @@ -116,14 +110,11 @@ const getFileIcon = (mimeType: string | null, filename: string | null) => { interface AssetsPaneProps { onClose: () => void; - activeMemo?: MemoDetail | null; repository: EdgeEverRepository; } -export const AssetsPane = ({ onClose, activeMemo, repository }: AssetsPaneProps) => { +export const AssetsPane = ({ onClose, repository }: AssetsPaneProps) => { const { t } = useTranslation(); - const queryClient = useQueryClient(); - const fileInputRef = useRef(null); // States const [searchQuery, setSearchQuery] = useState(""); @@ -132,8 +123,6 @@ export const AssetsPane = ({ onClose, activeMemo, repository }: AssetsPaneProps) return (localStorage.getItem("assets_layout_mode") as "grid" | "list") || "grid"; }); const [lightboxIndex, setLightboxIndex] = useState(null); - const [uploadState, setUploadState] = useState<"idle" | "compressing" | "uploading" | "error" | "success">("idle"); - const [uploadProgress, setUploadProgress] = useState(""); // Query resources const resourcesQuery = useQuery({ @@ -149,63 +138,6 @@ export const AssetsPane = ({ onClose, activeMemo, repository }: AssetsPaneProps) attachmentCount: 0, }; - // Drag and Drop Upload Handler - const handleUploadFiles = useCallback( - async (files: File[]) => { - if (!activeMemo || activeMemo.isDeleted || files.length === 0) return; - - const targetMemoId = activeMemo.id; - setUploadState("uploading"); - - try { - let count = 0; - for (const file of files) { - count++; - setUploadProgress(t("assets.uploadNth", { current: count, total: files.length })); - - const isImage = file.type.startsWith("image/"); - // Compress images if enabled - const shouldCompress = isImage; - if (shouldCompress) { - setUploadState("compressing"); - setUploadProgress(t("assets.compressingFile", { filename: file.name })); - } - - const uploadFile = shouldCompress ? (await compressImageForUpload(file)).file : file; - - setUploadState("uploading"); - setUploadProgress(t("assets.uploadingFile", { filename: uploadFile.name })); - - if (typeof navigator !== "undefined" && !navigator.onLine) { - await repository.uploadMemoResource(targetMemoId, uploadFile); - continue; - } - - try { - await repository.uploadMemoResource(targetMemoId, uploadFile); - } catch (error) { - const staged = await stageDesktopResource(targetMemoId, uploadFile); - if (!staged) throw error; - } - } - - void queryClient.invalidateQueries({ queryKey: ["resources"] }); - setUploadState("success"); - setTimeout(() => setUploadState("idle"), 2000); - } catch (err) { - setUploadState("error"); - setTimeout(() => setUploadState("idle"), 3000); - } - }, - [activeMemo, queryClient, repository, t] - ); - - const { getRootProps, getInputProps, isDragActive } = useDropzone({ - onDrop: handleUploadFiles, - disabled: !activeMemo || activeMemo.isDeleted, - noClick: true, - }); - // Filter Logic const filteredResources = useMemo(() => { return resources.filter((resource) => { @@ -248,29 +180,11 @@ export const AssetsPane = ({ onClose, activeMemo, repository }: AssetsPaneProps) } }; - const handleManualUploadClick = () => { - if (fileInputRef.current) { - fileInputRef.current.click(); - } - }; - - const handleManualFileChange = (e: React.ChangeEvent) => { - if (e.target.files && e.target.files.length > 0) { - handleUploadFiles(Array.from(e.target.files)); - } - }; - - const activeMemoTitle = activeMemo?.title || activeMemo?.excerpt || t("assets.unnamedMemo"); - const activeMemoShortTitle = activeMemo?.title || activeMemo?.excerpt || t("assets.unnamedShort"); const getResourceMemoSource = (resource: { memoDeleted: boolean; memoTitle: string | null; memoExcerpt: string | null; memoId: string }) => resource.memoDeleted ? t("assets.deletedMemo") : resource.memoTitle || resource.memoExcerpt || resource.memoId; return ( -
- +
{/* Header */}
@@ -369,45 +283,6 @@ export const AssetsPane = ({ onClose, activeMemo, repository }: AssetsPaneProps)
- {/* Target Upload Memo Info Banner */} - {activeMemo ? ( -
-

- {t("assets.activeMemo", { title: activeMemoTitle })} -

-
- - -
-
- ) : ( -
- {t("assets.noActiveMemoHint")} -
- )} - {/* Main Content Area */}
@@ -559,50 +434,6 @@ export const AssetsPane = ({ onClose, activeMemo, repository }: AssetsPaneProps)
- {/* Drag Active Overlay */} - {isDragActive && activeMemo && ( -
-
- -

{t("assets.dropTitle")}

-

- {t("assets.dropDescription")} - {activeMemoShortTitle} -

-
-
- )} - - {/* Uploading Status Overlay (Non-intrusive bottom loader) */} - {uploadState !== "idle" && ( -
- {uploadState === "compressing" && ( - <> - - {uploadProgress} - - )} - {uploadState === "uploading" && ( - <> - - {uploadProgress} - - )} - {uploadState === "success" && ( - <> - ✓ - {t("assets.uploadSuccess")} - - )} - {uploadState === "error" && ( - <> - ✕ - {t("assets.uploadError")} - - )} -
- )} - {/* Lightbox Viewer */} {lightboxIndex !== null && ( ) : rightView === "assets" ? ( - + ) : rightView === "tags" ? ( ) : rightView === "templates" ? ( diff --git a/packages/shared/src/i18n/en-US.ts b/packages/shared/src/i18n/en-US.ts index af5dc94f..1dd7eeb5 100644 --- a/packages/shared/src/i18n/en-US.ts +++ b/packages/shared/src/i18n/en-US.ts @@ -538,9 +538,6 @@ export const enUS = { permanentDelete: "Delete permanently", }, assets: { - uploadNth: "Uploading file {{current}}/{{total}}...", - compressingFile: "Compressing: {{filename}}", - uploadingFile: "Uploading: {{filename}}", title: "Attachment manager", fileCount: "{{count}} files", imageCount: "{{count}} images", @@ -553,27 +550,18 @@ export const enUS = { searchPlaceholder: "Search attachment names or source notes...", gridView: "Grid view", listView: "List view", - activeMemo: "Current note: “{{title}}”", unnamedMemo: "Untitled note", - unnamedShort: "Untitled", - uploadAttachment: "Upload attachment", - processing: "Processing...", - noActiveMemoHint: "Tip: open a note in the editor on the right to drag or upload new files here.", loading: "Loading attachments...", noMatches: "No matching attachments", empty: "No attachments yet", noMatchesDescription: "Try changing the search term or category filter", - emptyDescription: "Drag files here, or upload files above", + emptyDescription: "Images and attachments added to notes will appear here", previewImage: "Preview image", downloadOpen: "Download/open", deletedMemo: "Deleted note", fromMemo: "From: {{source}}", sourceMemo: "Source note: {{source}}", openInNewWindow: "Open in new window", - dropTitle: "Release files here", - dropDescription: "Files will be uploaded and attached to the note currently being edited:", - uploadSuccess: "Upload complete", - uploadError: "Upload failed. Please try again later", }, tagsDialog: { title: "Tags", diff --git a/packages/shared/src/i18n/zh-CN.ts b/packages/shared/src/i18n/zh-CN.ts index 7e3212e8..4bc2a6dc 100644 --- a/packages/shared/src/i18n/zh-CN.ts +++ b/packages/shared/src/i18n/zh-CN.ts @@ -538,9 +538,6 @@ export const zhCN = { permanentDelete: "永久删除", }, assets: { - uploadNth: "正在上传第 {{current}}/{{total}} 个文件...", - compressingFile: "正在压缩: {{filename}}", - uploadingFile: "正在上传: {{filename}}", title: "附件管理", fileCount: "{{count}} 文件", imageCount: "{{count}} 图片", @@ -553,27 +550,18 @@ export const zhCN = { searchPlaceholder: "搜索附件名或来源笔记...", gridView: "网格视图", listView: "列表视图", - activeMemo: "当前关联笔记:《{{title}}》", unnamedMemo: "未命名笔记", - unnamedShort: "未命名", - uploadAttachment: "上传附件", - processing: "处理中...", - noActiveMemoHint: "提示:在右侧编辑器中打开一篇笔记,即可在此处拖放或上传新文件。", loading: "正在加载附件列表...", noMatches: "未找到匹配附件", empty: "暂无附件", noMatchesDescription: "请尝试更改搜索词或分类过滤器", - emptyDescription: "拖拽文件到这里,或在上方上传文件", + emptyDescription: "在笔记中添加的图片和附件会显示在这里", previewImage: "大图预览", downloadOpen: "下载/打开", deletedMemo: "已删除笔记", fromMemo: "来自:{{source}}", sourceMemo: "来源笔记:{{source}}", openInNewWindow: "在新窗口打开", - dropTitle: "将文件释放于此", - dropDescription: "文件将上传并附加到当前正在编辑的笔记:", - uploadSuccess: "上传成功!", - uploadError: "上传失败,请稍后重试", }, tagsDialog: { title: "标签", diff --git a/tests/e2e/assets-pane.spec.ts b/tests/e2e/assets-pane.spec.ts new file mode 100644 index 00000000..8ad9d473 --- /dev/null +++ b/tests/e2e/assets-pane.spec.ts @@ -0,0 +1,10 @@ +import { expect, test } from "@playwright/test"; + +test("keeps the global attachment manager independent from the selected note", async ({ page }) => { + await page.goto("/"); + await page.getByRole("button", { name: "附件", exact: true }).click(); + + await expect(page.getByRole("heading", { name: "附件管理", exact: true })).toBeVisible(); + await expect(page.getByText("当前关联笔记")).toHaveCount(0); + await expect(page.getByRole("button", { name: "上传附件", exact: true })).toHaveCount(0); +});