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.
| Projects: pinned and recent projects, per person per workspace | 1 | /** |
| 2 | * Pinned and recent projects: what each person keeps at hand in a | |
| 3 | * workspace. Pins are theirs to order, up to `MAX_PINS` a workspace; | |
| 4 | * recent is the projects they opened last that they have not pinned. | |
| 5 | * Pure, so the rules are tested apart from the service. | |
| 6 | */ | |
| 7 | ||
| 8 | /** Pins a person may have in one workspace: what its sidebar shows. */ | |
| 9 | export const MAX_PINS = 8; | |
| 10 | /** Recent projects shown, after the pins. */ | |
| 11 | export const MAX_RECENT = 5; | |
| 12 | /** Visits kept per person, across their workspaces. */ | |
| 13 | export const MAX_VISITS = 100; | |
| 14 | ||
| 15 | /** | |
| 16 | * The pins' order after pinning `id`: at `position` (0 first) when given, | |
| 17 | * else at the end. Pinning one already pinned moves it. Null when the | |
| 18 | * workspace's pins are full. | |
| 19 | */ | |
| 20 | export function placePin(order: string[], id: string, position?: number | null): string[] | null { | |
| 21 | const rest = order.filter((pinned) => pinned !== id); | |
| 22 | if (rest.length >= MAX_PINS) return null; | |
| 23 | const at = position == null || !Number.isFinite(position) ? rest.length : Math.max(0, Math.min(rest.length, Math.trunc(position))); | |
| 24 | return [...rest.slice(0, at), id, ...rest.slice(at)]; | |
| 25 | } | |
| 26 | ||
| 27 | /** | |
| 28 | * The pins in the order asked for. Every pin is named once and nothing | |
| 29 | * else, or the reason it cannot be. | |
| 30 | */ | |
| 31 | export function reorderPins(order: string[], wanted: string[]): { ok: true; order: string[] } | { ok: false; message: string } { | |
| 32 | const pinned = new Set(order); | |
| 33 | const named = new Set(wanted); | |
| 34 | if (named.size !== wanted.length) return { ok: false, message: "Name each pinned project once." }; | |
| 35 | const stranger = wanted.find((id) => !pinned.has(id)); | |
| 36 | if (stranger) return { ok: false, message: `${stranger} is not pinned.` }; | |
| 37 | if (named.size !== pinned.size) return { ok: false, message: "Name every pinned project, in the order you want them." }; | |
| 38 | return { ok: true, order: [...wanted] }; | |
| 39 | } | |
| 40 | ||
| 41 | /** The recent projects to show: latest first, leaving out the pinned, at most `MAX_RECENT`. */ | |
| 42 | export function recentAfterPins<T extends { id: string }>(visited: T[], pinned: Iterable<string>, limit = MAX_RECENT): T[] { | |
| 43 | const skip = new Set(pinned); | |
| 44 | return visited.filter((project) => !skip.has(project.id)).slice(0, limit); | |
| 45 | } |