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.
| Navigation: the workspace's own page with tabs, pins and recent in the sidebar | 1 | /** |
| 2 | * Pinned and recent projects in the sidebar: the viewer's pins in the | |
| 3 | * workspace (up to eight, in their order), then what they opened last that | |
| 4 | * they have not pinned (up to five), then All projects. The projects | |
| 5 | * service keeps both; opening a project records it after the page is sent. | |
| 6 | */ | |
| 7 | ||
| 8 | /** A project as the sidebar lists it. */ | |
| 9 | export type ShortcutProject = { namespace: string; name: string; title?: string; isPrivate: boolean }; | |
| 10 | ||
| 11 | export const MAX_PINS = 8; | |
| 12 | export const MAX_RECENT = 5; | |
| 13 | ||
| 14 | const same = (a: { namespace: string; name: string }, b: { namespace: string; name: string }) => | |
| 15 | a.namespace.toLowerCase() === b.namespace.toLowerCase() && a.name.toLowerCase() === b.name.toLowerCase(); | |
| 16 | ||
| 17 | /** | |
| 18 | * Recent, with the project being looked at first when it is the | |
| 19 | * workspace's and not pinned: it was opened just now, before the projects | |
| 20 | * service has heard. | |
| 21 | */ | |
| 22 | export function recentWith( | |
| 23 | recent: ShortcutProject[], | |
| 24 | pinned: ShortcutProject[], | |
| 25 | current: ShortcutProject | null, | |
| 26 | workspace: string | null, | |
| 27 | ): ShortcutProject[] { | |
| 28 | const here = current && workspace && current.namespace.toLowerCase() === workspace.toLowerCase() && !pinned.some((p) => same(p, current)) ? current : null; | |
| 29 | const rest = recent.filter((project) => !pinned.some((p) => same(p, project)) && !(here && same(project, here))); | |
| 30 | return [...(here ? [here] : []), ...rest].slice(0, MAX_RECENT); | |
| 31 | } | |
| 32 | ||
| 33 | /** One change to the viewer's pins, as a form posts it. */ | |
| 34 | export type PinChange = | |
| 35 | | { intent: "pin"; slug: string; position: number | null } | |
| 36 | | { intent: "unpin"; slug: string } | |
| 37 | | { intent: "reorder"; slugs: string[] }; | |
| 38 | ||
| 39 | /** What a pin form asks for; null when it asks for nothing that makes sense. */ | |
| 40 | export function pinFromForm(form: FormData): PinChange | null { | |
| 41 | const intent = form.get("intent"); | |
| 42 | const slug = String(form.get("slug") ?? "").trim().toLowerCase(); | |
| 43 | if (intent === "reorder") { | |
| 44 | const slugs = form.getAll("slug").map((value) => String(value).trim().toLowerCase()).filter(Boolean); | |
| 45 | return slugs.length > 0 && new Set(slugs).size === slugs.length ? { intent, slugs } : null; | |
| 46 | } | |
| 47 | if (!slug) return null; | |
| 48 | if (intent === "unpin") return { intent, slug }; | |
| 49 | if (intent !== "pin") return null; | |
| 50 | const position = Number.parseInt(String(form.get("position") ?? ""), 10); | |
| 51 | return { intent, slug, position: Number.isFinite(position) && position >= 0 ? position : null }; | |
| 52 | } | |
| 53 | ||
| 54 | /** | |
| 55 | * The pins in a new order after moving the one at `from` by `by` places | |
| 56 | * (-1 up, 1 down); null when it cannot move that way. | |
| 57 | */ | |
| 58 | export function movedPin<T>(pins: T[], from: number, by: number): T[] | null { | |
| 59 | const to = from + by; | |
| 60 | if (from < 0 || from >= pins.length || to < 0 || to >= pins.length || by === 0) return null; | |
| 61 | const next = [...pins]; | |
| 62 | const [moving] = next.splice(from, 1); | |
| 63 | next.splice(to, 0, moving!); | |
| 64 | return next; | |
| 65 | } |