fix(sharing): align memo sharing actions and allow revocation retries
This commit is contained in:
1 parent
043752546f
commit
82fa24c3a0
4 files changed
+39
-6
No files matched your search
@@ -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");
|
||||
|
||||
@@ -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 = ?`
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in new issue
Block a user