Give visited workspaces their own project navigation
3 files+52−250/3 viewed
| 1118 | 1118 | <Rule /> | |
| 1119 | 1119 | </> | |
| 1120 | 1120 | )} | |
| 1121 | − | <ModeHeader title={browsing ? "Explore" : "Code"} /> | |
| 1121 | + | <ModeHeader title={browsing?.owner?.kind === "workspace" ? "Workspace" : browsing ? "Explore" : "Code"} /> | |
| 1122 | 1122 | <Drill trail={trail} /> | |
| 1123 | 1123 | </div> | |
| 1124 | 1124 | ); | |
| ⋯ | |||
| 1176 | 1176 | const stars = atMine && query.get("tab") === "stars"; | |
| 1177 | 1177 | const shown = useBrowsedOwner(owner); | |
| 1178 | 1178 | 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 | + | } | |
| 1179 | 1217 | return ( | |
| 1180 | 1218 | <nav aria-label="Explore" className={PANEL}> | |
| 1181 | 1219 | <NavRows> | |
| ⋯ | |||
| 1202 | 1240 | <> | |
| 1203 | 1241 | <Rule /> | |
| 1204 | 1242 | <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" && ( | |
| 1219 | 1244 | <> | |
| 1220 | 1245 | <SidebarLink | |
| 1221 | 1246 | to={`/u/${owner.name}`} | |
| 12 | 12 | import { redirectIfRenamed } from "../../lib/renamed.server"; | |
| 13 | 13 | import { rememberWorkspace } from "../../lib/workspace-choice"; | |
| 14 | 14 | import { workspacePage, workspaceRedirect } from "../../lib/workspace-nav"; | |
| 15 | + | import { shortcutOf, workspaceProjects } from "../../lib/workspace-projects.server"; | |
| 15 | 16 | import { WorkspacePageSkeleton } from "../../components/workspace-skeletons"; | |
| 16 | 17 | import { identity } from "../../lib/services.server"; | |
| 17 | 18 | import { getViewer, roleIn } from "../../lib/session.server"; | |
| ⋯ | |||
| 35 | 36 | } | |
| 36 | 37 | const role = roleIn(viewer, workspace.slug); | |
| 37 | 38 | // 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 | + | } | |
| 39 | 43 | const secure = new URL(request.url).protocol === "https:"; | |
| 40 | 44 | const headers = new Headers({ "Set-Cookie": rememberWorkspace(workspace.slug, secure) }); | |
| 41 | 45 | // Just joined with an invite: welcomed once (routes/invite.tsx). | |
| ⋯ | |||
| 203 | 207 | <h1 className="truncate text-2xl font-semibold tracking-tight"> | |
| 204 | 208 | {workspace.name} | |
| 205 | 209 | </h1> | |
| 210 | + | <Badge size="md">Workspace</Badge> | |
| 206 | 211 | {role && <Badge size="md">{role}</Badge>} | |
| 207 | 212 | </div> | |
| 208 | 213 | <p className="font-mono text-sm text-muted">g1t.sh/{workspace.slug}</p> | |
| ⋯ | |||
| 213 | 218 | New project | |
| 214 | 219 | </ButtonLink> | |
| 215 | 220 | ) : ( | |
| 216 | − | // A visitor's sidebar is not this workspace's: its packages | |
| 217 | − | // are a link here, as its projects are below. | |
| 218 | 221 | <ButtonLink to={`/${workspace.slug}/-/packages`} variant="outline" prefetch="intent"> | |
| 219 | 222 | <Package size={15} /> | |
| 220 | 223 | Packages | |
| 38 | 38 | ||
| 39 | 39 | export async function loader({ params, context, request }: Route.LoaderArgs) { | |
| 40 | 40 | 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; | |
| 44 | 42 | const url = new URL(request.url); | |
| 45 | 43 | const type = url.searchParams.get("type"); | |
| 46 | 44 | const ecosystem = type && (ECOSYSTEMS as readonly string[]).includes(type) ? (type as Ecosystem) : null; | |
| 47 | 45 | const query = url.searchParams.get("q")?.trim() || null; | |
| 48 | 46 | const visibility = pick<VisibilityFilter>(url.searchParams.get("visibility"), ["all", "public", "private"], "all"); | |
| 49 | 47 | 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"; | |
| 51 | 49 | // Deleted packages are listed for those who administer them, and only | |
| 52 | 50 | // then is there a Deleted packages view. | |
| 53 | 51 | const [found, deletedFound] = await Promise.all([ | |
| 54 | 52 | packages.list(params.owner, viewer, { ecosystem, query }), | |
| 55 | − | packages.deleted(params.owner, viewer), | |
| 53 | + | member ? packages.deleted(params.owner, viewer) : null, | |
| 56 | 54 | ]); | |
| 57 | 55 | const listed = unwrap(found); | |
| 58 | − | const deleted = deletedFound.ok ? deletedFound.value : []; | |
| 56 | + | const deleted = deletedFound?.ok ? deletedFound.value : []; | |
| 59 | 57 | // Whether the workspace has any at all decides between a filtered-out | |
| 60 | 58 | // list and the first-package page. | |
| 61 | 59 | const any = listed.length > 0 || deleted.length > 0 || ecosystem != null || query != null; | |
| 62 | 60 | return { | |
| 61 | + | member, | |
| 63 | 62 | list: arrange(listed, visibility, sort), | |
| 64 | 63 | deleted, | |
| 65 | 64 | view, | |
| ⋯ | |||
| 104 | 103 | /** The packages a workspace publishes: filtered, sorted, or, with none yet, how to start. */ | |
| 105 | 104 | export default function Packages({ loaderData, actionData }: Route.ComponentProps) { | |
| 106 | 105 | 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>; | |
| 108 | 107 | if (view === "deleted") return <DeletedPackages workspace={workspace} deleted={deleted} outcome={actionData as Outcome | undefined} />; | |
| 109 | 108 | const filters: Filters = { type: ecosystem, q: query, visibility, sort }; | |
| 110 | 109 | const filtered = ecosystem != null || query != null || visibility !== "all"; | |
Comments
Sign in to comment.