fix(sharing): align memo sharing actions and allow revocation retries

This commit is contained in:
tianma-if committed 2026-10-14 10:27:01 +08:00
1 parent 043752546f
commit 82fa24c3a0
4 files changed
+39 -6

No files matched your search

+19
View File
@@ -336,6 +336,25 @@ const readCookieValue = (response, name) => {
};
describe("memo share management", () => {
test("revocation retries succeed and keep the old link inaccessible", async () => {
const { sqlite, environment } = createDatabaseEnvironment();
const app = createShareApp(environment);
const path = "/api/v1/memos/memo_source/share";
expect((await app.request(`/api/public/shares/${sourceToken}`, {}, environment)).status).toBe(200);
for (let attempt = 0; attempt < 2; attempt++) {
const response = await app.request(path, { method: "DELETE" }, environment);
expect(response.status).toBe(200);
expect(await response.json()).toEqual({ ok: true });
expect((await (await app.request(path, {}, environment)).json()).share).toBeNull();
expect((await app.request(`/api/public/shares/${sourceToken}`, {}, environment)).status).toBe(404);
}
expect((await app.request(`/api/public/shares/${targetToken}`, {}, environment)).status).toBe(200);
const unauthenticated = new Hono();
registerMemoShareRoutes(unauthenticated);
expect((await unauthenticated.request(path, { method: "DELETE" }, environment)).status).toBe(403);
sqlite.close();
});
test("lists only live shares from the signed-in workspace without exposing passwords", async () => {
const { sqlite, environment } = createDatabaseEnvironment();
sqlite.query("INSERT INTO workspaces (id, name, is_personal) VALUES (?, ?, 1)").run("ws_other", "Other workspace");
+2 -1
View File
@@ -475,7 +475,8 @@ export const registerMemoShareRoutes = (app: Hono<AppEnv>) => {
const existing = await c.env.storage.db.prepare(
`SELECT memo_id FROM memo_shares WHERE memo_id = ? AND workspace_id = ?`
).bind(memoId, workspaceId).first<{ memo_id: string }>();
if (!existing) return notFound(c, "Active share not found");
// A retry after successful revocation already has the requested state.
if (!existing) return c.json({ ok: true });
await c.env.storage.db.prepare(
`DELETE FROM memo_shares WHERE memo_id = ? AND workspace_id = ?`
+15 -2
View File
@@ -11,6 +11,7 @@ import {
type KeyboardEvent as ReactKeyboardEvent,
type ReactNode,
} from "react";
import { useQuery } from "@tanstack/react-query";
import { useVirtualizer } from "@tanstack/react-virtual";
import { createPortal } from "react-dom";
import { useTranslation } from "react-i18next";
@@ -44,6 +45,7 @@ import {
TagX,
Image as ImageIcon,
Share2,
Globe,
FileDown,
FileCode2,
FileUp,
@@ -517,6 +519,17 @@ export const MemoListPane = ({
}) => {
const { t } = useTranslation();
const [memoContextMenu, setMemoContextMenu] = useState<MemoContextMenuState | null>(null);
const contextShareMemoId = memoContextMenu?.memo.id;
const contextShareQuery = useQuery({
queryKey: ["memo-share", contextShareMemoId ?? ""],
queryFn: () => {
if (!contextShareMemoId) throw new Error("Memo share query requires a memo id");
return api.getMemoShare(contextShareMemoId);
},
enabled: Boolean(contextShareMemoId && !isLocalMemoId(contextShareMemoId)),
retry: false,
staleTime: 30_000,
});
const [listDensity, setListDensity] = useState<MemoListDensity>(() => readMemoListDensityPreference());
const [lastSelectedMemoId, setLastSelectedMemoId] = useState<string | null>(null);
const [moveTargetNotebookId, setMoveTargetNotebookId] = useState("");
@@ -1671,8 +1684,8 @@ export const MemoListPane = ({
disabled={isLocalMemoId(memoContextMenu.memo.id)}
onClick={() => requestContextDocumentAction("share")}
>
<Share2 className="h-4 w-4 text-slate-500" />
{t(isLocalMemoId(memoContextMenu.memo.id) ? "sharing.afterSync" : "sharing.action")}
{contextShareQuery.data?.share ? <Globe className="h-4 w-4 text-slate-950" /> : <Share2 className="h-4 w-4 text-slate-500" />}
{t(isLocalMemoId(memoContextMenu.memo.id) ? "sharing.afterSync" : contextShareQuery.data?.share ? "sharing.manage" : "sharing.action")}
</DropdownMenuItem>
{!memoContextMenu.memo.diagramKind && !memoContextMenu.memo.structuredTable && !memoContextMenu.memo.infographic && !memoContextMenu.memo.poster && (
<DropdownMenuItem
@@ -1,5 +1,5 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { Check, Copy, ExternalLink, LoaderCircle, Share2, Trash2 } from "lucide-react";
import { Check, Copy, ExternalLink, Globe, LoaderCircle, Share2, Trash2 } from "lucide-react";
import { useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { Link, useLocation } from "react-router";
@@ -132,8 +132,8 @@ export const ShareMemoDialog = ({
>
<DialogHeader className="space-y-1.5 text-left">
<DialogTitle className="flex items-center gap-2 text-base font-semibold">
<Share2 className="h-5 w-5 text-slate-700" />
{t("sharing.title")}
{share ? <Globe className="h-5 w-5 text-slate-700" /> : <Share2 className="h-5 w-5 text-slate-700" />}
{t(share ? "sharing.manage" : "sharing.title")}
</DialogTitle>
</DialogHeader>