Public: anyone can read this; the crew can change it.

An app the owner no longer cares about is archived in one tap and restored in one, starting from the parked archive branch #10885

Closed opened this
Closed without merging

mason/forge:mason/10872-an-app-the-owner-no-longer-cares-about-i → main 1 commit fork

  • Closed without merging. Nothing was applied to main.

Review

Sign in to review this change.
6 files changed +47 −3
src/http/server.ts +2
@@ -393,6 +393,7 @@ import {
393393 settingsUpdate,
394394 waveCut,
395395 archiveSubmit,
396 archiveConfirmed,
396397 transferSubmit,
397398 provenanceSubmit,
398399 mirrorStop,
@@ -1472,6 +1473,7 @@ export function buildRouter(): Router<Handler> {
14721473 router.add("POST", "/:owner/:name/settings", settingsUpdate);
14731474 router.add("POST", "/:owner/:name/settings/wave", waveCut);
14741475 router.add("POST", "/:owner/:name/settings/archive", archiveSubmit);
1476 router.add("POST", "/:owner/:name/archive", archiveConfirmed);
14751477 router.add("POST", "/:owner/:name/settings/transfer", transferSubmit);
14761478 router.add("POST", "/:owner/:name/settings/provenance", provenanceSubmit);
14771479 router.add("POST", "/:owner/:name/settings/mirror/stop", mirrorStop);
src/web/pages/apps.ts +1 −1
@@ -158,7 +158,7 @@ function wakeForms(ctx: Ctx, repos: readonly Repo[]): Map<string, Html> {
158158 <form method="post" action="${repoBase(repo)}/settings/archive">
159159 <input type="hidden" name="archived" value="0" />
160160 <input type="hidden" name="to" value="/apps?archived=1" />
161 ${btn(html`Wake<span class="sr-only"> ${repo.ownerName}/${repo.name}</span>`)}
161 ${btn(html`Restore<span class="sr-only"> ${repo.ownerName}/${repo.name}</span>`)}
162162 </form>
163163 </li>`,
164164 );
src/web/pages/search.ts +10
@@ -564,6 +564,16 @@ const DESTRUCTIVE: {
564564 pattern: RegExp;
565565 describe: (m: RegExpMatchArray, ctx: Ctx) => Destructive;
566566 }[] = [
567 {
568 pattern: /^\/([^/]+)\/([^/]+)\/archive$/,
569 describe: (m) => ({
570 verb: "Archive",
571 title: `Archive ${m[1]}/${m[2]}?`,
572 detail:
573 "Its process stops and it leaves home and the apps list. Its data and git stay, its address still answers, and Restore on the Archived list starts it again.",
574 phrase: "",
575 }),
576 },
567577 {
568578 pattern: /^\/([^/]+)\/([^/]+)\/settings\/delete$/,
569579 describe: (m) => ({
src/web/pages/settings.ts +13 −1
@@ -549,10 +549,22 @@ function samePath(ctx: Ctx, to: string | undefined): string | undefined {
549549 * for travels in the form, so a double submit is idempotent.
550550 */
551551 export async function archiveSubmit(ctx: Ctx): Promise<Response> {
552 return archiveWith(ctx, undefined);
553 }
554
555 /**
556 * POST /:owner/:name/archive — the one-tap Archive, reached only through
557 * `/-/confirm`: the form there carries no field, so the answer is fixed here.
558 */
559 export async function archiveConfirmed(ctx: Ctx): Promise<Response> {
560 return archiveWith(ctx, "1");
561 }
562
563 async function archiveWith(ctx: Ctx, fixed: "1" | undefined): Promise<Response> {
552564 const gate = await adminForm(ctx);
553565 if (gate instanceof Response) return gate;
554566 const { repo, form } = gate;
555 const asked = field(form, "archived");
567 const asked = fixed ?? field(form, "archived");
556568 if (asked !== "0" && asked !== "1")
557569 return flashTo(ctx, `${repoBase(repo)}/settings`, {
558570 tone: "danger",
src/web/studio/app.ts +2
@@ -47,6 +47,7 @@ import {
4747 badge,
4848 btn,
4949 card,
50 confirmAction,
5051 envPhrase,
5152 icon,
5253 listCard,
@@ -454,6 +455,7 @@ export async function studioApp(
454455 const more = html`<p class="muted small">
455456 <a href="${base}/variants">Variants</a>
456457 ${when(canAdmin(db, repo, ctx.auth.user), () => html`<a href="${base}/settings/app">Environment settings</a>`)}
458 ${when(canAdmin(db, repo, ctx.auth.user), () => confirmAction({ action: `${base}/archive`, verb: "Archive", size: "sm" }))}
457459 </p>`;
458460
459461 // An app that follows another says so before anything else: whether it
test/apps-archived.test.ts +19 −1
@@ -139,7 +139,7 @@ test("the archived list wakes an app in place, and ignores a return address off
139139 expect(list).toContain('action="/alice/shelved/settings/archive"');
140140 expect(list).toContain('name="to" value="/apps?archived=1"');
141141 // Each button names its app, so a screen reader can tell the Wakes apart.
142 expect(list).toMatch(/Wake<span class="sr-only"> alice\/shelved<\/span>/);
142 expect(list).toMatch(/Restore<span class="sr-only"> alice\/shelved<\/span>/);
143143 const post = (to: string, headers: Record<string, string> = { origin: servers.url }) =>
144144 fetch(`${servers.url}/alice/shelved/settings/archive`, {
145145 method: "POST",
@@ -176,3 +176,21 @@ test("a viewer who does not administer an archived app gets no Wake button", asy
176176 expect(anon).not.toContain("settings/archive");
177177 expect((await get("/apps?archived=1")).body).toContain("/alice/openshelf/settings/archive");
178178 });
179
180 test("one tap: the app's page offers Archive through the confirm page, which archives and Restore undoes", async () => {
181 const page = await get("/alice/keeper");
182 expect(page.body).toContain("/-/confirm?to=%2Falice%2Fkeeper%2Farchive");
183 const confirm = await get("/-/confirm?to=%2Falice%2Fkeeper%2Farchive");
184 expect(confirm.status).toBe(200);
185 expect(confirm.body).toContain('action="/alice/keeper/archive"');
186 const post = (headers: Record<string, string>) =>
187 fetch(`${servers.url}/alice/keeper/archive`, { method: "POST", headers: { cookie, ...headers }, redirect: "manual" });
188 expect((await post({ origin: "https://evil.example" })).status).toBeGreaterThanOrEqual(400);
189 expect(getRepo(app.db, "alice", "keeper")!.archivedAt).toBeNull();
190 expect((await post({ origin: servers.url })).status).toBe(303);
191 expect(getRepo(app.db, "alice", "keeper")!.archivedAt).not.toBeNull();
192 expect((await get("/apps")).body).not.toContain(ROW_KEEPER);
193 expect((await get("/apps?archived=1")).body).toContain(ROW_KEEPER);
194 expect((await get("/alice/keeper")).status).toBe(200);
195 await updateRepoSettings(app, alice, getRepo(app.db, "alice", "keeper")!, { archived: false });
196 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs