Skip to content

g1t/packages/contracts/src/teams.ts

164 lines5,677 bytesCodeBlame
1/**
2 * Teams: groups of a workspace's members, given roles on repositories
3 * together, mentioned together and asked to review together. Kept by the
4 * identity service; mirrors `crates/contracts/src/teams.rs`.
5 */
6import type { RepoRole } from "./access";
7import type { User } from "./identity";
8import type { Result } from "./result";
9
10export type TeamVisibility = "visible" | "secret";
11export type TeamRole = "member" | "maintainer";
12export type ReviewAlgorithm = "round_robin" | "load_balance";
13
14export const TEAM_VISIBILITY_LABELS: Record<TeamVisibility, string> = {
15 visible: "Visible",
16 secret: "Secret",
17};
18
19export const TEAM_VISIBILITY_SUMMARIES: Record<TeamVisibility, string> = {
20 visible: "Every member of the workspace can see it and mention it.",
21 secret: "Only its own people and the workspace's owners can see it. Secret teams cannot be nested.",
22};
23
24export const REVIEW_ALGORITHM_LABELS: Record<ReviewAlgorithm, string> = {
25 round_robin: "Round robin",
26 load_balance: "Load balance",
27};
28
29export const REVIEW_ALGORITHM_SUMMARIES: Record<ReviewAlgorithm, string> = {
30 round_robin: "Whoever this team asked least recently goes first.",
31 load_balance: "Whoever has the fewest pull requests waiting on their review goes first.",
32};
33
34/** The most people review assignment picks for one request. */
35export const MAX_ASSIGNED = 10;
36
37export type ReviewAssignment = {
38 enabled: boolean;
39 algorithm: ReviewAlgorithm;
40 count: number;
41 skip_busy: boolean;
42 busy_at: number;
43 include_child_teams: boolean;
44 excluded: string[];
45 notify_team: boolean;
46};
47
48export const DEFAULT_REVIEW_ASSIGNMENT: ReviewAssignment = {
49 enabled: false,
50 algorithm: "round_robin",
51 count: 1,
52 skip_busy: false,
53 busy_at: 5,
54 include_child_teams: false,
55 excluded: [],
56 notify_team: false,
57};
58
59export type TeamRef = { slug: string; name: string };
60
61export type Team = {
62 id: string;
63 workspace: string;
64 slug: string;
65 name: string;
66 description: string | null;
67 visibility: TeamVisibility;
68 parent: TeamRef | null;
69 notify: boolean;
70 review_assignment: ReviewAssignment;
71 members_count: number;
72 repos_count: number;
73 child_teams_count: number;
74 viewer_role: TeamRole | null;
75 can_manage: boolean;
76 created_at: string;
77 updated_at: string;
78};
79
80export type TeamMember = {
81 username: string;
82 name: string | null;
83 avatar: string | null;
84 role: TeamRole;
85 /** The child team they are in, when listed through one. */
86 via: string | null;
87};
88
89export type TeamRepo = {
90 /** `workspace/name`. */
91 repo: string;
92 repo_id: string;
93 role: RepoRole;
94 /** The parent team it comes from, when inherited. */
95 inherited_from: string | null;
96};
97
98/** A team with a role on a repository, as its Access settings list it. */
99export type RepoTeam = {
100 slug: string;
101 name: string;
102 role: RepoRole;
103 members_count: number;
104 visibility: TeamVisibility;
105};
106
107export type NewTeam = {
108 name: string;
109 slug?: string | null;
110 description?: string | null;
111 visibility?: TeamVisibility | null;
112 parent?: string | null;
113 notify?: boolean | null;
114 members?: string[];
115};
116
117/** What changes; the rest stays. `parent: ""` takes the team out from under its parent. */
118export type TeamChanges = {
119 name?: string;
120 slug?: string;
121 description?: string;
122 visibility?: TeamVisibility;
123 parent?: string;
124 notify?: boolean;
125 review_assignment?: ReviewAssignment;
126};
127
128/** `@acme/backend`, the way a team is mentioned. */
129export function teamHandle(team: { workspace: string; slug: string }): string {
130 return `@${team.workspace}/${team.slug}`;
131}
132
133/** A team's slug from its name, as identity makes it. Null when nothing is left. */
134export function teamSlug(name: string): string | null {
135 let slug = "";
136 for (const c of name.trim()) {
137 if (/[A-Za-z0-9]/.test(c)) slug += c.toLowerCase();
138 else if (slug && !slug.endsWith("-")) slug += "-";
139 }
140 slug = slug.replace(/-+$/, "").slice(0, 60).replace(/-+$/, "");
141 return slug ? slug : null;
142}
143
144/** One person's teams in a workspace, for the Members page. */
145export type MemberTeams = { username: string; teams: TeamRef[] };
146
147/** Identity's team methods. */
148export interface TeamsClient {
149 listTeams(viewer: User | null, workspace: string, query?: string | null): Promise<Result<Team[]>>;
150 getTeam(viewer: User | null, workspace: string, team: string): Promise<Result<Team>>;
151 createTeam(actor: User, workspace: string, team: NewTeam): Promise<Result<Team>>;
152 updateTeam(actor: User, workspace: string, team: string, changes: TeamChanges): Promise<Result<Team>>;
153 deleteTeam(actor: User, workspace: string, team: string): Promise<Result<boolean>>;
154 teamMembers(viewer: User | null, workspace: string, team: string, includeChildTeams?: boolean): Promise<Result<TeamMember[]>>;
155 setTeamMember(actor: User, workspace: string, team: string, username: string, role: TeamRole): Promise<Result<TeamMember>>;
156 removeTeamMember(actor: User, workspace: string, team: string, username: string): Promise<Result<boolean>>;
157 childTeams(viewer: User | null, workspace: string, team: string): Promise<Result<Team[]>>;
158 teamRepos(viewer: User | null, workspace: string, team: string): Promise<Result<TeamRepo[]>>;
159 setTeamRepo(actor: User, workspace: string, team: string, owner: string, name: string, role: RepoRole): Promise<Result<TeamRepo>>;
160 removeTeamRepo(actor: User, workspace: string, team: string, owner: string, name: string): Promise<Result<boolean>>;
161 userTeams(viewer: User | null, workspace: string, username: string): Promise<Result<Team[]>>;
162 /** Each member's teams the viewer can see. Members only. */
163 teamMemberships(viewer: User | null, workspace: string): Promise<Result<MemberTeams[]>>;
164}