Skip to content

Commit

New project and Import from GitHub work in the workspace you're in, as the sidebar shows it, with no workspace to pick on the page; a link that names another of your workspaces switches you to it first. The projects guide says so.

syntaqxcommitted Parent1cd92adBrowse files
6 files+66−770/6 viewed
+3−1
249249
250250 ## Create a project
251251
252−1. Choose **New project** in the sidebar, or go to `g1t.sh/new`.
252+1. In the workspace the project belongs to, choose **New project** in the
253+ sidebar, or go to `g1t.sh/new`. It's made in the workspace you're in; to
254+ make one somewhere else, switch workspaces first.
253255 2. Choose where its code comes from:
254256 - **Start empty**: a new repository on g1t.
255257 - **Import code**: copy a public repository from GitHub or any git host
+3−3
767767 done: repos.length > 0,
768768 title: "Add a project",
769769 about: "Create a repository, or import one by its address. Push to it with git as usual.",
770− to: workspace ? `/new?workspace=${workspace}` : "/new",
770+ to: "/new",
771771 action: "Add",
772772 },
773773 trial?.open
864864 </div>
865865 ) : (
866866 <Link
867− to={workspace ? `/new?workspace=${workspace}` : "/new"}
867+ to={"/new"}
868868 className="inline-flex items-center gap-1.5 rounded-md border border-line-strong px-3 py-2 text-sm font-medium text-fg/90 hover:bg-raised hover:text-fg"
869869 >
870870 <Plus size={14} /> New project
10301030 <Empty
10311031 action={
10321032 <Link
1033− to={workspace ? `/new?workspace=${workspace}` : "/new"}
1033+ to={"/new"}
10341034 className="rounded-md bg-fg px-3 py-1.5 text-sm font-medium text-bg hover:bg-fg-hover"
10351035 >
10361036 Create or import a project
+1−1
626626 <SidebarGroup
627627 title="Projects"
628628 action={
629− <Link to={`/new?workspace=${slug}`} aria-label="New project" className="rounded p-0.5 text-faint hover:bg-raised hover:text-fg">
629+ <Link to="/new" aria-label="New project" className="rounded p-0.5 text-faint hover:bg-raised hover:text-fg">
630630 <Plus size={13} />
631631 </Link>
632632 }
+29−0
1+/**
2+ * The workspace a page outside `/<workspace>` acts on (New project, Import
3+ * from GitHub): always the one you're in, as the sidebar shows it, never a
4+ * choice made on the page. A link that names another of your workspaces
5+ * (`?workspace=`) switches you to it first, so the page and the shell
6+ * agree, and then drops it from the address.
7+ */
8+import { redirect } from "react-router";
9+import type { User } from "@g1t/contracts";
10+
11+import { readCookie } from "./mission";
12+import { WORKSPACE_COOKIE, chosenWorkspace, rememberWorkspace } from "./workspace-choice";
13+import { pagePath } from "./workspace-nav";
14+
15+export function currentWorkspace(user: User, request: Request) {
16+ const memberships = user.workspaces ?? [];
17+ const url = new URL(request.url);
18+ const asked = url.searchParams.get("workspace")?.trim().toLowerCase();
19+ const current = chosenWorkspace(memberships, readCookie(request.headers.get("cookie"), WORKSPACE_COOKIE));
20+ if (asked) {
21+ const named = memberships.find((membership) => membership.slug.toLowerCase() === asked);
22+ url.searchParams.delete("workspace");
23+ // A click asks for `<path>.data?_routes=…`; the address given is the page's own.
24+ url.searchParams.delete("_routes");
25+ const headers = named && named.slug !== current?.slug ? { "set-cookie": rememberWorkspace(named.slug, url.protocol === "https:") } : undefined;
26+ throw redirect(`${pagePath(url.pathname)}${url.search}`, { headers });
27+ }
28+ return current;
29+}
+16−33
1111 import { githubApp } from "../lib/github.server";
1212 import { page } from "../lib/meta";
1313 import { assertSameOrigin, requireUser, roleIn } from "../lib/session.server";
14+import { currentWorkspace } from "../lib/current-workspace.server";
1415
1516 export function meta(args: Route.MetaArgs) {
1617 return page(args, { title: "Import from GitHub · g1t" });
1920 export async function loader({ request, context }: Route.LoaderArgs) {
2021 const user = requireUser(context, request);
2122 const url = new URL(request.url);
22− const workspaces = (user.workspaces ?? []).map((membership) => ({
23− slug: membership.slug,
24− name: membership.name ?? membership.slug,
25− owner: membership.role === "owner",
26− }));
27− if (workspaces.length === 0) throw redirect("/workspaces/new");
28− const asked = url.searchParams.get("workspace")?.toLowerCase();
29− const workspace = workspaces.find((option) => option.slug === asked)?.slug ?? workspaces[0].slug;
23+ const current = currentWorkspace(user, request);
24+ if (!current) throw redirect("/workspaces/new");
25+ const workspace = current.slug;
26+ const workspaceName = current.name?.trim() || current.slug;
3027 // What the person can see on GitHub is asked for alongside the status:
3128 // an installation made there directly is offered here to be added.
3229 const [status, visible] = await Promise.all([
3330 githubApp.status(user, workspace),
3431 githubApp.visibleInstallations(user).catch(() => null),
3532 ]);
36− if (!status.ok || !status.value.configured) throw redirect(`/new?workspace=${workspace}`);
33+ if (!status.ok || !status.value.configured) throw redirect("/new");
3734 const { installations, linked } = status.value;
3835 const recorded = new Set(installations.map((item) => item.id));
3936 const claimable =
5350 else error = listed.error.message;
5451 }
5552 return {
56− workspaces,
5753 workspace,
54+ workspaceName,
5855 owner: roleIn(user, workspace) === "owner",
5956 linked,
6057 installations,
7572 if (form.get("intent") === "add-installation") {
7673 const added = await githubApp.addInstallation(user, workspace, installationId);
7774 if (!added.ok) return { error: null, addError: added.error.message, results: [] };
78− throw redirect(`/new/github?workspace=${encodeURIComponent(workspace)}&installation=${installationId}`);
75+ throw redirect(`/new/github?installation=${installationId}`);
7976 }
8077 if (form.get("intent") === "remove-installation") {
8178 const removed = await githubApp.removeInstallation(user, workspace, installationId);
8279 // Shown by the list it was removed from, which is there with or without an account chosen.
8380 if (!removed.ok) return { error: null, removeError: removed.error.message, results: [] };
84− throw redirect(`/new/github?workspace=${encodeURIComponent(workspace)}`);
81+ throw redirect("/new/github");
8582 }
8683 const mode = (["import", "mirror", "push"] as const).find((option) => option === form.get("mode")) ?? "import";
8784 const chosen = form.getAll("repo").map(Number).filter((id) => Number.isSafeInteger(id) && id > 0).slice(0, 20);
10299 }
103100
104101 export default function NewFromGithub({ loaderData, actionData }: Route.ComponentProps) {
105− const { workspace, workspaces, installations, claimable, installation, repositories, linked, owner } = loaderData;
106− const workspaceName = workspaces.find((option) => option.slug === workspace)?.name ?? workspace;
102+ const { workspace, workspaceName, installations, claimable, installation, repositories, linked, owner } = loaderData;
107103 const these = claimable.length === 1 ? "this account" : "these accounts";
108− const here = `/new/github?workspace=${workspace}`;
104+ const here = "/new/github";
109105 const names = new Map((repositories?.repositories ?? []).map((repo) => [repo.id, repo.fullName]));
110106 return (
111107 <main className="mx-auto max-w-2xl px-4 py-12">
112− <Link to={`/new?workspace=${workspace}`} className="inline-flex items-center gap-1.5 text-sm text-muted hover:text-fg">
108+ <Link to="/new" className="inline-flex items-center gap-1.5 text-sm text-muted hover:text-fg">
113109 <ArrowLeft size={14} /> New project
114110 </Link>
115111 <span className="mt-6 flex size-10 items-center justify-center rounded-xl bg-accent/10 text-accent ring-1 ring-accent/30">
121117 which repositories it can see on GitHub; nothing else is read.
122118 </p>
123119
124− {workspaces.length > 1 && (
125− <nav className="mt-6 flex flex-wrap gap-2" aria-label="Workspace">
126− {workspaces.map((option) => (
127− <Link
128− key={option.slug}
129− to={`/new/github?workspace=${option.slug}`}
130− className={`rounded-full border px-3 py-1 text-sm ${option.slug === workspace ? "border-accent/60 text-fg" : "border-line text-muted hover:text-fg"}`}
131− >
132− {option.name}
133− </Link>
134− ))}
135− </nav>
136− )}
137120
138121 {!linked ? (
139122 <section className="mt-8 rounded-lg border border-line p-6">
231214 {installations.map((item) => (
232215 <li key={item.id} className="flex flex-wrap items-center gap-x-3 gap-y-1 px-4 py-2.5 text-sm">
233216 <Link
234− to={`${here}&installation=${item.id}`}
217+ to={`${here}?installation=${item.id}`}
235218 className={`whitespace-nowrap ${item.id === installation?.id ? "font-medium text-fg" : "text-muted hover:text-fg"}`}
236219 >
237220 {item.account}
310293 {repositories && repositories.total > repositories.perPage && (
311294 <div className="flex justify-between text-sm">
312295 {repositories.page > 1 ? (
313− <Link to={`${here}&installation=${installation.id}&page=${repositories.page - 1}`} className="text-muted hover:text-fg">
296+ <Link to={`${here}?installation=${installation.id}&page=${repositories.page - 1}`} className="text-muted hover:text-fg">
314297 Previous
315298 </Link>
316299 ) : (
317300 <span />
318301 )}
319302 {repositories.page * repositories.perPage < repositories.total && (
320− <Link to={`${here}&installation=${installation.id}&page=${repositories.page + 1}`} className="text-muted hover:text-fg">
303+ <Link to={`${here}?installation=${installation.id}&page=${repositories.page + 1}`} className="text-muted hover:text-fg">
321304 Next
322305 </Link>
323306 )}
362345 <SubmitButton variant="accent" pending="Bringing them across…" match={{ intent: "import" }}>
363346 Bring to g1t
364347 </SubmitButton>
365− <ButtonLink to={`/new?workspace=${workspace}`} variant="quiet">
348+ <ButtonLink to="/new" variant="quiet">
366349 Cancel
367350 </ButtonLink>
368351 </div>
+14−39
44
55 import type { Route } from "./+types/new";
66 import { page } from "../lib/meta";
7−import { Avatar, ErrorText, SubmitButton } from "../components/ui";
7+import { ErrorText, SubmitButton } from "../components/ui";
88 import { Field, FieldDescription, FieldLabel, FieldLegend, FieldSet } from "../components/ui/field";
99 import { Input, InputAddon, InputGroup } from "../components/ui/input";
1010 import { RadioCard, RadioGroup } from "../components/ui/radio-group";
11−import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue } from "../components/ui/select";
1211 import { repos } from "../lib/services.server";
1312 import { GithubMark } from "../components/github";
1413 import { githubApp } from "../lib/github.server";
1514 import { assertSameOrigin, requireUser } from "../lib/session.server";
15+import { currentWorkspace } from "../lib/current-workspace.server";
1616
1717 export function meta(args: Route.MetaArgs) {
1818 return page(args, { title: "New project · g1t" });
2020
2121 export async function loader({ request, context }: Route.LoaderArgs) {
2222 const user = requireUser(context, request);
23− const workspaces = (user.workspaces ?? []).map((membership) => ({
24− slug: membership.slug,
25− role: membership.role,
26− name: membership.name ?? membership.slug,
27− avatar: membership.avatar ?? null,
28− }));
29− // Projects live in a workspace, so there has to be one first.
30− if (workspaces.length === 0) throw redirect("/workspaces/new");
31− const asked = new URL(request.url).searchParams.get("workspace");
32− const selected = asked && workspaces.some((workspace) => workspace.slug === asked) ? asked : workspaces[0].slug;
23+ // Projects live in a workspace, so there has to be one first; a new one
24+ // goes in the workspace you're in.
25+ const current = currentWorkspace(user, request);
26+ if (!current) throw redirect("/workspaces/new");
27+ const selected = current.slug;
3328 // With g1t's GitHub App configured, repositories can come from GitHub.
3429 const github = await githubApp.status(user, selected).catch(() => null);
35− return { workspaces, selected, github: Boolean(github?.ok && github.value.configured) };
30+ return { selected, name: current.name?.trim() || current.slug, github: Boolean(github?.ok && github.value.configured) };
3631 }
3732
3833 export async function action({ request, context }: Route.ActionArgs) {
7671 },
7772 ];
7873
79−const ROLE_LABELS: Record<string, string> = { owner: "Owner", admin: "Admin", member: "Member" };
8074
8175 /** Replaces the mirror card when g1t's GitHub App is configured. */
8276 const GITHUB_SOURCE = {
9185 const [source, setSource] = useState<Source>("empty");
9286 const navigate = useNavigate();
9387 const sources = loaderData.github ? SOURCES.map((option) => (option.id === "mirror" ? GITHUB_SOURCE : option)) : SOURCES;
94− const [workspace, setWorkspace] = useState(loaderData.selected);
88+ const workspace = loaderData.selected;
9589 const [name, setName] = useState("");
9690 return (
9791 <main className="mx-auto max-w-2xl px-4 py-12">
111105 name="source"
112106 value={source}
113107 onValueChange={(value) =>
114− value === "github" ? navigate(`/new/github?workspace=${workspace}`) : setSource(value as Source)
108+ value === "github" ? navigate("/new/github") : setSource(value as Source)
115109 }
116110 aria-label="Where its code comes from"
117111 className="gap-3 sm:grid-cols-3"
141135 <Field>
142136 <FieldLabel htmlFor="name">Name</FieldLabel>
143137 <InputGroup className="h-10">
144− <Select name="workspace" value={workspace} onValueChange={setWorkspace}>
145− <SelectTrigger
146− aria-label="Workspace"
147− className="h-full w-auto max-w-[55%] shrink-0 rounded-none border-0 bg-surface px-2.5 font-medium hover:bg-raised focus-visible:ring-0 data-[state=open]:bg-raised data-[state=open]:ring-0 sm:max-w-[45%]"
148− >
149− <SelectValue />
150− </SelectTrigger>
151− <SelectContent align="start" className="min-w-64">
152− <SelectGroup>
153− <SelectLabel>Workspaces</SelectLabel>
154− {loaderData.workspaces.map((option) => (
155− <SelectItem
156− key={option.slug}
157− value={option.slug}
158− icon={<Avatar name={option.name} image={option.avatar} size={18} square />}
159− description={`g1t.sh/${option.slug} · ${ROLE_LABELS[option.role] ?? option.role}`}
160− >
161− {option.name}
162− </SelectItem>
163− ))}
164− </SelectGroup>
165− </SelectContent>
166− </Select>
138+ <input type="hidden" name="workspace" value={workspace} />
139+ <InputAddon className="max-w-[55%] shrink-0 gap-2 bg-surface px-2.5 font-medium text-fg sm:max-w-[45%]">
140+ <span className="truncate">{loaderData.name}</span>
141+ </InputAddon>
167142 <InputAddon className="border-l border-line px-2 font-mono text-base text-faint">/</InputAddon>
168143 <Input
169144 id="name"