fix(webapp): new-project dialog preselects the RECOMMENDED row (BEA-72) (#123)

The RECOMMENDED badge renders on options[0]; the initial selection was
hard-wired to "" (the last row, Empty project), so on any hub shipping
templates the dialog contradicted its own advice — and the default was the
option that produces a project with nothing to look at.

Hoist the options array above the state and seed `template` from
options[0].value: badge row and checked row are now the same element by
construction, not by coincidence. On a template-less hub options[0] is
"I already have a folder", which still creates an empty project.

The e2e test that pinned the old default now asserts the invariant (badged
row === checked row) rather than a title, and checks that creating without
touching the radios actually seeds the template.
This commit is contained in:
Snow Lee (Sungwon)
2026-08-05 17:25:53 +09:00
committed by GitHub
parent 5d399c3d31
commit 27cc558ac7
5 changed files with 46 additions and 34 deletions
+14 -4
View File
@@ -116,17 +116,27 @@ test("the create dialog does not open itself when projects exist", async ({ page
await expect(page.locator(".modal-input")).toHaveCount(0);
});
test("new project via the sidebar + modal", async ({ page }) => {
test("new project via the sidebar + modal seeds the recommended option", async ({ page }) => {
await login(page);
await page.click("#projects .nav-add");
await page.fill(".modal-input", "scratch");
// The starting point defaults to an empty project, so this path still
// describes exactly what it did before templates existed.
// The badged row IS the checked row — asserted as an invariant, not by
// name, so reordering the options can't quietly split them apart again.
await expect(page.locator(".start-points")).toBeVisible();
await expect(page.locator(".start-point.on")).toContainText("Empty project");
const on = page.locator(".start-point.on");
const rec = page.locator(".start-point:has(.sp-rec)");
await expect(on).toHaveCount(1);
await expect(rec).toHaveCount(1);
expect(await on.textContent()).toBe(await rec.textContent());
await page.click(".modal .pbtn");
await page.waitForURL(/\/[0-9a-f-]{36}$/);
await expect(page.locator("#project-select")).toContainText("scratch");
// Creating without touching the radios seeds that template. Asserted on the
// file tree, not #content: a brand-new project's dashboard paints no
// treemap cells (#93).
for (const name of ["docs", "decisions", "AGENTS.md"]) {
await expect(page.locator("#sidebar").getByText(name, { exact: true }).first()).toBeVisible();
}
// Open the switcher: both projects listed; picking one navigates.
await page.click("#project-select");
await expect(page.getByRole("option", { name: "wiki" })).toBeVisible();
@@ -17,9 +17,11 @@ export const EXISTING = "__existing__";
// shape. This is a local useState over the Dialog we already have.
//
// The options come from /api/config, so a hub that ships another template
// needs no change here. "Empty project" is the synthetic first-class option
// (value "") and stays preselected: creating a project without picking
// anything must behave exactly as it did before templates existed.
// needs no change here. The first option is both the badged one and the
// preselected one — by construction, since the initial state is read off the
// same array the badge indexes into. On a hub shipping no templates that
// first option is "I already have a folder", which still creates an empty
// project.
export function NewProjectDialog({
templates,
onCreate,
@@ -29,10 +31,24 @@ export function NewProjectDialog({
onCreate: (name: string, template: string) => Promise<void>;
onClose: () => void;
}) {
// Recommended first, then the rest, then the two that seed nothing. The
// divider before them is doing real work: everything above answers "what
// should we put in it", everything below answers "nothing".
const options = [
...templates.map((t) => ({ value: t.name, title: t.title, blurb: t.blurb, rule: false })),
{
value: EXISTING,
title: "I already have a folder",
blurb: "nothing is seeded — connect it and your files stay as they are",
rule: true,
},
{ value: "", title: "Empty project", blurb: "just the folder", rule: false },
];
const [name, setName] = useState("");
// "" is an empty project; EXISTING is also an empty project — same artifact,
// different intent, and the intent is what the next screen needs to know.
const [template, setTemplate] = useState("");
const [template, setTemplate] = useState(options[0].value);
const [err, setErr] = useState("");
const [busy, setBusy] = useState(false);
@@ -50,20 +66,6 @@ export function NewProjectDialog({
}
};
// Recommended first, then the rest, then the two that seed nothing. The
// divider before them is doing real work: everything above answers "what
// should we put in it", everything below answers "nothing".
const options = [
...templates.map((t) => ({ value: t.name, title: t.title, blurb: t.blurb, rule: false })),
{
value: EXISTING,
title: "I already have a folder",
blurb: "nothing is seeded — connect it and your files stay as they are",
rule: true,
},
{ value: "", title: "Empty project", blurb: "just the folder", rule: false },
];
return (
<Dialog open onOpenChange={(open) => !open && onClose()}>
<DialogContent className="modal" showCloseButton={false}>
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -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-C_yRHCCD.js"></script>
<script type="module" crossorigin src="/assets/index-ClP6ITud.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-C52IQv2y.css">
</head>
<body>