mirror of
https://github.com/runbear-io/beardrive.git
synced 2026-08-25 08:08:08 +02:00
fix(hub): one Revoke per share link — the modal hands off, the banner undoes (BEA-32) (#77)
Opening Share on a file that already had a live public link showed the URL twice, each copy with its own Revoke: the transient dialog on top of the persistent ShareBanner. Both hit the same link, so nothing was at risk, but a destructive control shown twice is a control nobody wants to click. The dialog loses Revoke and keeps its job — confirm what happened, hand over the URL (Copy link / Open / Done) and set an expiry. The banner keeps Revoke: it is what BEA-16 added so the undo outlives the dialog, and it is still there on every later visit to the file. Deviation from the reviewed plan: it also had this delete the `token` derivation and the `api` import, which existed only for the Revoke handler. BEA-29 (#74) landed first and its expiry PATCH now uses both, so they stay — the plan assumed this PR would go in ahead of it. The e2e that revoked through the modal now revokes through the banner, and asserts `.modal .ai-del` has count 0 right after Share — the assertion that stops the duplication coming back. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
946a552c64
commit
31472c56e7
@@ -155,13 +155,22 @@ test("share mints a public link that serves the file, revoke kills it", async ({
|
|||||||
const pid = await wikiId(page);
|
const pid = await wikiId(page);
|
||||||
await page.goto(`/${pid}/guide.md`);
|
await page.goto(`/${pid}/guide.md`);
|
||||||
await page.click("#share-btn");
|
await page.click("#share-btn");
|
||||||
|
// BEA-32: the modal hands over the URL and nothing destructive — Revoke is
|
||||||
|
// the banner's, and two of them for one link is what this asserts away.
|
||||||
|
await expect(page.locator(".modal .ai-del")).toHaveCount(0);
|
||||||
const url = await page.locator(".modal-url").textContent();
|
const url = await page.locator(".modal-url").textContent();
|
||||||
expect(url).toContain("/s/");
|
expect(url).toContain("/s/");
|
||||||
const publicRes = await page.request.get(url!);
|
const publicRes = await page.request.get(url!);
|
||||||
expect(publicRes.status()).toBe(200);
|
expect(publicRes.status()).toBe(200);
|
||||||
expect(await publicRes.text()).toContain("Second version");
|
expect(await publicRes.text()).toContain("Second version");
|
||||||
await page.click(".modal .ai-del"); // revoke
|
|
||||||
await expectToast(page, "revoked");
|
// Revoke where the control actually lives, from the file page.
|
||||||
|
await page.click(".modal button:has-text('Done')");
|
||||||
|
const banner = page.locator(".share-banner");
|
||||||
|
await expect(banner).toBeVisible();
|
||||||
|
await banner.locator(".ai-del").click();
|
||||||
|
await page.click(".modal .danger-btn");
|
||||||
|
await expectToast(page, "Share revoked");
|
||||||
const gone = await page.request.get(url!);
|
const gone = await page.request.get(url!);
|
||||||
expect(gone.status()).toBe(404);
|
expect(gone.status()).toBe(404);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -515,7 +515,10 @@ export default function Browser(props: {
|
|||||||
copied={share.copied}
|
copied={share.copied}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
setShare(null);
|
setShare(null);
|
||||||
refreshShares(); // the dialog can revoke; the banner must agree
|
// Create may have handed back an existing link, in which case
|
||||||
|
// shareNow's own invalidation raced a stale cache. Refresh again
|
||||||
|
// so the banner agrees with what the dialog just showed.
|
||||||
|
refreshShares();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -20,9 +20,13 @@ const EXPIRY_PRESETS = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
/* A clear, explicitly-public share confirmation: warns that anyone with the
|
/* A clear, explicitly-public share confirmation: warns that anyone with the
|
||||||
link can view, and offers copy / open / revoke. The title says "Public
|
link can view, and offers copy / open / expiry. The title says "Public
|
||||||
link", not "created": ShareDB.Create hands back the file's existing live
|
link", not "created": ShareDB.Create hands back the file's existing live
|
||||||
link when there is one, so a second Share click is not a second link. */
|
link when there is one, so a second Share click is not a second link.
|
||||||
|
|
||||||
|
Revoke lives on the ShareBanner, not here: this modal is a transient
|
||||||
|
handoff of the URL, the banner is what is still on the file after it
|
||||||
|
closes. Two Revokes for one link is a destructive control shown twice. */
|
||||||
export function ShareDialog({
|
export function ShareDialog({
|
||||||
url,
|
url,
|
||||||
copied,
|
copied,
|
||||||
@@ -105,20 +109,6 @@ export function ShareDialog({
|
|||||||
<Button variant="subtle" onClick={() => window.open(url, "_blank")}>
|
<Button variant="subtle" onClick={() => window.open(url, "_blank")}>
|
||||||
Open
|
Open
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
|
||||||
variant="subtle" className="ai-del"
|
|
||||||
onClick={async () => {
|
|
||||||
try {
|
|
||||||
await api("DELETE", "/api/shares/" + token);
|
|
||||||
toast("Link revoked — it no longer works.");
|
|
||||||
onClose();
|
|
||||||
} catch (e) {
|
|
||||||
toast((e as Error).message, true);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Revoke
|
|
||||||
</Button>
|
|
||||||
<Button variant="subtle" onClick={onClose}>
|
<Button variant="subtle" onClick={onClose}>
|
||||||
Done
|
Done
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
+6
-6
File diff suppressed because one or more lines are too long
@@ -5,7 +5,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>BearDrive</title>
|
<title>BearDrive</title>
|
||||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32' fill='%23f5a623'><rect x='4' y='4' width='5.6' height='24'/><rect x='11.2' y='4' width='14.4' height='11.2'/><rect x='11.2' y='16.8' width='16.8' height='11.2'/></svg>">
|
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32' fill='%23f5a623'><rect x='4' y='4' width='5.6' height='24'/><rect x='11.2' y='4' width='14.4' height='11.2'/><rect x='11.2' y='16.8' width='16.8' height='11.2'/></svg>">
|
||||||
<script type="module" crossorigin src="/assets/index-DJI_9uJj.js"></script>
|
<script type="module" crossorigin src="/assets/index-d1aFxMDF.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-Dyv4ewL7.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-Dyv4ewL7.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
Reference in New Issue
Block a user