| 22 | 22 | | Users, |
| 23 | 23 | | Wrench, |
| 24 | 24 | | } from "lucide-react"; |
| 25 | | − | import { Suspense, useState } from "react"; |
| 25 | + | import { Suspense, useEffect, useState } from "react"; |
| 26 | 26 | | import { Await, Form, Link, redirect } from "react-router"; |
| 27 | 27 | | |
| 28 | 28 | | import { |
| ⋯ |
| 143 | 143 | | const pullFound = work.getPull(path, number, viewer); |
| 144 | 144 | | // Its labels and milestone, with the repository's to choose from. |
| 145 | 145 | | const labelsFound = work.listLabels(path, viewer); |
| 146 | | − | // How it may merge now, for the merge box: beside the rest. |
| 147 | | − | const choicesFound = tab === "conversation" ? work.mergeChoices(viewer, path, number).catch(() => null) : Promise.resolve(null); |
| 146 | + | // How it may merge now, for the merge box: streamed, since its default |
| 147 | + | // messages are read from the branch's commits, which takes seconds. Only |
| 148 | + | // one still open or a draft can merge. |
| 149 | + | const choicesFound = |
| 150 | + | tab === "conversation" |
| 151 | + | ? pullFound |
| 152 | + | .then((found) => |
| 153 | + | found.ok && (found.value.pull.status === "open" || found.value.pull.status === "draft") |
| 154 | + | ? work.mergeChoices(viewer, path, number) |
| 155 | + | : null, |
| 156 | + | ) |
| 157 | + | .catch(() => null) |
| 158 | + | : Promise.resolve(null); |
| 148 | 159 | | const milestonesFound = work.listMilestones(path, viewer); |
| 149 | 160 | | labelsFound.catch(() => null); |
| 150 | 161 | | milestonesFound.catch(() => null); |
| ⋯ |
| 232 | 243 | | tab === "conversation" && pull.issue != null && (found.value.issue?.pullCount ?? 0) > 1 |
| 233 | 244 | | ? attemptsFor(path, viewer, pull, found.value.comments).catch(() => [] as Attempt[]) |
| 234 | 245 | | : null; |
| 235 | | − | const [labels, milestones, served] = await Promise.all([labelsFound, milestonesFound, choicesFound]); |
| 246 | + | const [labels, milestones] = await Promise.all([labelsFound, milestonesFound]); |
| 247 | + | // The merge box opens on what the settings allow, then takes the |
| 248 | + | // service's answer, with its messages, when it arrives. |
| 249 | + | const settledChoices = mergeChoicesOf(settings.ok ? settings.value : null, found.value.behind); |
| 250 | + | const servedChoices = choicesFound.then((served) => |
| 251 | + | served?.ok && served.value.methods.length > 0 ? choicesFromService(served.value) : settledChoices, |
| 252 | + | ); |
| 236 | 253 | | const repoDefault = repo.ok ? repo.value.defaultBranch : "main"; |
| 237 | 254 | | const viewerOwns = viewer != null && viewer.id === workOwner(pull).id; |
| 238 | 255 | | // The default branch's protection covers pull requests into it only. |
| ⋯ |
| 291 | 308 | | canProtect: can.manage_protection, |
| 292 | 309 | | defaultBranch: repo.ok ? repo.value.defaultBranch : "main", |
| 293 | 310 | | // What the merge box offers: methods, messages, the branch afterwards. |
| 294 | | − | choices: |
| 295 | | − | served?.ok && served.value.methods.length > 0 |
| 296 | | − | ? choicesFromService(served.value) |
| 297 | | − | : mergeChoicesOf(settings.ok ? settings.value : null, found.value.behind), |
| 311 | + | // At first from the settings; the service's answer is streamed. |
| 312 | + | choices: settledChoices, |
| 313 | + | servedChoices, |
| 298 | 314 | | // Auto-merge on: whoever may merge it, since it merges as them. Off: |
| 299 | 315 | | // they, or whoever it is for. |
| 300 | 316 | | canAutoMerge: can.merge, |
| ⋯ |
| 670 | 686 | | } |
| 671 | 687 | | |
| 672 | 688 | | /** The box at the foot of the conversation saying what stands before a merge. */ |
| 689 | + | /** `initial` until `promise` settles, then what it resolved to. */ |
| 690 | + | function useArrived<T>(promise: Promise<T>, initial: T): T { |
| 691 | + | const [arrived, setArrived] = useState<{ from: Promise<T>; value: T } | null>(null); |
| 692 | + | useEffect(() => { |
| 693 | + | let current = true; |
| 694 | + | promise.then( |
| 695 | + | (value) => current && setArrived({ from: promise, value }), |
| 696 | + | () => {}, |
| 697 | + | ); |
| 698 | + | return () => { |
| 699 | + | current = false; |
| 700 | + | }; |
| 701 | + | }, [promise]); |
| 702 | + | return arrived?.from === promise ? arrived.value : initial; |
| 703 | + | } |
| 704 | + | |
| 673 | 705 | | function StatusBox({ children }: { children: React.ReactNode }) { |
| 674 | 706 | | return ( |
| 675 | 707 | | <div className="flex gap-3"> |
| ⋯ |
| 751 | 783 | | canManage, |
| 752 | 784 | | canModerate, |
| 753 | 785 | | defaultBranch: repoDefault, |
| 754 | | − | choices, |
| 786 | + | choices: settledChoices, |
| 787 | + | servedChoices, |
| 755 | 788 | | canAutoMerge, |
| 756 | 789 | | canStopAutoMerge, |
| 757 | 790 | | canChangeBranch, |
| 758 | 791 | | } = loaderData; |
| 792 | + | // The merge box takes the service's choices in place when they arrive, |
| 793 | + | // keeping what the viewer already picked or has open. |
| 794 | + | const choices = useArrived(servedChoices, settledChoices); |
| 759 | 795 | | // The branch it merges into: what every line below names. |
| 760 | 796 | | const defaultBranch = pull.base ?? repoDefault; |
| 761 | 797 | | const base = `/${params.owner}/${params.repo}`; |