| 1 | /** |
| 2 | * People on the site: the directory, profiles, and teams. People only. |
| 3 | * Types only from the contracts, so this file runs under `node --test`. |
| 4 | */ |
| 5 | import type { DirectoryPerson, DirectoryTeam, PeopleDirectory, TeamLead } from "@g1t/contracts"; |
| 6 | |
| 7 | /** Where a person's profile is. */ |
| 8 | export function personPath(workspace: string, username: string): string { |
| 9 | return `/${workspace}/-/people/${username}`; |
| 10 | } |
| 11 | |
| 12 | /** The teams a person is on. */ |
| 13 | export function teamsOfPerson<T extends Pick<DirectoryTeam, "people">>(teams: readonly T[], username: string): T[] { |
| 14 | return teams.filter((team) => team.people.some((person) => person.username === username)); |
| 15 | } |
| 16 | |
| 17 | /** Whether `lead` is this person. An old agent lead (agents no longer exist) is nobody's. */ |
| 18 | export function leads(lead: TeamLead | null, who: { username: string }): boolean { |
| 19 | return !!lead && lead.kind === "user" && lead.username === who.username; |
| 20 | } |
| 21 | |
| 22 | /** A person's name as pages show it. */ |
| 23 | export function personName(person: Pick<DirectoryPerson, "username" | "name" | "display_username">): string { |
| 24 | return person.name?.trim() || person.display_username || person.username; |
| 25 | } |
| 26 | |
| 27 | /** Whether every word of `query` is somewhere in `fields`. */ |
| 28 | export function matches(query: string, fields: readonly (string | null | undefined)[]): boolean { |
| 29 | const words = query.toLowerCase().split(/\s+/).filter(Boolean); |
| 30 | if (!words.length) return true; |
| 31 | const text = fields.filter(Boolean).join(" ").toLowerCase(); |
| 32 | return words.every((word) => text.includes(word)); |
| 33 | } |
| 34 | |
| 35 | /** One card in the directory: a person and their teams. */ |
| 36 | export type DirectoryEntry = { person: DirectoryPerson; teams: DirectoryTeam[] }; |
| 37 | |
| 38 | /** |
| 39 | * The people matching `query`, in the directory's order (by name). A |
| 40 | * person matches on their name, username, title, teams, what they own, |
| 41 | * where they are and their bio. |
| 42 | */ |
| 43 | export function directoryEntries(directory: PeopleDirectory, query: string): DirectoryEntry[] { |
| 44 | const out: DirectoryEntry[] = []; |
| 45 | for (const person of directory.people) { |
| 46 | const teams = teamsOfPerson(directory.teams, person.username); |
| 47 | const fields = [person.username, person.name, person.title, person.location, person.bio, ...person.owns, ...teams.map((team) => team.name), person.role === "owner" ? "owner" : null]; |
| 48 | if (matches(query, fields)) out.push({ person, teams }); |
| 49 | } |
| 50 | return out; |
| 51 | } |
| 52 | |
| 53 | /** What someone owns, from a form's text: one per line or comma. */ |
| 54 | export function ownsFromText(text: string): string[] { |
| 55 | return text |
| 56 | .split(/[\n,]+/) |
| 57 | .map((phrase) => phrase.trim()) |
| 58 | .filter(Boolean); |
| 59 | } |
| 60 | |
| 61 | /** The access to Code a member has, in words: their role, the base permission, and the teams that add to it. */ |
| 62 | export function codeAccessWords( |
| 63 | person: Pick<DirectoryPerson, "role">, |
| 64 | base: PeopleDirectory["base_permission"], |
| 65 | teams: readonly Pick<DirectoryTeam, "name" | "repos_count">[], |
| 66 | ): { summary: string; through: string[] } { |
| 67 | const summary = |
| 68 | person.role === "owner" |
| 69 | ? "Admin on every repository, as an owner" |
| 70 | : base === "none" |
| 71 | ? "Only repositories they're given a role on" |
| 72 | : `${base[0]!.toUpperCase()}${base.slice(1)} on every repository, the workspace's base permission`; |
| 73 | const through = teams |
| 74 | .filter((team) => team.repos_count > 0) |
| 75 | .map((team) => `Roles on ${team.repos_count} ${team.repos_count === 1 ? "repository" : "repositories"} through ${team.name}`); |
| 76 | return { summary, through }; |
| 77 | } |