diff --git a/apps/web/src/components/TableEditorPane.tsx b/apps/web/src/components/TableEditorPane.tsx index eb108c0c..f6f5ee30 100644 --- a/apps/web/src/components/TableEditorPane.tsx +++ b/apps/web/src/components/TableEditorPane.tsx @@ -1,7 +1,7 @@ import { useEffect, useMemo, useRef, useState } from "react"; import { useQuery } from "@tanstack/react-query"; import { flexRender, getCoreRowModel, useReactTable, type ColumnDef } from "@tanstack/react-table"; -import { ChevronLeft, Download, Form, Paperclip, Plus, RefreshCw, TableProperties, Trash2, X } from "lucide-react"; +import { Check, ChevronLeft, Copy, Download, Form, Paperclip, Plus, RefreshCw, TableProperties, Trash2, X } from "lucide-react"; import { useTranslation } from "react-i18next"; import { addTableField, @@ -49,7 +49,8 @@ import { import { Input } from "@/components/ui/input"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { api } from "@/lib/api"; -import { toDesktopResourceUrl } from "@/lib/desktop-resources"; +import { copyImageUrlToClipboard } from "@/lib/clipboard"; +import { toDesktopResourceDownloadUrl, toDesktopResourceUrl } from "@/lib/desktop-resources"; import { EDITOR_LOCAL_SAVE_DELAY_MS } from "@/lib/app-helpers"; import { createLocalEditSession } from "@/components/editor/editor-pane-helpers"; import { isLocalMemoId } from "@/lib/local-mirror"; @@ -160,6 +161,16 @@ const FieldHeader = ({ const attachmentItems = (value: TableCellValue): TableAttachment[] => Array.isArray(value) ? value : []; +const isImageAttachment = (item: TableAttachment) => item.mimeType.toLowerCase().startsWith("image/"); + +const downloadTableAttachment = (href: string, filename: string) => { + const anchor = document.createElement("a"); + anchor.href = toDesktopResourceDownloadUrl(href, filename); + anchor.download = filename; + anchor.rel = "noreferrer"; + anchor.click(); +}; + const AttachmentCell = ({ field, record, @@ -181,11 +192,33 @@ const AttachmentCell = ({ }) => { const { t } = useTranslation(); const inputRef = useRef(null); + const copyTimer = useRef(null); const [uploading, setUploading] = useState(false); const [uploadError, setUploadError] = useState(null); + const [copiedId, setCopiedId] = useState(null); + const [copyError, setCopyError] = useState(null); const items = attachmentItems(record.cells[field.id] ?? null); const limitReached = items.length >= TABLE_ATTACHMENT_LIMIT; + useEffect(() => () => { + if (copyTimer.current !== null) window.clearTimeout(copyTimer.current); + }, []); + + const copyImage = async (href: string, resourceId: string) => { + setCopyError(null); + const copied = await copyImageUrlToClipboard(href); + if (!copied) { + setCopiedId(null); + setCopyError(t("structuredTable.copyImageFailed")); + return; + } + setCopiedId(resourceId); + if (copyTimer.current !== null) window.clearTimeout(copyTimer.current); + copyTimer.current = window.setTimeout(() => { + setCopiedId((current) => current === resourceId ? null : current); + }, 2000); + }; + const uploadFiles = async (files: FileList | null) => { if (!files?.length || readOnly) return; setUploading(true); @@ -214,19 +247,74 @@ const AttachmentCell = ({ }; return ( -
+
{items.map((item) => { const href = toDesktopResourceUrl(tableAttachmentUrl(item.resourceId)); - const image = item.mimeType.startsWith("image/"); + const image = isImageAttachment(item); + const openLabel = t("structuredTable.openAttachment", { name: item.filename }); + const copied = copiedId === item.resourceId; return ( -
- {image ? :