| 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 | } |