mirror of
https://github.com/runbear-io/beardrive.git
synced 2026-08-25 08:08:08 +02:00
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:
@@ -85,7 +85,7 @@ classDiagram
|
||||
OrgAdmin HubSettings ProjectSettings
|
||||
Palette shell AccountBar ...
|
||||
}
|
||||
note for components "NewProjectDialog replaced ProjectNav's name-only modalPrompt: name + starting point, POSTing {name, template}. Its options come from useConfig()'s `templates`, never a hardcoded list, so a hub shipping another template needs no frontend change; 'Empty project' (value: the empty string) stays preselected so an unpicked create behaves exactly as it did before templates. modal.tsx keeps its one-field API — teaching it about choices would tax every other caller"
|
||||
note for components "NewProjectDialog replaced ProjectNav's name-only modalPrompt: name + starting point, POSTing {name, template}. Its options come from useConfig()'s `templates`, never a hardcoded list, so a hub shipping another template needs no frontend change; the initial selection is options[0].value — the same array element the RECOMMENDED badge indexes, so the badged row and the checked row are one row by construction (on a template-less hub that row is 'I already have a folder', which still creates an empty project). modal.tsx keeps its one-field API — teaching it about choices would tax every other caller"
|
||||
note for components "HistoryFilters drives the SERVER (?q=/?user=/?since=/?until= on the history API), never the loaded page — filtering what is on screen would lie about everything below the fold and break next_cursor. Its state is Route.filters, so a narrowed feed is linkable, survives reload, and Back undoes it; the author list accumulates across fetches, because filtering by one author leaves only their rows loaded"
|
||||
note for components "FileView's transformHTML now drops `data:image/svg` from any rendered img and any `data:` href from any rendered link — goldmark admits them, and an inline SVG is a document rather than a picture (the same property the server's sandboxInline walls off). Insights builds its per-device folder bag with Object.create(null), since folder names come off a peer's journal and one named __proto__ silently emptied the matrix. style.css sets unicode-bidi isolate-override on the peer-authored strings a reader is expected to CHECK (listing rows, breadcrumb, history path/note/device) — journal.SafeText refuses the bidi CONTROLS, but a single strong-RTL LETTER is legal and still reorders a row"
|
||||
note for components "components/ui — shadcn/ui primitives (Radix, copied in), themed from BearDrive tokens in tw.css; rendered markdown is transformed as a string before mounting, link clicks delegated on the container — never patch the dangerouslySetInnerHTML subtree"
|
||||
|
||||
@@ -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}>
|
||||
|
||||
+10
-10
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-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>
|
||||
|
||||
Reference in New Issue
Block a user