Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| Navigation: the workspace's own page with tabs, pins and recent in the sidebar | 1 | /** |
| 2 | * A workspace's own page at `g1t.sh/<workspace>`: its header, and tabs for | |
| 3 | * what it has (Overview, Projects, Packages, Teams, People, Insights and, | |
| 4 | * for owners, Settings). The sidebar is always the current workspace's; | |
| 5 | * this page is where the workspace itself is shown, to members and to | |
| 6 | * everyone else. | |
| 7 | */ | |
| 8 | ||
| 9 | export type WorkspaceTabKey = "overview" | "projects" | "packages" | "teams" | "people" | "insights" | "settings"; | |
| 10 | ||
| 11 | export type WorkspaceTab = { | |
| 12 | key: WorkspaceTabKey; | |
| 13 | label: string; | |
| 14 | to: string; | |
| 15 | /** Shown beside the label; left out when unknown. */ | |
| 16 | count?: number | null; | |
| 17 | /** Not built yet: its tab opens what it will be. */ | |
| 18 | soon?: boolean; | |
| 19 | }; | |
| 20 | ||
| 21 | /** The workspace's pages under `-/` that are drawn under its tabs. */ | |
| 22 | export const TAB_PAGES = ["projects", "packages", "teams", "people", "insights"] as const; | |
| 23 | ||
| 24 | /** Tabs whose pages say what they will be, until they are built. */ | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 25 | export const SOON_TABS = new Set<WorkspaceTabKey>(["insights"]); |
| Navigation: the workspace's own page with tabs, pins and recent in the sidebar | 26 | |
| 27 | /** | |
| 28 | * The tabs, in order. People, Teams and Insights are for members, and | |
| 29 | * Settings for owners; everyone sees the rest. | |
| 30 | */ | |
| 31 | export function workspaceTabs( | |
| 32 | slug: string, | |
| 33 | options: { member: boolean; owner: boolean; projects?: number | null; people?: number | null }, | |
| 34 | ): WorkspaceTab[] { | |
| 35 | const base = `/${slug}`; | |
| 36 | const tabs: (WorkspaceTab | false)[] = [ | |
| 37 | { key: "overview", label: "Overview", to: base }, | |
| 38 | { key: "projects", label: "Projects", to: `${base}/-/projects`, count: options.projects ?? null }, | |
| 39 | { key: "packages", label: "Packages", to: `${base}/-/packages` }, | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 40 | options.member && { key: "teams", label: "Teams", to: `${base}/-/teams` }, |
| Navigation: the workspace's own page with tabs, pins and recent in the sidebar | 41 | options.member && { key: "people", label: "People", to: `${base}/-/people`, count: options.people ?? null }, |
| 42 | options.member && { key: "insights", label: "Insights", to: `${base}/-/insights`, soon: true }, | |
| 43 | options.owner && { key: "settings", label: "Settings", to: `${base}/-/settings` }, | |
| 44 | ]; | |
| 45 | return tabs.filter((tab): tab is WorkspaceTab => tab !== false); | |
| 46 | } | |
| 47 | ||
| 48 | /** | |
| 49 | * Which tab a path is, under `/<workspace>`: null for the workspace's other | |
| 50 | * pages (its settings, Agent fleet, Usage and the rest), which have a page | |
| 51 | * heading of their own, and for one package's page. | |
| 52 | */ | |
| 53 | export function workspaceTab(pathname: string, slug: string): WorkspaceTabKey | null { | |
| 54 | const parts = pathname.split("/").filter(Boolean); | |
| 55 | if (parts[0]?.toLowerCase() !== slug.toLowerCase()) return null; | |
| 56 | if (parts.length === 1) return "overview"; | |
| 57 | if (parts[1] !== "-" || parts.length !== 3) return null; | |
| 58 | return (TAB_PAGES as readonly string[]).includes(parts[2]!) ? (parts[2] as WorkspaceTabKey) : null; | |
| 59 | } | |
| 60 | ||
| 61 | /** `?tab=` as people write it, from habit: the tab it means. */ | |
| 62 | const TAB_WORDS: Record<string, string> = { | |
| 63 | overview: "", | |
| 64 | projects: "-/projects", | |
| 65 | repositories: "-/projects", | |
| 66 | repos: "-/projects", | |
| 67 | packages: "-/packages", | |
| 68 | people: "-/people", | |
| 69 | members: "-/people", | |
| 70 | teams: "-/teams", | |
| 71 | insights: "-/insights", | |
| 72 | }; | |
| 73 | ||
| 74 | /** Workspace pages that moved, by their old name under `-/`. */ | |
| 75 | const MOVED: Record<string, string> = { | |
| 76 | members: "-/people", | |
| 77 | overview: "", | |
| 78 | "soon/teams": "-/teams", | |
| 79 | "soon/insights": "-/insights", | |
| 80 | }; | |
| 81 | ||
| Billing: credits with a kind and expiry, discounts instead of comped, and safer charging | 82 | /** The page a request is for: a click's data request ends in `.data`. */ |
| 83 | export function pagePath(pathname: string): string { | |
| 84 | return pathname.replace(/\.data$/, "").replace(/\/+$/, "") || "/"; | |
| 85 | } | |
| 86 | ||
| Navigation: the workspace's own page with tabs, pins and recent in the sidebar | 87 | /** |
| 88 | * Where an old address of a workspace's pages is now, keeping its query; | |
| 89 | * null when it has not moved. `/<workspace>?tab=projects` and the like | |
| 90 | * open that tab. | |
| 91 | */ | |
| 92 | export function workspaceRedirect(pathname: string, search = ""): string | null { | |
| Billing: credits with a kind and expiry, discounts instead of comped, and safer charging | 93 | // A click asks for the page's data at `<path>.data?_routes=…`; the page |
| 94 | // is the same one a full load asks for. | |
| 95 | const params = new URLSearchParams(search); | |
| 96 | params.delete("_routes"); | |
| 97 | search = params.toString(); | |
| 98 | const parts = pagePath(pathname).split("/").filter(Boolean); | |
| Navigation: the workspace's own page with tabs, pins and recent in the sidebar | 99 | const slug = parts[0]; |
| 100 | if (!slug) return null; | |
| 101 | if (parts.length === 1 && params.has("tab")) { | |
| 102 | const to = TAB_WORDS[(params.get("tab") ?? "").toLowerCase()]; | |
| 103 | if (to === undefined) return null; | |
| 104 | params.delete("tab"); | |
| 105 | const rest = params.toString(); | |
| 106 | return `/${slug}${to ? `/${to}` : ""}${rest ? `?${rest}` : ""}`; | |
| 107 | } | |
| 108 | if (parts[1] !== "-") return null; | |
| 109 | const to = MOVED[parts.slice(2).join("/")]; | |
| 110 | if (to === undefined) return null; | |
| 111 | return `/${slug}${to ? `/${to}` : ""}${search && search !== "?" ? (search.startsWith("?") ? search : `?${search}`) : ""}`; | |
| 112 | } |