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 bar | 1 | /** |
| 2 | * Teams on the site: finding them, and reading their forms. Types only from | |
| 3 | * the contracts, so this file runs under `node --test`. | |
| 4 | */ | |
| 5 | import type { NewTeam, ReviewAlgorithm, ReviewAssignment, Team, TeamChanges, TeamVisibility } from "@g1t/contracts"; | |
| 6 | ||
| 7 | /** The most people review assignment picks; the same as the contracts' `MAX_ASSIGNED`. */ | |
| 8 | const MAX_ASSIGNED = 10; | |
| 9 | ||
| 10 | /** Where a team's pages are. */ | |
| 11 | export function teamPath(workspace: string, slug: string, tab?: "teams" | "repositories" | "settings"): string { | |
| 12 | return `/${workspace}/-/teams/${slug}${tab ? `/${tab}` : ""}`; | |
| 13 | } | |
| 14 | ||
| 15 | /** The teams whose name, slug or description has every word of `query`, in their order. */ | |
| 16 | export function filterTeams<T extends Pick<Team, "name" | "slug" | "description">>(teams: T[], query: string): T[] { | |
| 17 | const words = query.toLowerCase().split(/\s+/).filter(Boolean); | |
| 18 | if (words.length === 0) return teams; | |
| 19 | return teams.filter((team) => { | |
| 20 | const text = `${team.name} ${team.slug} ${team.description ?? ""}`.toLowerCase(); | |
| 21 | return words.every((word) => text.includes(word)); | |
| 22 | }); | |
| 23 | } | |
| 24 | ||
| 25 | /** The viewer's teams, then the rest, keeping the order given. */ | |
| 26 | export function splitTeams<T extends Pick<Team, "viewer_role">>(teams: T[]): { mine: T[]; others: T[] } { | |
| 27 | return { | |
| 28 | mine: teams.filter((team) => team.viewer_role != null), | |
| 29 | others: teams.filter((team) => team.viewer_role == null), | |
| 30 | }; | |
| 31 | } | |
| 32 | ||
| 33 | /** The teams `team` could go under: visible, not itself, and not one of its own children. */ | |
| 34 | export function parentChoices<T extends Pick<Team, "slug" | "visibility" | "parent">>(teams: T[], team: string | null): T[] { | |
| 35 | if (!team) return teams.filter((other) => other.visibility === "visible"); | |
| 36 | // Everything under `team`, by following parents. | |
| 37 | const below = new Set<string>([team]); | |
| 38 | let grew = true; | |
| 39 | while (grew) { | |
| 40 | grew = false; | |
| 41 | for (const other of teams) { | |
| 42 | if (other.parent && below.has(other.parent.slug) && !below.has(other.slug)) { | |
| 43 | below.add(other.slug); | |
| 44 | grew = true; | |
| 45 | } | |
| 46 | } | |
| 47 | } | |
| 48 | return teams.filter((other) => other.visibility === "visible" && !below.has(other.slug)); | |
| 49 | } | |
| 50 | ||
| 51 | const text = (form: FormData, key: string) => String(form.get(key) ?? "").trim(); | |
| 52 | ||
| 53 | function visibilityOf(value: string): TeamVisibility | undefined { | |
| 54 | return value === "secret" || value === "visible" ? value : undefined; | |
| 55 | } | |
| 56 | ||
| 57 | /** A new team from the form on the New team page. */ | |
| 58 | export function newTeamFromForm(form: FormData): NewTeam { | |
| 59 | const members = text(form, "members") | |
| 60 | .split(/[\s,]+/) | |
| 61 | .map((name) => name.replace(/^@/, "")) | |
| 62 | .filter(Boolean); | |
| 63 | return { | |
| 64 | name: text(form, "name"), | |
| 65 | slug: text(form, "slug") || null, | |
| 66 | description: text(form, "description") || null, | |
| 67 | visibility: visibilityOf(text(form, "visibility")) ?? "visible", | |
| 68 | parent: text(form, "parent") || null, | |
| 69 | members, | |
| 70 | }; | |
| 71 | } | |
| 72 | ||
| 73 | /** What the team's settings form changes: only the fields it has. */ | |
| 74 | export function teamChangesFromForm(form: FormData): TeamChanges { | |
| 75 | const changes: TeamChanges = {}; | |
| 76 | if (form.has("name")) changes.name = text(form, "name"); | |
| 77 | if (form.has("slug")) changes.slug = text(form, "slug"); | |
| 78 | if (form.has("description")) changes.description = text(form, "description"); | |
| 79 | const visibility = visibilityOf(text(form, "visibility")); | |
| 80 | if (visibility) changes.visibility = visibility; | |
| 81 | // "" takes it out from under its parent. | |
| 82 | if (form.has("parent")) changes.parent = text(form, "parent"); | |
| 83 | if (form.has("notify-shown")) changes.notify = form.get("notify") != null; | |
| 84 | return changes; | |
| 85 | } | |
| 86 | ||
| 87 | function count(value: string, fallback: number, low: number, high: number): number { | |
| 88 | const number = Number.parseInt(value, 10); | |
| 89 | if (!Number.isFinite(number)) return fallback; | |
| 90 | return Math.min(high, Math.max(low, number)); | |
| 91 | } | |
| 92 | ||
| 93 | /** Review assignment from its form, within bounds; `current` fills what the form leaves out. */ | |
| 94 | export function reviewAssignmentFromForm(form: FormData, current: ReviewAssignment): ReviewAssignment { | |
| 95 | const algorithm = text(form, "algorithm"); | |
| 96 | return { | |
| 97 | enabled: form.get("enabled") != null, | |
| 98 | algorithm: (algorithm === "round_robin" || algorithm === "load_balance" ? algorithm : current.algorithm) as ReviewAlgorithm, | |
| 99 | count: count(text(form, "count"), current.count, 1, MAX_ASSIGNED), | |
| 100 | skip_busy: form.get("skip_busy") != null, | |
| 101 | busy_at: count(text(form, "busy_at"), current.busy_at, 1, 100), | |
| 102 | include_child_teams: form.get("include_child_teams") != null, | |
| 103 | excluded: [ | |
| 104 | ...new Set( | |
| 105 | text(form, "excluded") | |
| 106 | .split(/[\s,]+/) | |
| 107 | .map((name) => name.replace(/^@/, "").toLowerCase()) | |
| 108 | .filter(Boolean), | |
| 109 | ), | |
| 110 | ], | |
| 111 | notify_team: form.get("notify_team") != null, | |
| 112 | }; | |
| 113 | } | |
| 114 | ||
| 115 | /** "3 members · 2 repositories · 1 child team", leaving out what is none. */ | |
| 116 | export function teamCounts(team: Pick<Team, "members_count" | "repos_count" | "child_teams_count">): string { | |
| 117 | const part = (n: number, one: string, many: string) => (n === 0 ? null : `${n} ${n === 1 ? one : many}`); | |
| 118 | const parts = [ | |
| 119 | part(team.members_count, "member", "members"), | |
| 120 | part(team.repos_count, "repository", "repositories"), | |
| 121 | part(team.child_teams_count, "child team", "child teams"), | |
| 122 | ].filter(Boolean); | |
| 123 | return parts.length ? parts.join(" · ") : "No members yet"; | |
| 124 | } |