Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh

Commit

Give visited workspaces their own project navigation

syntaqxcommitted Parent6e24edfBrowse files
3 files+52−250/3 viewed
+40−15
11181118 <Rule />
11191119 </>
11201120 )}
1121− <ModeHeader title={browsing ? "Explore" : "Code"} />
1121+ <ModeHeader title={browsing?.owner?.kind === "workspace" ? "Workspace" : browsing ? "Explore" : "Code"} />
11221122 <Drill trail={trail} />
11231123 </div>
11241124 );
11761176 const stars = atMine && query.get("tab") === "stars";
11771177 const shown = useBrowsedOwner(owner);
11781178 const profileTab = query.get("tab");
1179+ const workspace = useRouteLoaderData("routes/workspace/layout") as
1180+ | { workspace: { slug: string }; projects?: { namespace: string; name: string; title: string }[] }
1181+ | undefined;
1182+ if (owner?.kind === "workspace" && shown) {
1183+ const base = `/${owner.name}`;
1184+ const projects = workspace?.workspace.slug.toLowerCase() === owner.name.toLowerCase() ? workspace.projects ?? [] : [];
1185+ return (
1186+ <nav aria-label={`${owner.name} workspace`} className={PANEL}>
1187+ <NavRows>
1188+ <SidebarLink to={base} icon={<Avatar name={owner.name} image={shown.avatar} size={16} square />} current={false}>
1189+ {shown.name}
1190+ </SidebarLink>
1191+ <SidebarLink to={base} icon={<LayoutDashboard size={15} />} current={path.toLowerCase() === base.toLowerCase()}>
1192+ Overview
1193+ </SidebarLink>
1194+ </NavRows>
1195+ <Rule />
1196+ <NavGroup title="Projects">
1197+ {projects.slice(0, 8).map((project) => (
1198+ <SidebarLink key={project.name} to={`/${project.namespace}/${project.name}`} icon={<Box size={15} />}>
1199+ {project.title}
1200+ </SidebarLink>
1201+ ))}
1202+ <SidebarLink to={`${base}/-/projects`} icon={<LayoutGrid size={15} />} count={projects.length || undefined}>
1203+ All projects
1204+ </SidebarLink>
1205+ </NavGroup>
1206+ <Rule />
1207+ <NavRows>
1208+ <SidebarLink to={`${base}/-/packages`} icon={<Package size={15} />}>Packages</SidebarLink>
1209+ </NavRows>
1210+ <Rule />
1211+ <NavRows>
1212+ <SidebarLink to="/explore" icon={<Compass size={15} />}>Explore</SidebarLink>
1213+ </NavRows>
1214+ </nav>
1215+ );
1216+ }
11791217 return (
11801218 <nav aria-label="Explore" className={PANEL}>
11811219 <NavRows>
12021240 <>
12031241 <Rule />
12041242 <NavGroup title="You are viewing">
1205− {owner.kind === "workspace" ? (
1206− <>
1207− <SidebarLink
1208− to={`/${owner.name}`}
1209− icon={<Avatar name={owner.name} image={shown.avatar} size={16} square />}
1210− current={path.toLowerCase() === `/${owner.name}`.toLowerCase()}
1211− >
1212− {shown.name}
1213− </SidebarLink>
1214− <SidebarLink to={`/${owner.name}/-/projects`} icon={<LayoutGrid size={15} />}>
1215− Projects
1216− </SidebarLink>
1217− </>
1218− ) : (
1243+ {owner.kind === "person" && (
12191244 <>
12201245 <SidebarLink
12211246 to={`/u/${owner.name}`}
+6−3
1212 import { redirectIfRenamed } from "../../lib/renamed.server";
1313 import { rememberWorkspace } from "../../lib/workspace-choice";
1414 import { workspacePage, workspaceRedirect } from "../../lib/workspace-nav";
15+import { shortcutOf, workspaceProjects } from "../../lib/workspace-projects.server";
1516 import { WorkspacePageSkeleton } from "../../components/workspace-skeletons";
1617 import { identity } from "../../lib/services.server";
1718 import { getViewer, roleIn } from "../../lib/session.server";
3536 }
3637 const role = roleIn(viewer, workspace.slug);
3738 // Opening one of your workspaces makes it the one you are in.
38− if (!role) return { workspace, role, welcome: false };
39+ if (!role) {
40+ const listed = await workspaceProjects(workspace.slug, viewer);
41+ return { workspace, role, welcome: false, projects: listed.ok ? listed.value.map(shortcutOf) : [] };
42+ }
3943 const secure = new URL(request.url).protocol === "https:";
4044 const headers = new Headers({ "Set-Cookie": rememberWorkspace(workspace.slug, secure) });
4145 // Just joined with an invite: welcomed once (routes/invite.tsx).
203207 <h1 className="truncate text-2xl font-semibold tracking-tight">
204208 {workspace.name}
205209 </h1>
210+ <Badge size="md">Workspace</Badge>
206211 {role && <Badge size="md">{role}</Badge>}
207212 </div>
208213 <p className="font-mono text-sm text-muted">g1t.sh/{workspace.slug}</p>
213218 New project
214219 </ButtonLink>
215220 ) : (
216− // A visitor's sidebar is not this workspace's: its packages
217− // are a link here, as its projects are below.
218221 <ButtonLink to={`/${workspace.slug}/-/packages`} variant="outline" prefetch="intent">
219222 <Package size={15} />
220223 Packages
+6−7
3838
3939 export async function loader({ params, context, request }: Route.LoaderArgs) {
4040 const viewer = getViewer(context);
41− // Members only for now; a workspace's public packages are found through
42− // their repositories and search.
43− if (!roleIn(viewer, params.owner)) throw data(null, { status: 404 });
41+ const member = roleIn(viewer, params.owner) != null;
4442 const url = new URL(request.url);
4543 const type = url.searchParams.get("type");
4644 const ecosystem = type && (ECOSYSTEMS as readonly string[]).includes(type) ? (type as Ecosystem) : null;
4745 const query = url.searchParams.get("q")?.trim() || null;
4846 const visibility = pick<VisibilityFilter>(url.searchParams.get("visibility"), ["all", "public", "private"], "all");
4947 const sort = pick<PackageSort>(url.searchParams.get("sort"), ["updated", "downloads", "name"], "updated");
50− const view = url.searchParams.get("view") === "deleted" ? "deleted" : "active";
48+ const view = member && url.searchParams.get("view") === "deleted" ? "deleted" : "active";
5149 // Deleted packages are listed for those who administer them, and only
5250 // then is there a Deleted packages view.
5351 const [found, deletedFound] = await Promise.all([
5452 packages.list(params.owner, viewer, { ecosystem, query }),
55− packages.deleted(params.owner, viewer),
53+ member ? packages.deleted(params.owner, viewer) : null,
5654 ]);
5755 const listed = unwrap(found);
58− const deleted = deletedFound.ok ? deletedFound.value : [];
56+ const deleted = deletedFound?.ok ? deletedFound.value : [];
5957 // Whether the workspace has any at all decides between a filtered-out
6058 // list and the first-package page.
6159 const any = listed.length > 0 || deleted.length > 0 || ecosystem != null || query != null;
6260 return {
61+ member,
6362 list: arrange(listed, visibility, sort),
6463 deleted,
6564 view,
104103 /** The packages a workspace publishes: filtered, sorted, or, with none yet, how to start. */
105104 export default function Packages({ loaderData, actionData }: Route.ComponentProps) {
106105 const { list, deleted, view, any, ecosystem, query, visibility, sort, workspace } = loaderData;
107− if (!any) return <ChooseRegistry workspace={workspace} />;
106+ if (!any) return loaderData.member ? <ChooseRegistry workspace={workspace} /> : <EmptyState title="No public packages">This workspace has no packages you can view yet.</EmptyState>;
108107 if (view === "deleted") return <DeletedPackages workspace={workspace} deleted={deleted} outcome={actionData as Outcome | undefined} />;
109108 const filters: Filters = { type: ecosystem, q: query, visibility, sort };
110109 const filtered = ecosystem != null || query != null || visibility !== "all";

Comments

Sign in to comment.