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

Explore's empty state offers a stranger Create an account, not only Sign in #8627

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

torque/forge:torque/8463-explore-s-empty-state-offers-a-stranger → main 2 commits fork 2 threads · 2 unresolved

  • Merged. torque/8463-explore-s-empty-state-offers-a-stranger was merged into main as 95df35f.

Review

Sign in to review this change.
4 files changed +66 −14
src/web/chrome.ts +10
@@ -148,6 +148,16 @@ export function signInHref(ctx: Ctx): string {
148148 return `/signin?next=${encodeURIComponent(next)}`;
149149 }
150150
151 /**
152 * Whether a signed-out visitor is offered Create an account beside Sign in.
153 * Behind a platform edge the edge is the one way in, unless it has vouched
154 * for them as a stranger who may sign up here. Home and explore share this.
155 */
156 export function offersSignup(ctx: Ctx): boolean {
157 const token = activeEdgeToken(ctx.app);
158 return token === undefined || edgeStrangerLogin(ctx.request, token, ctx.auth.user) !== undefined;
159 }
160
151161 // ---- repo header ----
152162
153163 export type RepoTab =
src/web/studio/explore.ts +11 −9
@@ -16,7 +16,8 @@ import {
1616 } from "../../actions/explore.ts";
1717 import { notePhase, takeSessionTime } from "../../core/route_timing.ts";
1818 import type { Ctx } from "../../http/context.ts";
19 import { pageHead } from "../chrome.ts";
19 import { edgeLoginUrl } from "../../http/edge.ts";
20 import { offersSignup, pageHead } from "../chrome.ts";
2021 import { html, type Html } from "../html.ts";
2122 import { page } from "../layout.ts";
2223 import { btn, empty, listCard, nameMark, row as rowOf, seg, timeAgo } from "../ui.ts";
@@ -94,7 +95,7 @@ function terms(query: ExploreQuery): { term: string; without: ExploreQuery }[] {
9495 }
9596
9697 /** Nothing to list: a bare line means nothing is public yet, not a miss. */
97 function emptyState(signedIn: boolean, query: ExploreQuery): Html {
98 function emptyState(ctx: Ctx, query: ExploreQuery): Html {
9899 const line = exploreLine(query);
99100 if (line !== "")
100101 return empty({
@@ -112,12 +113,13 @@ function emptyState(signedIn: boolean, query: ExploreQuery): Html {
112113 icon: "search",
113114 title: "Nothing public yet",
114115 note: "Make the first app and it shows up here.",
115 action: signedIn
116 ? btn("Make an app", { href: "/apps/new", variant: "primary" })
117 : btn("Sign in to make one", {
118 href: "/signin?next=/apps/new",
119 variant: "primary",
120 }),
116 action:
117 ctx.auth.user !== undefined
118 ? btn("Make an app", { href: "/apps/new", variant: "primary" })
119 : offersSignup(ctx)
120 ? html`${btn("Create an account", { href: "/signup?next=/apps/new", variant: "primary" })}
outdated

.form-actions carries margin-top:16px and exists for forms. Check the spacing on the phone look. If it is off, reuse the empty() action spacing rather than this wrapper.

121 ${btn("Sign in", { href: "/signin?next=/apps/new" })}`
122 : btn("Sign in", { href: edgeLoginUrl("/apps/new"), variant: "primary" }),
121123 });
outdated

This edge-login branch is untested. Add a test with an edge token and no vouched stranger that expects the edge login URL and no Create an account. Add a second test with a vouched stranger that expects Create an account.

122124 }
123125
@@ -200,7 +202,7 @@ export async function explorePage(ctx: Ctx): Promise<Response> {
200202 >
201203 </div>
202204 ${rows.length === 0
203 ? emptyState(ctx.auth.user !== undefined, query)
205 ? emptyState(ctx, query)
204206 : listCard(rows.map(rowHtml))}`;
205207 const response = page({ ctx, title: "Explore", body });
206208 notePhase(ctx.request, "render", performance.now() - rendering);
src/web/studio/home.ts +3 −3
@@ -23,9 +23,9 @@ import { LOOK_SIZES } from "../../core/looks.ts";
2323 import { canRead, getRepo } from "../../core/repos.ts";
2424 import { type User } from "../../core/users.ts";
2525 import type { Ctx } from "../../http/context.ts";
26 import { activeEdgeToken, edgeLoginUrl, edgeStrangerLogin } from "../../http/edge.ts";
26 import { edgeLoginUrl } from "../../http/edge.ts";
2727 import { sshCommand } from "../../ssh/command.ts";
28 import { repoBase, signInFirst, NOT_ADMIN_HERE, forbiddenPage } from "../chrome.ts";
28 import { repoBase, offersSignup, signInFirst, NOT_ADMIN_HERE, forbiddenPage } from "../chrome.ts";
2929 import { field, readForm, type Form } from "../form.ts";
3030 import { liveRegionsJs } from "../client.ts";
3131 import { html, when, type Html } from "../html.ts";
@@ -608,7 +608,7 @@ export function studioDoor(ctx: Ctx): Response {
608608 <div class="bar">
609609 ${ctx.auth.user !== undefined
610610 ? null
611 : activeEdgeToken(ctx.app) === undefined || edgeStrangerLogin(ctx.request, activeEdgeToken(ctx.app), ctx.auth.user) !== undefined
611 : offersSignup(ctx)
612612 ? html`${btn("Create an account", {
613613 href: `/signup${nextQuery}`,
614614 variant: "primary",
test/explore.test.ts +42 −2
@@ -17,7 +17,8 @@ import { createIssue } from "../src/core/issues.ts";
1717 import type { Repo } from "../src/core/repos.ts";
1818 import { createToken } from "../src/core/tokens.ts";
1919 import { createUser, type User } from "../src/core/users.ts";
20 import { serve, type Servers } from "../src/http/server.ts";
20 import { edgeLoginUrl } from "../src/http/edge.ts";
21 import { forgeHttp, serve, type Servers } from "../src/http/server.ts";
2122 import { quietLogs } from "./helpers/globals.ts";
2223 import { proveAll } from "./helpers/verified.ts";
2324 import { promote } from "./helpers/members.ts";
@@ -434,11 +435,13 @@ describe("explore on a forge with nothing public", () => {
434435 const out = await get("/explore", false);
435436 expect(out).toContain("Nothing public yet");
436437 expect(out).toContain('href="/signin?next=/apps/new"');
438 expect(out).toContain('href="/signup?next=/apps/new"');
439 expect(out).toContain("Create an account");
437440 expect(out).not.toContain("Clear the line");
438441 const mine = await get("/explore", true);
439442 expect(mine).toContain("Nothing public yet");
440443 expect(mine).toContain('href="/apps/new"');
441 expect(mine).not.toContain("Sign in to make one");
444 expect(mine).not.toContain("Create an account");
442445 });
443446
444447 test("a query with no hits still says nothing matches", async () => {
@@ -630,3 +633,40 @@ test("isExploreLine is true for each explore key and false for plain words", ()
630633 expect(isExploreLine(line)).toBe(true);
631634 for (const line of ["", "garden", "zed-one", "downer:x"]) expect(isExploreLine(line)).toBe(false);
632635 });
636
637 describe("explore's empty state behind a platform edge", () => {
638 const TOKEN = "edge-secret-for-explore";
639 let dataDir: string;
640 let app: App;
641 let http: ReturnType<typeof forgeHttp>;
642
643 beforeAll(async () => {
644 dataDir = await mkdtemp(join(tmpdir(), "forge-explore-edge-"));
645 app = createApp({ dataDir, port: 0, url: "https://forge.example", edgeToken: TOKEN });
646 http = forgeHttp(app);
647 }, 30_000);
648
649 afterAll(async () => {
650 http.close();
651 app.close();
652 await rm(dataDir, { recursive: true, force: true });
653 });
654
655 const get = async (headers: Record<string, string>) =>
656 (await http.handle(new Request("http://forge.test/explore", { headers }))).text();
657
658 test("a stranger the edge has not vouched for gets one Sign in to the edge, no Create an account", async () => {
659 const out = await get({});
660 expect(out).toContain("Nothing public yet");
661 expect(out).toContain(`href="${edgeLoginUrl("/apps/new")}"`);
662 expect(out).not.toContain("/signup");
663 expect(out).not.toContain("Create an account");
664 expect(out).not.toContain('href="/signin?next=/apps/new"');
665 });
666
667 test("a stranger the edge vouches for is offered Create an account", async () => {
668 const out = await get({ "x-plat-edge-token": TOKEN, "x-plat-user": "newcomer" });
669 expect(out).toContain("Create an account");
670 expect(out).toContain('href="/signup?next=/apps/new"');
671 });
672 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs