| 1 | import type { ThreadPullRequestLink } from "@t3tools/contracts"; |
| 2 | |
| 3 | import { visibleThreadPullRequests } from "./threadPullRequests.ts"; |
| 4 | |
| 5 | export interface ComposerPullRequestMatch { |
| 6 | readonly number: number; |
| 7 | readonly projectId: string; |
| 8 | readonly repository: string; |
| 9 | readonly updatedAt: string; |
| 10 | } |
| 11 | |
| 12 | /** |
| 13 | * Suggestion rows for the pull requests a thread links, from any repository. Built from the |
| 14 | * synced snapshot, so a link the server has not read yet is left out until it has one. |
| 15 | */ |
| 16 | export function composerPullRequestsFromLinks( |
| 17 | links: ReadonlyArray<ThreadPullRequestLink>, |
| 18 | projectId: string, |
| 19 | ) { |
| 20 | return visibleThreadPullRequests(links).flatMap((link) => |
| 21 | link.snapshot === null |
| 22 | ? [] |
| 23 | : [ |
| 24 | { |
| 25 | projectId, |
| 26 | repository: link.repository, |
| 27 | number: link.number, |
| 28 | url: link.url, |
| 29 | title: link.snapshot.title, |
| 30 | headBranch: link.snapshot.headBranch, |
| 31 | baseBranch: link.snapshot.baseBranch, |
| 32 | state: link.snapshot.state, |
| 33 | isDraft: link.snapshot.isDraft, |
| 34 | updatedAt: link.snapshot.updatedAt ?? link.linkedAt, |
| 35 | }, |
| 36 | ], |
| 37 | ); |
| 38 | } |
| 39 | |
| 40 | /** |
| 41 | * Pull requests matching the numeric fragment typed after `#`, de-duplicated. `entries` are kept |
| 42 | * to the project's repository; `linked` rows (the thread's own pull requests) may come from any |
| 43 | * repository. An exact number match always outranks a substring match so the result limit can |
| 44 | * never drop the pull request the user typed in full; then linked rows lead, newest first. |
| 45 | */ |
| 46 | export function filterComposerPullRequestMatches< |
| 47 | Entry extends ComposerPullRequestMatch, |
| 48 | Linked extends ComposerPullRequestMatch = never, |
| 49 | >(input: { |
| 50 | readonly entries: ReadonlyArray<Entry>; |
| 51 | readonly linked?: ReadonlyArray<Linked>; |
| 52 | readonly projectId: string; |
| 53 | readonly repository: string; |
| 54 | readonly query: string; |
| 55 | readonly limit: number; |
| 56 | }): ReadonlyArray<Entry | Linked> { |
| 57 | const repository = input.repository.trim().toLowerCase(); |
| 58 | const keyOf = (entry: ComposerPullRequestMatch) => |
| 59 | `${entry.repository.trim().toLowerCase()}#${entry.number}`; |
| 60 | const linked = (input.linked ?? []).filter((entry) => String(entry.number).includes(input.query)); |
| 61 | const linkedKeys = new Set(linked.map(keyOf)); |
| 62 | const matchingEntries = input.entries.filter( |
| 63 | (entry) => |
| 64 | entry.projectId === input.projectId && |
| 65 | entry.repository.trim().toLowerCase() === repository && |
| 66 | String(entry.number).includes(input.query), |
| 67 | ); |
| 68 | // Project rows go first so a linked pull request the listing also has keeps the fresher row. |
| 69 | const uniqueEntries = new Map<string, Entry | Linked>(); |
| 70 | for (const entry of [...matchingEntries, ...linked]) { |
| 71 | if (!uniqueEntries.has(keyOf(entry))) { |
| 72 | uniqueEntries.set(keyOf(entry), entry); |
| 73 | } |
| 74 | } |
| 75 | const isExactMatch = (entry: ComposerPullRequestMatch) => String(entry.number) === input.query; |
| 76 | const isLinked = (entry: ComposerPullRequestMatch) => linkedKeys.has(keyOf(entry)); |
| 77 | // `.sort()` on a copy, not `.toSorted()`: this runs on Hermes, which has no ES2023 array |
| 78 | // methods, and reaching for one here crashed the composer as the suggestions loaded. |
| 79 | return [...uniqueEntries.values()] |
| 80 | .sort( |
| 81 | (left, right) => |
| 82 | Number(isExactMatch(right)) - Number(isExactMatch(left)) || |
| 83 | Number(isLinked(right)) - Number(isLinked(left)) || |
| 84 | right.updatedAt.localeCompare(left.updatedAt), |
| 85 | ) |
| 86 | .slice(0, input.limit); |
| 87 | } |