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

In the studio, a repository that is not an app drops you into the git tree with nothing to say #188

Merged opened this

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

Merged

scout/forge:scout/181-in-the-studio-a-repository-that-is-not-a → main 1 commit fork

  • Merged. scout/181-in-the-studio-a-repository-that-is-not-a was merged into main as 1adf9aa.

Review

Sign in to review this change.
3 files changed +90 −7
docs/STUDIO.md +12
@@ -58,6 +58,18 @@ page: the studio only offers principled moves.
5858
5959 ## The app
6060
61 `/:owner/:name` in the studio, on a repository with no app record, keeps
62 the studio's own framing rather than silently becoming dev's code page: one
63 card above the code says in one line that this repository has no app yet,
64 with a door to make it one — `/settings/app`, the same "Run as an app" form
65 Settings already carries — open only to whoever could submit it (`canAdmin`);
66 a reader sees the sentence with no button that would just come back
67 refused. `?mode=dev` on the same URL is unaffected: the card is the studio's
68 own, never dev's. `/tells` and `/changes` on such a repository still send a
69 studio viewer on to `/issues` and `/pulls`, as they do for any repository
70 the studio does not apply to — the card names the one thing to do here,
71 not a second front door onto lists that would be empty anyway.
72
6173 An app's home in the studio (`/:owner/:name`, when the repository is an
6274 app) shows, top to bottom: what it serves and where; the box; what asks
6375 you; what is happening now. Beside it, who is on the crew and what each is
src/web/pages/repo.ts +33 −7
@@ -628,6 +628,31 @@ function aboutCard(
628628
629629 // ---- pages ----
630630
631 /**
632 * Studio's own word for a repository it otherwise has nothing to say about:
633 * no app record, so no head card, no box, no Now — just the code underneath,
634 * same as it always rendered. One line says so, and the one door out —
635 * `/settings/app`, the same "Run as an app" form Settings already carries —
636 * opens only for whoever could actually submit it, so a reader never sees a
637 * button that would just come back refused.
638 */
639 function noAppCard(ctx: Ctx, repo: Repo): Html {
640 const admin = canAdmin(ctx.app.db, repo, ctx.auth.user);
641 return empty({
642 icon: "rocket",
643 title: "This repository has no app yet.",
644 ...(admin
645 ? {
646 action: btn("Run as an app", {
647 href: `${repoBase(repo)}/settings/app`,
648 variant: "primary",
649 icon: "rocket",
650 }),
651 }
652 : {}),
653 });
654 }
655
631656 /** The tree, the README under it, and the ref switcher above both. */
632657 export async function renderCode(
633658 scope: RepoScope,
@@ -843,17 +868,18 @@ export async function repoHome(ctx: Ctx): Promise<Response> {
843868 const repo = loadRepo(ctx);
844869 if (!repo) return notFoundPage(ctx);
845870 // In the studio an app's home is the app: what it serves, who is on it,
846 // what it asks. Dev mode is the code, as ever.
847 if (ctx.mode === "studio") {
848 const record = getAppByRepo(ctx.app.db, repo.id);
849 if (record !== undefined) return await studioApp(ctx, repo, record);
850 }
871 // what it asks. Dev mode is the code, as ever. A repository with no app
872 // record has nothing of that to show — read once, used either way below.
873 const studioMode = ctx.mode === "studio";
874 const record = studioMode ? getAppByRepo(ctx.app.db, repo.id) : undefined;
875 if (record !== undefined) return await studioApp(ctx, repo, record);
876 const noApp = studioMode ? noAppCard(ctx, repo) : null;
851877 const scope = localScope(ctx, repo);
852878 const cloneUrl = `${ctx.url.origin}/${repo.ownerName}/${repo.name}.git`;
853879 const refs = await refSetFrom(scope.source);
854880
855881 if (refs.branches.length === 0) {
856 const body = html`${scopeHeader(ctx, scope, "code")}
882 const body = html`${scopeHeader(ctx, scope, "code")}${noApp}
857883 <div class="mt">
858884 ${empty({
859885 icon: "repo",
@@ -906,7 +932,7 @@ export async function repoHome(ctx: Ctx): Promise<Response> {
906932
907933 const body = html`${scopeHeader(ctx, scope, "code", {
908934 branchCount: refs.branches.length,
909 })}
935 })}${noApp}
910936 ${appStrip(ctx, repo)} ${forkBar(ctx, repo)}
911937 ${await renderCode(scope, {
912938 refs,
test/studio.test.ts +45
@@ -222,6 +222,51 @@ describe("the studio", () => {
222222 expect(dev).toContain(">Commits<");
223223 });
224224
225 test("a repository with no app record keeps the studio's framing: one card, one door to make it one; ?mode=dev is unchanged, and an app itself never shows the card", async () => {
226 const plain = await createRepo(app, alice, {
227 name: "plainhome",
228 visibility: "public",
229 description: "no app here",
230 });
231 if (!plain.ok) throw new Error(String(plain.error));
232 const seeded = await commitToBranch(app, alice, plain.value, {
233 message: "Start",
234 files: [{ path: "README.md", content: "hi\n" }],
235 });
236 if (!seeded.ok) throw new Error(JSON.stringify(seeded.error));
237
238 const studio = await (await get("/alice/plainhome")).text();
239 expect(studio).toContain("This repository has no app yet.");
240 expect(studio).toContain(`href="/alice/plainhome/settings/app"`);
241 expect(studio).toContain("Run as an app");
242
243 // `?mode=dev` on the very same URL renders exactly what it always has:
244 // the code, no card above it at all.
245 const dev = await (
246 await get("/alice/plainhome", { cookie: `${cookie}; forge_mode=dev` })
247 ).text();
248 expect(dev).toContain("README.md");
249 expect(dev).toContain(">Commits<");
250 expect(dev).not.toContain("This repository has no app yet.");
251 expect(dev).not.toContain("Run as an app");
252
253 // A signed-out reader of the same public repository sees the same
254 // sentence but no door it could not walk through.
255 const stranger = await (
256 await fetch(`${base}/alice/plainhome`, {
257 headers: { cookie: "forge_mode=studio" },
258 redirect: "manual",
259 })
260 ).text();
261 expect(stranger).toContain("This repository has no app yet.");
262 expect(stranger).not.toContain("Run as an app");
263 expect(stranger).not.toContain(`href="/alice/plainhome/settings/app"`);
264
265 // An app itself never carries this card.
266 const app_ = await (await get("/alice/notes")).text();
267 expect(app_).not.toContain("This repository has no app yet.");
268 });
269
225270 test("the studio's design is in the stylesheet, not in every page", async () => {
226271 // The rules are static, so they belong in the file the browser caches.
227272 // A studio page carries no <style> at all — only a page that computes CSS

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs