| 1 | import type { EnvironmentId, ProjectId } from "@t3tools/contracts"; |
| 2 | |
| 3 | import type { OpenPreviewMutation } from "./browser/openFileInPreview"; |
| 4 | import type { ClosedView, ClosedViewEntry } from "./closedViewStore"; |
| 5 | import type { PullRequestListPreferences } from "./components/pullRequest/pullRequestListPreferences"; |
| 6 | import { openPreviewSession } from "./components/preview/openPreviewSession"; |
| 7 | import { |
| 8 | type RightPanelSurface, |
| 9 | type ThreadRightPanelState, |
| 10 | useRightPanelStore, |
| 11 | } from "./rightPanelStore"; |
| 12 | |
| 13 | export interface ReopenOwnerState { |
| 14 | /** False when the entry's environment is not in the catalog. */ |
| 15 | environmentKnown: boolean; |
| 16 | catalogReady: boolean; |
| 17 | /** Whether the owning thread or draft still exists locally. */ |
| 18 | ownerExists: boolean; |
| 19 | /** A live shell that lacks the thread means it was deleted; a cached one may just be stale. */ |
| 20 | shellLive: boolean; |
| 21 | panel: ThreadRightPanelState; |
| 22 | } |
| 23 | |
| 24 | /** |
| 25 | * Picks the history entry the next reopen press restores, newest first. Entries whose |
| 26 | * environment is gone or whose tab is already open are dropped. A thread missing from a |
| 27 | * live shell is skipped but kept; anything not yet knowable stops the scan with no restore. |
| 28 | */ |
| 29 | export function planNextReopen( |
| 30 | entries: readonly ClosedViewEntry[], |
| 31 | ownerState: (entry: ClosedViewEntry) => ReopenOwnerState, |
| 32 | ): { drop: ClosedViewEntry[]; restore: ClosedViewEntry | null } { |
| 33 | const drop: ClosedViewEntry[] = []; |
| 34 | for (const entry of entries) { |
| 35 | const owner = ownerState(entry); |
| 36 | if (!owner.environmentKnown) { |
| 37 | if (!owner.catalogReady) break; |
| 38 | drop.push(entry); |
| 39 | continue; |
| 40 | } |
| 41 | if (!owner.ownerExists) { |
| 42 | if (owner.shellLive) continue; |
| 43 | break; |
| 44 | } |
| 45 | const alreadyOpen = |
| 46 | entry.kind === "panel-tab" |
| 47 | ? owner.panel.isOpen && owner.panel.surfaces.some((s) => s.id === entry.surface.id) |
| 48 | : owner.panel.surfaces.some( |
| 49 | (s) => s.kind === "preview" && s.resourceId === entry.snapshot.tabId, |
| 50 | ); |
| 51 | if (alreadyOpen) { |
| 52 | drop.push(entry); |
| 53 | continue; |
| 54 | } |
| 55 | return { drop, restore: entry }; |
| 56 | } |
| 57 | return { drop, restore: null }; |
| 58 | } |
| 59 | |
| 60 | type PullRequestsSearchLike = Partial<PullRequestListPreferences> & { |
| 61 | repository?: string; |
| 62 | number?: number; |
| 63 | selectedProjectId?: ProjectId; |
| 64 | selectedHost?: string; |
| 65 | selectedEnvironmentId?: EnvironmentId; |
| 66 | }; |
| 67 | |
| 68 | /** Selects the restored pull request on the Pull Requests page, keeping the list filters. */ |
| 69 | export function pullRequestsSearchForRestore<S extends PullRequestsSearchLike>( |
| 70 | previous: S, |
| 71 | selected: RightPanelSurface | null, |
| 72 | ): S & PullRequestListPreferences { |
| 73 | const { |
| 74 | repository: _repository, |
| 75 | number: _number, |
| 76 | selectedProjectId: _projectId, |
| 77 | selectedHost: _host, |
| 78 | selectedEnvironmentId: _environmentId, |
| 79 | ...filters |
| 80 | } = previous; |
| 81 | return { |
| 82 | ...filters, |
| 83 | involvement: previous.involvement ?? "all", |
| 84 | state: previous.state ?? "open", |
| 85 | ...(selected?.kind === "pull-request" |
| 86 | ? { |
| 87 | repository: selected.repository, |
| 88 | number: selected.number, |
| 89 | selectedProjectId: selected.projectId as ProjectId, |
| 90 | ...(selected.host === undefined ? {} : { selectedHost: selected.host }), |
| 91 | ...(selected.environmentId === undefined |
| 92 | ? {} |
| 93 | : { selectedEnvironmentId: selected.environmentId as EnvironmentId }), |
| 94 | } |
| 95 | : {}), |
| 96 | } as S & PullRequestListPreferences; |
| 97 | } |
| 98 | |
| 99 | export async function reopenClosedView( |
| 100 | view: ClosedView, |
| 101 | options: { |
| 102 | openPreview: OpenPreviewMutation; |
| 103 | workspaceAvailable: boolean; |
| 104 | }, |
| 105 | ): Promise<boolean> { |
| 106 | const panels = useRightPanelStore.getState(); |
| 107 | const ref = view.threadRef; |
| 108 | |
| 109 | if (view.kind === "browser") { |
| 110 | const url = view.snapshot.navStatus._tag === "Idle" ? undefined : view.snapshot.navStatus.url; |
| 111 | const result = await openPreviewSession({ |
| 112 | openPreview: options.openPreview, |
| 113 | threadRef: ref, |
| 114 | ...(url === undefined ? {} : { url }), |
| 115 | ...(view.snapshot.viewport === undefined ? {} : { viewport: view.snapshot.viewport }), |
| 116 | ...(view.snapshot.profileId === undefined ? {} : { profileId: view.snapshot.profileId }), |
| 117 | }); |
| 118 | if (result._tag === "Failure") return false; |
| 119 | panels.openBrowser(ref, result.value.tabId); |
| 120 | return true; |
| 121 | } |
| 122 | const surface = view.surface; |
| 123 | if ( |
| 124 | !options.workspaceAvailable && |
| 125 | (surface.kind === "files" || (surface.kind === "file" && !surface.attachment)) |
| 126 | ) |
| 127 | return false; |
| 128 | switch (surface.kind) { |
| 129 | case "preview": |
| 130 | if (surface.resourceId !== null) return false; |
| 131 | panels.openBrowser(ref, null); |
| 132 | break; |
| 133 | case "file": |
| 134 | if (surface.attachment) panels.openAttachment(ref, surface.attachment); |
| 135 | else panels.openFile(ref, surface.relativePath, surface.revealLine ?? undefined); |
| 136 | break; |
| 137 | case "device": |
| 138 | if (surface.target) { |
| 139 | panels.openDevice(ref, surface.target); |
| 140 | if (surface.title) panels.renameDevice(ref, surface.id, surface.title); |
| 141 | } else panels.open(ref, "device"); |
| 142 | break; |
| 143 | case "pull-request": |
| 144 | panels.openPullRequest(ref, surface); |
| 145 | break; |
| 146 | default: |
| 147 | panels.open(ref, surface.kind); |
| 148 | } |
| 149 | return true; |
| 150 | } |