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. */ | |
| People and teams are front and centre: one directory of people and agents with presence, local time, titles, teams and what each owns; profiles with manager and reports and the agents they work with; an org chart with each team's agents beside the person who leads it; and teams of any mix, with a lead, a channel, a budget agents keep to and the agents on them. Every agent is told its teams each turn (who leads, who owns what, who's around and who to page), and the team page shows exactly what. Member management is Members and invites; the people and teams guide says how. | 116 | export function teamCounts(team: Pick<Team, "members_count" | "repos_count" | "child_teams_count">, agents = 0): string { |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 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"), | |
| People and teams are front and centre: one directory of people and agents with presence, local time, titles, teams and what each owns; profiles with manager and reports and the agents they work with; an org chart with each team's agents beside the person who leads it; and teams of any mix, with a lead, a channel, a budget agents keep to and the agents on them. Every agent is told its teams each turn (who leads, who owns what, who's around and who to page), and the team page shows exactly what. Member management is Members and invites; the people and teams guide says how. | 120 | part(agents, "agent", "agents"), |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 121 | part(team.repos_count, "repository", "repositories"), |
| 122 | part(team.child_teams_count, "child team", "child teams"), | |
| 123 | ].filter(Boolean); | |
| 124 | return parts.length ? parts.join(" · ") : "No members yet"; | |
| 125 | } |