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);
|
||||
await page.goto(`/${pid}/guide.md`);
|
||||
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();
|
||||
expect(url).toContain("/s/");
|
||||
const publicRes = await page.request.get(url!);
|
||||
expect(publicRes.status()).toBe(200);
|
||||
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!);
|
||||
expect(gone.status()).toBe(404);
|
||||
});
|
||||
|
||||
@@ -515,7 +515,10 @@ export default function Browser(props: {
|
||||
copied={share.copied}
|
||||
onClose={() => {
|
||||
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
|
||||
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 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({
|
||||
url,
|
||||
copied,
|
||||
@@ -105,20 +109,6 @@ export function ShareDialog({
|
||||
<Button variant="subtle" onClick={() => window.open(url, "_blank")}>
|
||||
Open
|
||||
</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}>
|
||||
Done
|
||||
</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">
|
||||
<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>">
|
||||
<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">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
Reference in New Issue
Block a user