Skip to content
144 linesCodeBlameRaw

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.

Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1import { Users } from "lucide-react";
2import { Form, Link, data, useSearchParams } from "react-router";
3
4import type { TeamRole } from "@g1t/contracts";
5
6import type { Route } from "./+types/members";
7import { useTeam } from "../../../components/teams";
8import { Avatar, ErrorText, Field, Input, SubmitButton } from "../../../components/ui";
9import { Badge } from "../../../components/ui/badge";
10import { CheckboxOption } from "../../../components/ui/checkbox";
11import { Switch } from "../../../components/ui/switch";
12import { teamPath } from "../../../lib/teams";
13import { identity } from "../../../lib/services.server";
14import { assertSameOrigin, getViewer, requireUser, roleIn, unwrap } from "../../../lib/session.server";
Merge commit authors and hovercards: commits show their accounts, every name opens a card (repos 0016)15import { UserCard } from "../../../components/user-card";
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar16
17export async function loader({ params, context, request }: Route.LoaderArgs) {
18 const viewer = getViewer(context);
19 if (!roleIn(viewer, params.owner)) throw data(null, { status: 404 });
20 const children = new URL(request.url).searchParams.get("children") === "1";
21 const members = unwrap(await identity.teamMembers(viewer, params.owner, params.team, children));
22 return { members, children, me: viewer?.username ?? null };
23}
24
25export async function action({ request, params, context }: Route.ActionArgs) {
26 assertSameOrigin(request);
27 const user = requireUser(context, request);
28 const form = await request.formData();
29 const intent = String(form.get("intent") ?? "");
30 const username = String(form.get("username") ?? "").trim().replace(/^@/, "");
31 if (!username) return { intent, username, error: "Enter a username." };
32 if (intent === "remove") {
33 const removed = await identity.removeTeamMember(user, params.owner, params.team, username);
34 return removed.ok ? { intent, username, error: null } : { intent, username, error: removed.error.message };
35 }
36 const role: TeamRole = form.get("role") === "maintainer" ? "maintainer" : "member";
37 const set = await identity.setTeamMember(user, params.owner, params.team, username, role);
38 return set.ok ? { intent, username, error: null } : { intent, username, error: set.error.message };
39}
40
41export default function TeamMembers({ loaderData, actionData }: Route.ComponentProps) {
42 const team = useTeam();
43 const { members, children, me } = loaderData;
44 const [, setParams] = useSearchParams();
45 const manage = team.can_manage;
46 const rowError = (username: string) =>
47 actionData && actionData.intent !== "add" && actionData.username === username ? actionData.error : null;
48
49 return (
50 <div className="max-w-3xl space-y-6">
51 {team.child_teams_count > 0 && (
52 <label className="flex items-center gap-2.5 text-sm text-muted">
53 <Switch
54 size="sm"
55 checked={children}
56 onCheckedChange={(on) =>
57 setParams(on ? { children: "1" } : {}, { replace: true, preventScrollReset: true })
58 }
59 />
60 Include the people of child teams
61 </label>
62 )}
63
64 {members.length === 0 ? (
65 <div className="rounded-xl border border-dashed border-line px-6 py-10 text-center">
66 <Users size={18} className="mx-auto text-faint" />
67 <p className="mt-2 text-sm font-medium">No one is in {team.name} yet</p>
68 {manage && <p className="mt-1 text-sm text-muted">Add members of {team.workspace} below.</p>}
69 </div>
70 ) : (
71 <ul className="divide-y divide-line rounded-xl border border-line">
72 {members.map((member) => {
73 const self = member.username === me;
74 return (
75 <li key={member.username} className="px-4 py-3">
76 <div className="flex flex-wrap items-center gap-x-3 gap-y-2">
77 <Avatar name={member.username} image={member.avatar} size={28} />
78 <div className="min-w-0 grow basis-32 truncate">
Merge commit authors and hovercards: commits show their accounts, every name opens a card (repos 0016)79 <UserCard username={member.username}>
80 <Link to={`/u/${member.username}`} className="font-mono text-sm hover:text-accent">
81 {member.username}
82 </Link>
83 </UserCard>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar84 {member.name && <span className="ml-2 hidden text-sm text-muted sm:inline">{member.name}</span>}
85 </div>
86 {member.via ? (
87 <Link to={teamPath(team.workspace, member.via)}>
88 <Badge>via {member.via}</Badge>
89 </Link>
90 ) : member.role === "maintainer" ? (
91 <Badge tone="accent">Maintainer</Badge>
92 ) : (
93 <Badge>Member</Badge>
94 )}
95 {!member.via && (manage || self) && (
96 <span className="flex items-center gap-1">
97 {manage && (
98 <Form method="post">
99 <input type="hidden" name="intent" value="role" />
100 <input type="hidden" name="username" value={member.username} />
101 <input type="hidden" name="role" value={member.role === "maintainer" ? "member" : "maintainer"} />
102 <SubmitButton variant="quiet" match={{ intent: "role", username: member.username }} pending="Saving…">
103 {member.role === "maintainer" ? "Make member" : "Make maintainer"}
104 </SubmitButton>
105 </Form>
106 )}
107 <Form method="post">
108 <input type="hidden" name="intent" value="remove" />
109 <input type="hidden" name="username" value={member.username} />
110 <SubmitButton variant="quiet" match={{ intent: "remove", username: member.username }} pending="Removing…">
111 {self ? "Leave" : "Remove"}
112 </SubmitButton>
113 </Form>
114 </span>
115 )}
116 </div>
117 <ErrorText>{rowError(member.username)}</ErrorText>
118 </li>
119 );
120 })}
121 </ul>
122 )}
123
124 {manage && (
125 <Form method="post" key={members.length} className="flex flex-col gap-3 sm:flex-row sm:items-start">
126 <input type="hidden" name="intent" value="add" />
127 <div className="grow">
128 <Field label="Add a member" hint={`A member of ${team.workspace}, by username. Maintainers manage the team's people and settings.`}>
129 <Input name="username" required maxLength={39} placeholder="username" />
130 </Field>
131 </div>
132 <CheckboxOption name="role" value="maintainer" label="Maintainer" className="sm:pt-[2.1rem]" />
133
134 <div className="sm:pt-[1.625rem]">
135 <SubmitButton match={{ intent: "add" }} pending="Adding…">
136 Add
137 </SubmitButton>
138 </div>
139 </Form>
140 )}
141 {actionData?.intent === "add" && <ErrorText>{actionData.error}</ErrorText>}
142 </div>
143 );
144}

This file's history is long; its oldest lines are credited to the oldest commit read.