Skip to content

g1t/services/projects/src/pins.ts

45 lines2,159 bytesCodeBlame

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 workspace1/**
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. */
9export const MAX_PINS = 8;
10/** Recent projects shown, after the pins. */
11export const MAX_RECENT = 5;
12/** Visits kept per person, across their workspaces. */
13export 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 */
20export 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 */
31export 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`. */
42export 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}