feat(web): copy a note to WeChat from the card menu
Text-note card menus can copy the note as rich text. The open note uses the editor, including unsaved changes; other notes copy saved content without switching the editor.
This commit is contained in:
1 parent
fbb41aee9d
commit
65ef837f45
7 files changed
+91
-3
No files matched your search
@@ -51,6 +51,7 @@ import {
|
||||
Pencil,
|
||||
Copy,
|
||||
} from "lucide-react";
|
||||
import { WeChatIcon } from "./WeChatIcon";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -75,6 +76,7 @@ import type {
|
||||
MemoListDensity,
|
||||
MemoContextMenuState,
|
||||
MemoDocumentAction,
|
||||
MemoWeChatCopyResult,
|
||||
NotebookMoveOption,
|
||||
} from "@/lib/app-helpers";
|
||||
import { contentEnterMotion, paneEnterMotion } from "@/lib/motion";
|
||||
@@ -392,6 +394,7 @@ export const MemoListPane = ({
|
||||
onTogglePinMemo,
|
||||
onMoveMemo,
|
||||
onRequestDocumentAction,
|
||||
onCopyMemoToWeChat,
|
||||
onMoveSelectedMemos,
|
||||
onPinSelectedMemos,
|
||||
onExportSelectedMemos,
|
||||
@@ -471,6 +474,7 @@ export const MemoListPane = ({
|
||||
onTogglePinMemo: (memo: MemoSummary) => void;
|
||||
onMoveMemo: (memoId: string, notebookId: string) => void;
|
||||
onRequestDocumentAction: (memoId: string, action: MemoDocumentAction, printWindow?: Window | null) => void;
|
||||
onCopyMemoToWeChat: (memoId: string) => Promise<MemoWeChatCopyResult>;
|
||||
onMoveSelectedMemos: (notebookId: string) => void;
|
||||
onPinSelectedMemos: (pinned: boolean) => void;
|
||||
onExportSelectedMemos: () => void;
|
||||
@@ -515,6 +519,7 @@ export const MemoListPane = ({
|
||||
const [lastSelectedMemoId, setLastSelectedMemoId] = useState<string | null>(null);
|
||||
const [moveTargetNotebookId, setMoveTargetNotebookId] = useState("");
|
||||
const [memoIdCopyNotice, setMemoIdCopyNotice] = useState<{ status: "copied" | "error"; id: string } | null>(null);
|
||||
const [wechatCopyNotice, setWechatCopyNotice] = useState<"copied" | "error" | null>(null);
|
||||
const [fileDragActive, setFileDragActive] = useState(false);
|
||||
|
||||
const filterOptions = useMemo(() => getMemoFilterOptions(t), [t]);
|
||||
@@ -655,6 +660,16 @@ export const MemoListPane = ({
|
||||
window.setTimeout(() => setMemoIdCopyNotice(null), copied ? 2200 : 3000);
|
||||
};
|
||||
|
||||
const handleCopyContextMemoToWeChat = async () => {
|
||||
const memo = memoContextMenu?.memo;
|
||||
if (!memo || memo.diagramKind || memo.structuredTable || memo.infographic) return;
|
||||
setMemoContextMenu(null);
|
||||
const result = await onCopyMemoToWeChat(memo.id);
|
||||
if (result === "editor") return;
|
||||
setWechatCopyNotice(result);
|
||||
window.setTimeout(() => setWechatCopyNotice(null), result === "copied" ? 2200 : 2600);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const nextTargetId = resolveSelectionMoveTargetNotebookId(
|
||||
moveTargetNotebookId,
|
||||
@@ -875,7 +890,7 @@ export const MemoListPane = ({
|
||||
// Keep enough room for the full action list. Radix can still adjust the
|
||||
// final position, but this prevents the initial placement from starting
|
||||
// below the viewport on short or zoomed desktop viewports.
|
||||
const menuHeight = view === "trash" ? 216 : 356;
|
||||
const menuHeight = view === "trash" ? 216 : 392;
|
||||
const x = Math.min(clientX, Math.max(12, window.innerWidth - menuWidth - 12));
|
||||
const y = Math.min(clientY, Math.max(12, window.innerHeight - menuHeight - 12));
|
||||
|
||||
@@ -1634,6 +1649,15 @@ export const MemoListPane = ({
|
||||
<Share2 className="h-4 w-4 text-slate-500" />
|
||||
{t(isLocalMemoId(memoContextMenu.memo.id) ? "sharing.afterSync" : "sharing.action")}
|
||||
</DropdownMenuItem>
|
||||
{!memoContextMenu.memo.diagramKind && !memoContextMenu.memo.structuredTable && !memoContextMenu.memo.infographic && (
|
||||
<DropdownMenuItem
|
||||
className="flex h-9 w-full items-center gap-2 px-3 text-left text-xs text-slate-700 hover:bg-slate-50 cursor-pointer outline-none"
|
||||
onClick={() => void handleCopyContextMemoToWeChat()}
|
||||
>
|
||||
<WeChatIcon className="h-4 w-4 text-slate-500" />
|
||||
{t("editor.copyToWeChat")}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuItem
|
||||
className="flex h-9 w-full items-center gap-2 px-3 text-left text-xs text-slate-700 hover:bg-slate-50 cursor-pointer outline-none"
|
||||
onClick={() => requestContextDocumentAction("export-markdown")}
|
||||
@@ -1695,6 +1719,12 @@ export const MemoListPane = ({
|
||||
</ClipboardCopyNotice>
|
||||
)}
|
||||
|
||||
{wechatCopyNotice && (
|
||||
<ClipboardCopyNotice status={wechatCopyNotice}>
|
||||
{t(wechatCopyNotice === "copied" ? "editor.copiedToWeChat" : "editor.copyToWeChatFailed")}
|
||||
</ClipboardCopyNotice>
|
||||
)}
|
||||
|
||||
{selectionMode && (
|
||||
<MobileSelectionActionBar
|
||||
canDelete={selectedMemoIds.size > 0 && !isDeleting}
|
||||
|
||||
@@ -137,6 +137,7 @@ import { compressImageForUpload } from "@/lib/image-compression";
|
||||
import { createScreenshotMemo, screenshotFileFromImportPayload, screenshotImportDedupeKey, screenshotImportGate } from "@/lib/screenshot-import";
|
||||
import { createSharedFileMemo, sharedFileTitle } from "@/lib/shared-file-import";
|
||||
import { createWeChatChatMemo } from "@/lib/wechat-chat-import";
|
||||
import { copyMarkdownToWeChat } from "@/lib/wechat-copy";
|
||||
import { isDesktopResourceRuntime, stageDesktopResource, toDesktopResourceUrl } from "@/lib/desktop-resources";
|
||||
import { findMatchingMemoResource } from "@/lib/staged-resource-repair";
|
||||
|
||||
@@ -3380,6 +3381,29 @@ export const WorkspaceApp = ({
|
||||
printWindow,
|
||||
});
|
||||
}}
|
||||
onCopyMemoToWeChat={async (memoId) => {
|
||||
const openTextNote = selectedMemo?.id === memoId
|
||||
&& !selectedDiagram
|
||||
&& !selectedTableNote
|
||||
&& !selectedInfographicNote;
|
||||
if (openTextNote) {
|
||||
memoDocumentActionIdRef.current += 1;
|
||||
setMemoDocumentActionRequest({
|
||||
id: memoDocumentActionIdRef.current,
|
||||
memoId,
|
||||
action: "copy-wechat",
|
||||
});
|
||||
return "editor";
|
||||
}
|
||||
|
||||
try {
|
||||
const { memo } = await repository.getMemo(memoId, memoView === "trash");
|
||||
await copyMarkdownToWeChat(memo.contentMarkdown);
|
||||
return "copied";
|
||||
} catch {
|
||||
return "error";
|
||||
}
|
||||
}}
|
||||
onTogglePinMemo={handleToggleMemoPinned}
|
||||
onPinSelectedMemos={handlePinSelectedMemos}
|
||||
onExportSelectedMemos={handleExportSelectedMemos}
|
||||
|
||||
@@ -70,6 +70,7 @@ describe("dispatchMemoDocumentAction", () => {
|
||||
saveAsTemplate: () => calls.push("template"),
|
||||
share: () => calls.push("share"),
|
||||
shareImage: () => calls.push("image"),
|
||||
copyWeChat: () => calls.push("wechat"),
|
||||
};
|
||||
|
||||
dispatchMemoDocumentAction("share", handlers);
|
||||
@@ -78,7 +79,8 @@ describe("dispatchMemoDocumentAction", () => {
|
||||
dispatchMemoDocumentAction("export-pdf", handlers, null);
|
||||
dispatchMemoDocumentAction("share-image", handlers);
|
||||
dispatchMemoDocumentAction("save-as-template", handlers);
|
||||
dispatchMemoDocumentAction("copy-wechat", handlers);
|
||||
|
||||
expect(calls).toEqual(["share", "markdown", "html", ["pdf", null], "image", "template"]);
|
||||
expect(calls).toEqual(["share", "markdown", "html", ["pdf", null], "image", "template", "wechat"]);
|
||||
});
|
||||
});
|
||||
@@ -47,6 +47,7 @@ export const dispatchMemoDocumentAction = (
|
||||
saveAsTemplate: () => void;
|
||||
share: () => void;
|
||||
shareImage: () => void;
|
||||
copyWeChat: () => void;
|
||||
},
|
||||
printWindow?: Window | null,
|
||||
) => {
|
||||
@@ -69,5 +70,8 @@ export const dispatchMemoDocumentAction = (
|
||||
case "save-as-template":
|
||||
handlers.saveAsTemplate();
|
||||
break;
|
||||
case "copy-wechat":
|
||||
handlers.copyWeChat();
|
||||
break;
|
||||
}
|
||||
};
|
||||
@@ -237,6 +237,7 @@ export const useEditorDocumentActions = ({
|
||||
if (canShareMemo) setShareOpen(true);
|
||||
},
|
||||
shareImage: handleOpenImageShare,
|
||||
copyWeChat: () => void handleCopyToWeChat(),
|
||||
},
|
||||
documentActionRequest.printWindow,
|
||||
);
|
||||
@@ -244,6 +245,7 @@ export const useEditorDocumentActions = ({
|
||||
canShareMemo,
|
||||
documentActionRequest,
|
||||
editor,
|
||||
handleCopyToWeChat,
|
||||
handleExportHtml,
|
||||
handleExportMarkdown,
|
||||
handleExportPdf,
|
||||
|
||||
@@ -42,7 +42,9 @@ export type MemoDocumentAction =
|
||||
| "export-html"
|
||||
| "export-pdf"
|
||||
| "share-image"
|
||||
| "save-as-template";
|
||||
| "save-as-template"
|
||||
| "copy-wechat";
|
||||
export type MemoWeChatCopyResult = "editor" | "copied" | "error";
|
||||
export type MemoDocumentActionRequest = {
|
||||
id: number;
|
||||
memoId: string;
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
const readSource = (path: string) => readFileSync(new URL(path, import.meta.url), "utf8");
|
||||
|
||||
const memoListSource = readSource("../apps/web/src/components/MemoListPane.tsx");
|
||||
const workspaceSource = readSource("../apps/web/src/components/WorkspaceApp.tsx");
|
||||
const editorActionsSource = readSource("../apps/web/src/components/editor/useEditorDocumentActions.ts");
|
||||
|
||||
describe("memo list WeChat copy", () => {
|
||||
test("adds the publish copy action to text-note card menus", () => {
|
||||
expect(memoListSource).toContain('t("editor.copyToWeChat")');
|
||||
expect(memoListSource).toContain("!memoContextMenu.memo.diagramKind && !memoContextMenu.memo.structuredTable && !memoContextMenu.memo.infographic");
|
||||
expect(memoListSource).toContain("void handleCopyContextMemoToWeChat()");
|
||||
expect(memoListSource).toContain('wechatCopyNotice === "copied" ? "editor.copiedToWeChat" : "editor.copyToWeChatFailed"');
|
||||
});
|
||||
|
||||
test("copies the open text note through the editor and other notes from saved markdown", () => {
|
||||
expect(workspaceSource).toContain('action: "copy-wechat"');
|
||||
expect(workspaceSource).toContain("copyMarkdownToWeChat(memo.contentMarkdown)");
|
||||
expect(workspaceSource).toContain("return \"editor\"");
|
||||
expect(editorActionsSource).toContain("copyWeChat: () => void handleCopyToWeChat()");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user