Skip to content
Source
Sign inSign up

workspace-detail-selection.ts

114 lines4.8 KB

1import type {
2 NavigationAction,
3 ParamListBase,
4 PartialState,
5 NavigationState,
6 Router,
7 StackNavigationState,
8} from "@react-navigation/native";
9import { workspaceRoutesOf } from "../workspace-overlay-routes";
10​
11type RootParams = ReactNavigation.RootParamList;
12​
13/** A page the workspace's detail column shows on its own: what a list row selects. */
14export type WorkspaceDetail =
15 | { readonly name: "Thread"; readonly params: RootParams["Thread"] }
16 | { readonly name: "PullRequest"; readonly params: RootParams["PullRequest"] };
17​
18/** Android opens the reviewers sheet over the pull request in the same step. */
19export type WorkspaceDetailSheet = {
20 readonly name: "PullRequestReviewers";
21 readonly params: RootParams["PullRequestReviewers"];
22};
23​
24const SELECT_WORKSPACE_DETAIL = "SELECT_WORKSPACE_DETAIL";
25​
26export interface SelectWorkspaceDetailAction {
27 readonly type: typeof SELECT_WORKSPACE_DETAIL;
28 readonly payload: { readonly detail: WorkspaceDetail; readonly sheet?: WorkspaceDetailSheet };
30​
31/**
32 * Shows `detail` as the workspace's only page, the way selecting a list row does. Drilling in
33 * from a page pushes instead.
34 */
35export function selectWorkspaceDetail(
36 detail: WorkspaceDetail,
37 sheet?: WorkspaceDetailSheet,
38): SelectWorkspaceDetailAction {
39 return { type: SELECT_WORKSPACE_DETAIL, payload: sheet ? { detail, sheet } : { detail } };
41​
42const IDENTITY = {
43 Thread: ["environmentId", "threadId"],
44 PullRequest: ["environmentId", "projectId", "repository", "number", "host"],
45} as const;
46​
47type RouteLike = { readonly name: string; readonly key?: string; readonly params?: object };
48​
49function showsDetail(route: RouteLike, detail: WorkspaceDetail) {
50 if (route.name !== detail.name) return false;
51 const current = route.params as Readonly<Record<string, unknown>> | undefined;
52 const target = detail.params as Readonly<Record<string, unknown>> | undefined;
53 return IDENTITY[detail.name].every((key) => current?.[key] === target?.[key]);
55​
56// UIKit pops a card by native Back before the router hears of it, so a row tapped meanwhile
57// must not reuse the card on its way out.
58let nativelyPoppingRoute: string | null = null;
59​
60export function setNativelyPoppingWorkspaceRoute(key: string | null) {
61 nativelyPoppingRoute = key;
63​
64/**
65 * The root stack after selecting `detail`: Home, then the detail alone. The route already showing
66 * it, or else the detail column's current page of the same kind, keeps its key, so the column
67 * updates in place instead of pushing.
68 */
69function workspaceDetailState<
70 State extends { readonly index: number; readonly routes: readonly RouteLike[] },
71>(state: State, detail: WorkspaceDetail, sheet?: WorkspaceDetailSheet, popping?: string | null) {
72 const { workspace } = workspaceRoutesOf(state.routes.slice(0, state.index + 1));
73 const leaving = workspace.findIndex((route) => route.key !== undefined && route.key === popping);
74 const live = leaving < 0 ? workspace : workspace.slice(0, leaving);
75 const home = live.find((route) => route.name === "Home") ?? { name: "Home" };
76 const page = live.find((route) => route.name !== "Home" && route.name !== "Settings");
77 const reused =
78 live.findLast((route) => showsDetail(route, detail)) ??
79 (page?.name === detail.name ? page : undefined);
80 const routes = [
81 home,
82 reused ? { ...reused, params: detail.params } : { name: detail.name, params: detail.params },
83 ...(sheet ? [sheet] : []),
84 ];
85 return { ...state, index: routes.length - 1, routes };
87​
88/** Root stack router overrides that handle `selectWorkspaceDetail`. */
89export function workspaceDetailRouter<Action extends NavigationAction>(
90 original: Router<StackNavigationState<ParamListBase>, Action>,
91): Partial<Router<StackNavigationState<ParamListBase>, Action>> {
92 return {
93 getStateForAction(state, action, options) {
94 if (action.type !== SELECT_WORKSPACE_DETAIL) {
95 return original.getStateForAction(state, action, options);
96 }
97 const { detail, sheet } = (action as unknown as SelectWorkspaceDetailAction).payload;
98 const next = workspaceDetailState(state, detail, sheet, nativelyPoppingRoute);
99 const reset = { type: "RESET", payload: { ...next, stale: false } };
100 return original.getStateForAction(state, reset as unknown as Action, options);
101 },
102 };
104​
105/** A link to a single thread or pull request selects it, like its list row would. */
106export function workspaceDetailLinkAction(
107 state: PartialState<NavigationState> | NavigationState,
108): SelectWorkspaceDetailAction | undefined {
109 const pages = state.routes.filter((route) => route.name !== "Home");
110 const [page] = pages;
111 if (pages.length !== 1 || !page || page.state) return undefined;
112 if (page.name !== "Thread" && page.name !== "PullRequest") return undefined;
113 return selectWorkspaceDetail({ name: page.name, params: page.params } as WorkspaceDetail);