From a558062d376d1d6f62a85418fac4e04a67f7dd10 Mon Sep 17 00:00:00 2001 From: tianma-if Date: Sat, 15 Aug 2026 22:34:49 +0800 Subject: [PATCH] feat: copy note template IDs --- apps/web/src/components/TemplatesPane.tsx | 36 +++++++++++++++++++++++ packages/shared/src/i18n/en-US.ts | 3 ++ packages/shared/src/i18n/zh-CN.ts | 3 ++ tests/template-id-copy.test.ts | 15 ++++++++++ 4 files changed, 57 insertions(+) create mode 100644 tests/template-id-copy.test.ts diff --git a/apps/web/src/components/TemplatesPane.tsx b/apps/web/src/components/TemplatesPane.tsx index 2745eab7..2c7e02b4 100644 --- a/apps/web/src/components/TemplatesPane.tsx +++ b/apps/web/src/components/TemplatesPane.tsx @@ -2,6 +2,7 @@ import { useMemo, useState, type MouseEvent, type ReactNode } from "react"; import { Check, ChevronLeft, + Copy, Eye, LayoutList, Pencil, @@ -20,6 +21,7 @@ import { Input } from "@/components/ui/input"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; import { WORKSPACE_PAGE_TITLE_CLASSNAME } from "@/lib/workspace-ui"; +import { copyTextToClipboard } from "@/lib/clipboard"; import type { MemoTemplate as SavedMemoTemplate } from "@edgeever/shared"; const TemplateIconAction = ({ @@ -86,6 +88,16 @@ export const TemplatesPane = ({ } | null>(null); const [deleteConfirmTemplate, setDeleteConfirmTemplate] = useState(null); + const [templateIdCopyNotice, setTemplateIdCopyNotice] = useState<{ + id: string; + status: "copied" | "error"; + } | null>(null); + + const copyTemplateId = async (template: SavedMemoTemplate) => { + const copied = await copyTextToClipboard(template.id); + setTemplateIdCopyNotice({ id: template.id, status: copied ? "copied" : "error" }); + window.setTimeout(() => setTemplateIdCopyNotice(null), copied ? 2200 : 3000); + }; const startEditing = (template: SavedMemoTemplate) => { setCreatingTemplate(false); @@ -354,6 +366,16 @@ export const TemplatesPane = ({
+ { + e.stopPropagation(); + void copyTemplateId(template); + }} + > + + )} + + {templateIdCopyNotice && ( +
+ {t( + templateIdCopyNotice.status === "copied" ? "templates.idCopied" : "templates.idCopyFailed", + { id: templateIdCopyNotice.id }, + )} +
+ )}
); diff --git a/packages/shared/src/i18n/en-US.ts b/packages/shared/src/i18n/en-US.ts index a98f23e7..e448bb02 100644 --- a/packages/shared/src/i18n/en-US.ts +++ b/packages/shared/src/i18n/en-US.ts @@ -1272,6 +1272,9 @@ export const enUS = { emptyMyTemplates: "No templates yet", emptyMyTemplatesHint: "Click \"New Template\" above or save any note as a template directly in the editor.", delete: "Delete template", + copyId: "Copy template ID", + idCopied: "Template ID copied: {{id}}", + idCopyFailed: "Could not copy the template ID.", deleteConfirmTitle: "Delete template \"{{name}}\"", deleteConfirmDescription: "Are you sure you want to delete this template? This operation cannot be undone.", saveAsTemplate: "Save as template", diff --git a/packages/shared/src/i18n/zh-CN.ts b/packages/shared/src/i18n/zh-CN.ts index 68ceabc0..9e49b874 100644 --- a/packages/shared/src/i18n/zh-CN.ts +++ b/packages/shared/src/i18n/zh-CN.ts @@ -1272,6 +1272,9 @@ export const zhCN = { emptyMyTemplates: "暂无模板", emptyMyTemplatesHint: "你可以点击上方「新建模板」,或在笔记编辑器中将常用笔记另存为模板。", delete: "删除模板", + copyId: "复制模板 ID", + idCopied: "模板 ID 已复制:{{id}}", + idCopyFailed: "复制模板 ID 失败。", deleteConfirmTitle: "删除模板「{{name}}」", deleteConfirmDescription: "确定要删除该模板吗?此操作不可恢复。", saveAsTemplate: "另存为模板", diff --git a/tests/template-id-copy.test.ts b/tests/template-id-copy.test.ts new file mode 100644 index 00000000..eaf1e758 --- /dev/null +++ b/tests/template-id-copy.test.ts @@ -0,0 +1,15 @@ +import { describe, expect, test } from "bun:test"; +import { readFileSync } from "node:fs"; + +const templatesPaneSource = readFileSync( + new URL("../apps/web/src/components/TemplatesPane.tsx", import.meta.url), + "utf8", +); + +describe("copy note template ID", () => { + test("copies the raw persisted template ID from every template card", () => { + expect(templatesPaneSource).toContain("copyTextToClipboard(template.id)"); + expect(templatesPaneSource).toContain('label={t("templates.copyId")}'); + expect(templatesPaneSource).toContain('"templates.idCopied"'); + }); +});