feat(pull_request): started to implement the new pr flow [wip]
+ 311
- 6
@@ -1,5 +1,5 @@
 {
-  "_generatedAtUnix": 1665017412010,
+  "_generatedAtUnix": 1665025556931,
   "_hashAlgorithm": "sha1",
   "_version": 2,
   "islands": {

...
@@ -57,6 +57,12 @@
       "pathBundle": "./public/.islands/RepositoryInitialSetup.bundle.js",
       "pathSourceMap": "./public/.islands/RepositoryInitialSetup.bundle.js.map"
     },
+    "RepositoryPullRequestCreateForm": {
+      "hash": "2b062b98a7da02483b72dd96f7093dfee181c638",
+      "pathSource": "./app/islands/RepositoryPullRequestCreateForm.tsx",
+      "pathBundle": "./public/.islands/RepositoryPullRequestCreateForm.bundle.js",
+      "pathSourceMap": "./public/.islands/RepositoryPullRequestCreateForm.bundle.js.map"
+    },
     "RepositoryTreeView": {
       "hash": "846a1f7c4654c973bd106763dee544eb2a485ab2",
       "pathSource": "./app/islands/RepositoryTreeView.tsx",

...
@@ -114,7 +120,7 @@
       "pathSource": "./app/views/repository/RepositoryForkView.tsx"
     },
     "RepositoryPullRequestCreateView": {
-      "hash": "4fac956fc1de8340ad13b037dab93ad2ef2eca89",
+      "hash": "6ebba879724136e446776d131ab071232b838774",
       "pathSource": "./app/views/repository/RepositoryPullRequestCreateView.tsx"
     },
     "RepositoryPullRequestsView": {

app/controllers/repository/getRepositoryPullRequestCreateView.ts
@@ -5,6 +5,11 @@ import { AppRoute, AppRoutesParams } from "../../routes";
 // app services
 import { makeOrganizationService } from "../../services/organization";
 import { makeRepositoryService } from "../../services/repository";
+// app islands
+import {
+  PullRequestFormState,
+  RepositoryPullRequestCreateFormVariant,
+} from "../../islands/RepositoryPullRequestCreateForm";
 // app views
 import RepositoryPullRequestCreateView, {
   RepositoryPullRequestCreateViewProps,

...
@@ -27,6 +32,18 @@ const getRepositoryPullRequestCreateView: ReqHandler = async (
     return reply.status(404).callNotFound();
   }
 
+  const variant: RepositoryPullRequestCreateFormVariant = {
+    state: PullRequestFormState.CONFIGURE,
+    data: {
+      source: {
+        parentOrg,
+        repo,
+        isFork: repo.forkedFromRepoId != null,
+      },
+      initialTarget: undefined,
+    },
+  };
+
   const reqHandler = reply.makeRequestHandler(request, reply);
   return reqHandler<RepositoryPullRequestCreateViewProps>(
     RepositoryPullRequestCreateView.name,

...
@@ -35,6 +52,7 @@ const getRepositoryPullRequestCreateView: ReqHandler = async (
       initialValues: {},
       parentOrg,
       repo,
+      variant,
     }
   );
 };

app/controllers/repository/postRepositoryPullRequestCreateAction.ts
@@ -2,6 +2,11 @@
 import { ReqHandler } from "@ethicdevs/react-monolith";
 // app
 import { AppRoute, AppRoutesParams } from "../../routes";
+// app islands
+import {
+  PullRequestFormState,
+  RepositoryPullRequestCreateFormVariant,
+} from "../../islands/RepositoryPullRequestCreateForm";
 // app services
 import { makeOrganizationService } from "../../services/organization";
 import { makeRepositoryService } from "../../services/repository";

...
@@ -15,7 +20,9 @@ const postRepositoryPullRequestCreateAction: ReqHandler = async (
   reply
 ) => {
   const { orgSlug, repoSlug } =
-    request.params as AppRoutesParams[AppRoute.REPOSITORY_PULL_REQUEST_CREATE]["params"];
+    request.params as AppRoutesParams[AppRoute.REPOSITORY_PULL_REQUEST_CREATE_ACTION]["params"];
+  const { state_from: fromState, state_dest: desiredState } =
+    request.body as AppRoutesParams[AppRoute.REPOSITORY_PULL_REQUEST_CREATE_ACTION]["body"];
 
   const orgService = makeOrganizationService({ request });
   const repoService = makeRepositoryService({ request });

...
@@ -27,6 +34,55 @@ const postRepositoryPullRequestCreateAction: ReqHandler = async (
     return reply.status(404).callNotFound();
   }
 
+  if (
+    fromState === PullRequestFormState.ERROR ||
+    desiredState === PullRequestFormState.CONFIGURE
+  ) {
+    let redirectUri =
+      request.namedViewsPathMap[AppRoute.REPOSITORY_PULL_REQUEST_CREATE];
+    redirectUri = redirectUri
+      .replace(/:orgSlug/g, parentOrg.slug)
+      .replace(/:repoSlug/g, repo.slug);
+    reply.redirect(302, redirectUri);
+    return reply;
+  }
+
+  let variant: RepositoryPullRequestCreateFormVariant | null = null;
+
+  if (desiredState === PullRequestFormState.COMPARE) {
+    variant = {
+      state: desiredState,
+      data: {
+        source: {
+          parentOrg,
+          repo,
+          branch: "",
+        },
+        target: {
+          parentOrg,
+          repo,
+          branch: "",
+        },
+      },
+    };
+  } else if (desiredState === PullRequestFormState.DETAILS) {
+    variant = {
+      state: desiredState,
+      data: {
+        canCurrentUserSubmitPullRequest: false,
+      },
+    };
+  }
+
+  if (variant == null) {
+    variant = {
+      state: PullRequestFormState.ERROR,
+      data: {
+        errorMessage: "Something went wrong",
+      },
+    };
+  }
+
   const reqHandler = reply.makeRequestHandler(request, reply);
   return reqHandler<RepositoryPullRequestCreateViewProps>(
     RepositoryPullRequestCreateView.name,

...
@@ -35,6 +91,7 @@ const postRepositoryPullRequestCreateAction: ReqHandler = async (
       initialValues: {},
       parentOrg,
       repo,
+      variant,
     }
   );
 };

new file
app/islands/RepositoryPullRequestCreateForm.tsx
@@ -0,0 +1,203 @@
+// 1st-party
+import type { ReactIsland } from "@ethicdevs/react-monolith";
+// 3rd-party
+import React from "react";
+// generated via script[generate:prisma]
+import type { Organization, Repository } from "@prisma/client";
+// app
+import type { RepositoryForkedFromRepoMeta } from "../types";
+import { Grid } from "../components/Grid";
+
+export enum PullRequestFormState {
+  CONFIGURE = "configure",
+  COMPARE = "compare",
+  DETAILS = "input_details",
+  ERROR = "error",
+}
+
+export interface RepositoryPullRequestCreateFormPropsCommon {
+  errorMessage?: string | null;
+}
+
+export type RepositoryPullRequestCreateFormPropsByState<
+  S extends PullRequestFormState = PullRequestFormState
+> = S extends PullRequestFormState.CONFIGURE
+  ? {
+      source: {
+        parentOrg: Organization;
+        repo: Repository;
+        isFork: boolean;
+        forkedFromRepoMetas?: RepositoryForkedFromRepoMeta;
+      };
+      initialTarget?: {
+        parentOrg: Organization;
+        repo: Repository;
+      };
+    }
+  : S extends PullRequestFormState.COMPARE
+  ? {
+      source: {
+        parentOrg: Organization;
+        repo: Repository;
+        branch: string;
+      };
+      target: {
+        parentOrg: Organization;
+        repo: Repository;
+        branch: string;
+      };
+    }
+  : S extends PullRequestFormState.DETAILS
+  ? {
+      canCurrentUserSubmitPullRequest: boolean;
+    }
+  : S extends PullRequestFormState.ERROR
+  ? {
+      errorMessage: string;
+    }
+  : never;
+
+export type RepositoryPullRequestCreateFormVariant =
+  | {
+      state: PullRequestFormState.CONFIGURE;
+      data: RepositoryPullRequestCreateFormPropsByState<PullRequestFormState.CONFIGURE>;
+    }
+  | {
+      state: PullRequestFormState.COMPARE;
+      data: RepositoryPullRequestCreateFormPropsByState<PullRequestFormState.COMPARE>;
+    }
+  | {
+      state: PullRequestFormState.DETAILS;
+      data: RepositoryPullRequestCreateFormPropsByState<PullRequestFormState.DETAILS>;
+    }
+  | {
+      state: PullRequestFormState.ERROR;
+      data: RepositoryPullRequestCreateFormPropsByState<PullRequestFormState.ERROR>;
+    };
+
+export interface RepositoryPullRequestCreateFormProps
+  extends RepositoryPullRequestCreateFormPropsCommon {
+  variant: RepositoryPullRequestCreateFormVariant;
+}
+
+// PullRequestFormState.CONFIGURE
+const isConfigureState = (
+  s: PullRequestFormState
+): s is PullRequestFormState.CONFIGURE =>
+  typeof s !== "undefined" && s != null && s === PullRequestFormState.CONFIGURE;
+const isConfigureStateData = (
+  s: PullRequestFormState,
+  i: unknown
+): i is RepositoryPullRequestCreateFormPropsByState<PullRequestFormState.CONFIGURE> =>
+  typeof i !== "undefined" && i != null && s === PullRequestFormState.CONFIGURE;
+// PullRequestFormState.COMPARE
+const isCompareState = (
+  s: PullRequestFormState
+): s is PullRequestFormState.COMPARE =>
+  typeof s !== "undefined" && s != null && s === PullRequestFormState.COMPARE;
+const isCompareStateData = (
+  s: PullRequestFormState,
+  i: unknown
+): i is RepositoryPullRequestCreateFormPropsByState<PullRequestFormState.COMPARE> =>
+  typeof i !== "undefined" && i != null && s === PullRequestFormState.COMPARE;
+// PullRequestFormState.DETAILS
+const isDetailsState = (
+  s: PullRequestFormState
+): s is PullRequestFormState.DETAILS =>
+  typeof s !== "undefined" && s != null && s === PullRequestFormState.DETAILS;
+const isDetailsStateData = (
+  s: PullRequestFormState,
+  i: unknown
+): i is RepositoryPullRequestCreateFormPropsByState<PullRequestFormState.DETAILS> =>
+  typeof i !== "undefined" && i != null && s === PullRequestFormState.DETAILS;
+// PullRequestFormState.ERROR
+const isErrorState = (
+  s: PullRequestFormState
+): s is PullRequestFormState.ERROR =>
+  typeof s !== "undefined" && s != null && s === PullRequestFormState.ERROR;
+const isErrorStateData = (
+  s: PullRequestFormState,
+  i: unknown
+): i is RepositoryPullRequestCreateFormPropsByState<PullRequestFormState.ERROR> =>
+  typeof i !== "undefined" && i != null && s === PullRequestFormState.ERROR;
+
+const RepositoryPullRequestCreateForm: ReactIsland<RepositoryPullRequestCreateFormProps> =
+  ({ variant: { state, data } }) => {
+    // PullRequestFormState.CONFIGURE
+    if (isConfigureState(state) && isConfigureStateData(state, data)) {
+      return (
+        <Grid.Col fluid nowrap>
+          <h1>Configure input and output (org, repo, branch)</h1>
+          <form
+            method={"POST"}
+            action={`/${data.source.parentOrg.slug}/${data.source.repo.slug}/pulls/new`}
+          >
+            <input type={"hidden"} name={"state_from"} value={state} />
+            <input
+              type={"hidden"}
+              name={"state_dest"}
+              value={PullRequestFormState.COMPARE}
+            />
+            <button type={"submit"}>Go to compare</button>
+          </form>
+          <div>
+            <pre>
+              <code>{JSON.stringify(data, null, 2)}</code>
+            </pre>
+          </div>
+        </Grid.Col>
+      );
+    }
+    // PullRequestFormState.COMPARE
+    if (isCompareState(state) && isCompareStateData(state, data)) {
+      return (
+        <Grid.Col fluid nowrap>
+          <h1>Compare input and output (org, repo, branch)</h1>
+          <form
+            method={"POST"}
+            action={`/${data.source.parentOrg.slug}/${data.source.repo.slug}/pulls/new`}
+          >
+            <input type={"hidden"} name={"state_from"} value={state} />
+            <input
+              type={"hidden"}
+              name={"state_dest"}
+              value={PullRequestFormState.DETAILS}
+            />
+            <button type={"submit"}>Go to add details</button>
+          </form>
+          <div>
+            <pre>
+              <code>{JSON.stringify(data, null, 2)}</code>
+            </pre>
+          </div>
+        </Grid.Col>
+      );
+    }
+    // PullRequestFormState.DETAILS
+    if (isDetailsState(state) && isDetailsStateData(state, data)) {
+      return (
+        <Grid.Col fluid nowrap>
+          <h1>Add details about the Pull Request:</h1>
+          <div>
+            <pre>
+              <code>{JSON.stringify(data, null, 2)}</code>
+            </pre>
+          </div>
+        </Grid.Col>
+      );
+    }
+    // PullRequestFormState.ERROR
+    if (isErrorState(state) && isErrorStateData(state, data)) {
+      const { errorMessage } = data;
+      return (
+        <Grid.Col fluid nowrap>
+          <h1>Woops, an error occurred:</h1>
+          <div>{errorMessage}</div>
+        </Grid.Col>
+      );
+    }
+    return null;
+  };
+
+RepositoryPullRequestCreateForm.displayName = "RepositoryPullRequestCreateForm";
+export default RepositoryPullRequestCreateForm;


@@ -9,6 +9,8 @@ import { ResourceVisibility } from "@prisma/client";
 // app
 import type { AppThemeScheme } from "./types";
 import { authenticatedOrLogin, guestOrRedirect } from "./utils/server";
+// app islands
+import type { PullRequestFormState } from "./islands/RepositoryPullRequestCreateForm";
 // app controllers
 import {
   AuthController,

...
@@ -157,6 +159,8 @@ export interface AppRoutesParams extends IRouteParams {
       repoSlug: string;
     };
     body: {
+      state_from: PullRequestFormState;
+      state_dest: PullRequestFormState;
       summary: string;
       description: string;
       source_parent_org_slug: string;

...
@@ -486,6 +490,9 @@ export const AppRoutesSchemas: Record<AppRoute, undefined | FastifySchema> = {
     body: {
       type: "object",
       required: [
+        "state_from",
+        "state_dest",
+        // --
         "summary",
         "source_parent_org_slug",
         "source_repository_slug",

...
@@ -496,6 +503,12 @@ export const AppRoutesSchemas: Record<AppRoute, undefined | FastifySchema> = {
       ],
       additionalProperties: false,
       properties: {
+        state_from: {
+          type: "string",
+        },
+        state_dest: {
+          type: "string",
+        },
         summary: {
           type: "string",
         },

app/views/repository/RepositoryPullRequestCreateView.tsx
@@ -6,19 +6,23 @@ import React from "react";
 import type { Organization, PullRequest, Repository } from "@prisma/client";
 // app
 import type { CommonProps } from "../../types";
-import { Grid, IslandWrapper, Layout, PageWrapper } from "../../components";
+import { IslandWrapper, Layout, PageWrapper } from "../../components";
 // app islands
 import RepositoryHero from "../../islands/RepositoryHero";
+import RepositoryPullRequestCreateForm, {
+  RepositoryPullRequestCreateFormVariant,
+} from "../../islands/RepositoryPullRequestCreateForm";
 
 export interface RepositoryPullRequestCreateViewProps extends CommonProps {
   errorMessage?: string | null;
   initialValues?: PullRequest;
   parentOrg: Organization;
   repo: Repository;
+  variant: RepositoryPullRequestCreateFormVariant;
 }
 
 const RepositoryPullRequestCreateView: ReactView<RepositoryPullRequestCreateViewProps> =
-  ({ commonProps, parentOrg, repo }) => {
+  ({ commonProps, parentOrg, repo, variant }) => {
     return (
       <Layout {...commonProps}>
         <PageWrapper>

...
@@ -29,7 +33,11 @@ const RepositoryPullRequestCreateView: ReactView<RepositoryPullRequestCreateView
               repo={repo}
             />
           </IslandWrapper>
-          <Grid.Col fluid style={{ marginTop: 32 }}></Grid.Col>
+          <IslandWrapper
+            data-islandid={`${RepositoryPullRequestCreateForm.name}$$0`}
+          >
+            <RepositoryPullRequestCreateForm variant={variant} />
+          </IslandWrapper>
         </PageWrapper>
       </Layout>
     );

app/routes.tsx
@@ -9,6 +9,8 @@ import { ResourceVisibility } from "@prisma/client";
 // app
 import type { AppThemeScheme } from "./types";
 import { authenticatedOrLogin, guestOrRedirect } from "./utils/server";
+// app islands
+import type { PullRequestFormState } from "./islands/RepositoryPullRequestCreateForm";
 // app controllers
 import {
   AuthController,

...
@@ -157,6 +159,8 @@ export interface AppRoutesParams extends IRouteParams {
       repoSlug: string;
     };
     body: {
+      state_from: PullRequestFormState;
+      state_dest: PullRequestFormState;
       summary: string;
       description: string;
       source_parent_org_slug: string;

...
@@ -486,6 +490,9 @@ export const AppRoutesSchemas: Record<AppRoute, undefined | FastifySchema> = {
     body: {
       type: "object",
       required: [
+        "state_from",
+        "state_dest",
+        // --
         "summary",
         "source_parent_org_slug",
         "source_repository_slug",

...
@@ -496,6 +503,12 @@ export const AppRoutesSchemas: Record<AppRoute, undefined | FastifySchema> = {
       ],
       additionalProperties: false,
       properties: {
+        state_from: {
+          type: "string",
+        },
+        state_dest: {
+          type: "string",
+        },
         summary: {
           type: "string",
         },

app/views/repository/RepositoryPullRequestCreateView.tsx
@@ -6,19 +6,23 @@ import React from "react";
 import type { Organization, PullRequest, Repository } from "@prisma/client";
 // app
 import type { CommonProps } from "../../types";
-import { Grid, IslandWrapper, Layout, PageWrapper } from "../../components";
+import { IslandWrapper, Layout, PageWrapper } from "../../components";
 // app islands
 import RepositoryHero from "../../islands/RepositoryHero";
+import RepositoryPullRequestCreateForm, {
+  RepositoryPullRequestCreateFormVariant,
+} from "../../islands/RepositoryPullRequestCreateForm";
 
 export interface RepositoryPullRequestCreateViewProps extends CommonProps {
   errorMessage?: string | null;
   initialValues?: PullRequest;
   parentOrg: Organization;
   repo: Repository;
+  variant: RepositoryPullRequestCreateFormVariant;
 }
 
 const RepositoryPullRequestCreateView: ReactView<RepositoryPullRequestCreateViewProps> =
-  ({ commonProps, parentOrg, repo }) => {
+  ({ commonProps, parentOrg, repo, variant }) => {
     return (
       <Layout {...commonProps}>
         <PageWrapper>

...
@@ -29,7 +33,11 @@ const RepositoryPullRequestCreateView: ReactView<RepositoryPullRequestCreateView
               repo={repo}
             />
           </IslandWrapper>
-          <Grid.Col fluid style={{ marginTop: 32 }}></Grid.Col>
+          <IslandWrapper
+            data-islandid={`${RepositoryPullRequestCreateForm.name}$$0`}
+          >
+            <RepositoryPullRequestCreateForm variant={variant} />
+          </IslandWrapper>
         </PageWrapper>
       </Layout>
     );

GitFOSS • v0.2.0 (#421408f) • MIT License

GitFOSS
feat(pull_request): started to implement the new pr flow [wip]
+ 311
- 6
@@ -1,5 +1,5 @@
 {
-  "_generatedAtUnix": 1665017412010,
+  "_generatedAtUnix": 1665025556931,
   "_hashAlgorithm": "sha1",
   "_version": 2,
   "islands": {

...
@@ -57,6 +57,12 @@
       "pathBundle": "./public/.islands/RepositoryInitialSetup.bundle.js",
       "pathSourceMap": "./public/.islands/RepositoryInitialSetup.bundle.js.map"
     },
+    "RepositoryPullRequestCreateForm": {
+      "hash": "2b062b98a7da02483b72dd96f7093dfee181c638",
+      "pathSource": "./app/islands/RepositoryPullRequestCreateForm.tsx",
+      "pathBundle": "./public/.islands/RepositoryPullRequestCreateForm.bundle.js",
+      "pathSourceMap": "./public/.islands/RepositoryPullRequestCreateForm.bundle.js.map"
+    },
     "RepositoryTreeView": {
       "hash": "846a1f7c4654c973bd106763dee544eb2a485ab2",
       "pathSource": "./app/islands/RepositoryTreeView.tsx",

...
@@ -114,7 +120,7 @@
       "pathSource": "./app/views/repository/RepositoryForkView.tsx"
     },
     "RepositoryPullRequestCreateView": {
-      "hash": "4fac956fc1de8340ad13b037dab93ad2ef2eca89",
+      "hash": "6ebba879724136e446776d131ab071232b838774",
       "pathSource": "./app/views/repository/RepositoryPullRequestCreateView.tsx"
     },
     "RepositoryPullRequestsView": {

app/controllers/repository/getRepositoryPullRequestCreateView.ts
@@ -5,6 +5,11 @@ import { AppRoute, AppRoutesParams } from "../../routes";
 // app services
 import { makeOrganizationService } from "../../services/organization";
 import { makeRepositoryService } from "../../services/repository";
+// app islands
+import {
+  PullRequestFormState,
+  RepositoryPullRequestCreateFormVariant,
+} from "../../islands/RepositoryPullRequestCreateForm";
 // app views
 import RepositoryPullRequestCreateView, {
   RepositoryPullRequestCreateViewProps,

...
@@ -27,6 +32,18 @@ const getRepositoryPullRequestCreateView: ReqHandler = async (
     return reply.status(404).callNotFound();
   }
 
+  const variant: RepositoryPullRequestCreateFormVariant = {
+    state: PullRequestFormState.CONFIGURE,
+    data: {
+      source: {
+        parentOrg,
+        repo,
+        isFork: repo.forkedFromRepoId != null,
+      },
+      initialTarget: undefined,
+    },
+  };
+
   const reqHandler = reply.makeRequestHandler(request, reply);
   return reqHandler<RepositoryPullRequestCreateViewProps>(
     RepositoryPullRequestCreateView.name,

...
@@ -35,6 +52,7 @@ const getRepositoryPullRequestCreateView: ReqHandler = async (
       initialValues: {},
       parentOrg,
       repo,
+      variant,
     }
   );
 };

app/controllers/repository/postRepositoryPullRequestCreateAction.ts
@@ -2,6 +2,11 @@
 import { ReqHandler } from "@ethicdevs/react-monolith";
 // app
 import { AppRoute, AppRoutesParams } from "../../routes";
+// app islands
+import {
+  PullRequestFormState,
+  RepositoryPullRequestCreateFormVariant,
+} from "../../islands/RepositoryPullRequestCreateForm";
 // app services
 import { makeOrganizationService } from "../../services/organization";
 import { makeRepositoryService } from "../../services/repository";

...
@@ -15,7 +20,9 @@ const postRepositoryPullRequestCreateAction: ReqHandler = async (
   reply
 ) => {
   const { orgSlug, repoSlug } =
-    request.params as AppRoutesParams[AppRoute.REPOSITORY_PULL_REQUEST_CREATE]["params"];
+    request.params as AppRoutesParams[AppRoute.REPOSITORY_PULL_REQUEST_CREATE_ACTION]["params"];
+  const { state_from: fromState, state_dest: desiredState } =
+    request.body as AppRoutesParams[AppRoute.REPOSITORY_PULL_REQUEST_CREATE_ACTION]["body"];
 
   const orgService = makeOrganizationService({ request });
   const repoService = makeRepositoryService({ request });

...
@@ -27,6 +34,55 @@ const postRepositoryPullRequestCreateAction: ReqHandler = async (
     return reply.status(404).callNotFound();
   }
 
+  if (
+    fromState === PullRequestFormState.ERROR ||
+    desiredState === PullRequestFormState.CONFIGURE
+  ) {
+    let redirectUri =
+      request.namedViewsPathMap[AppRoute.REPOSITORY_PULL_REQUEST_CREATE];
+    redirectUri = redirectUri
+      .replace(/:orgSlug/g, parentOrg.slug)
+      .replace(/:repoSlug/g, repo.slug);
+    reply.redirect(302, redirectUri);
+    return reply;
+  }
+
+  let variant: RepositoryPullRequestCreateFormVariant | null = null;
+
+  if (desiredState === PullRequestFormState.COMPARE) {
+    variant = {
+      state: desiredState,
+      data: {
+        source: {
+          parentOrg,
+          repo,
+          branch: "",
+        },
+        target: {
+          parentOrg,
+          repo,
+          branch: "",
+        },
+      },
+    };
+  } else if (desiredState === PullRequestFormState.DETAILS) {
+    variant = {
+      state: desiredState,
+      data: {
+        canCurrentUserSubmitPullRequest: false,
+      },
+    };
+  }
+
+  if (variant == null) {
+    variant = {
+      state: PullRequestFormState.ERROR,
+      data: {
+        errorMessage: "Something went wrong",
+      },
+    };
+  }
+
   const reqHandler = reply.makeRequestHandler(request, reply);
   return reqHandler<RepositoryPullRequestCreateViewProps>(
     RepositoryPullRequestCreateView.name,

...
@@ -35,6 +91,7 @@ const postRepositoryPullRequestCreateAction: ReqHandler = async (
       initialValues: {},
       parentOrg,
       repo,
+      variant,
     }
   );
 };

new file
app/islands/RepositoryPullRequestCreateForm.tsx
@@ -0,0 +1,203 @@
+// 1st-party
+import type { ReactIsland } from "@ethicdevs/react-monolith";
+// 3rd-party
+import React from "react";
+// generated via script[generate:prisma]
+import type { Organization, Repository } from "@prisma/client";
+// app
+import type { RepositoryForkedFromRepoMeta } from "../types";
+import { Grid } from "../components/Grid";
+
+export enum PullRequestFormState {
+  CONFIGURE = "configure",
+  COMPARE = "compare",
+  DETAILS = "input_details",
+  ERROR = "error",
+}
+
+export interface RepositoryPullRequestCreateFormPropsCommon {
+  errorMessage?: string | null;
+}
+
+export type RepositoryPullRequestCreateFormPropsByState<
+  S extends PullRequestFormState = PullRequestFormState
+> = S extends PullRequestFormState.CONFIGURE
+  ? {
+      source: {
+        parentOrg: Organization;
+        repo: Repository;
+        isFork: boolean;
+        forkedFromRepoMetas?: RepositoryForkedFromRepoMeta;
+      };
+      initialTarget?: {
+        parentOrg: Organization;
+        repo: Repository;
+      };
+    }
+  : S extends PullRequestFormState.COMPARE
+  ? {
+      source: {
+        parentOrg: Organization;
+        repo: Repository;
+        branch: string;
+      };
+      target: {
+        parentOrg: Organization;
+        repo: Repository;
+        branch: string;
+      };
+    }
+  : S extends PullRequestFormState.DETAILS
+  ? {
+      canCurrentUserSubmitPullRequest: boolean;
+    }
+  : S extends PullRequestFormState.ERROR
+  ? {
+      errorMessage: string;
+    }
+  : never;
+
+export type RepositoryPullRequestCreateFormVariant =
+  | {
+      state: PullRequestFormState.CONFIGURE;
+      data: RepositoryPullRequestCreateFormPropsByState<PullRequestFormState.CONFIGURE>;
+    }
+  | {
+      state: PullRequestFormState.COMPARE;
+      data: RepositoryPullRequestCreateFormPropsByState<PullRequestFormState.COMPARE>;
+    }
+  | {
+      state: PullRequestFormState.DETAILS;
+      data: RepositoryPullRequestCreateFormPropsByState<PullRequestFormState.DETAILS>;
+    }
+  | {
+      state: PullRequestFormState.ERROR;
+      data: RepositoryPullRequestCreateFormPropsByState<PullRequestFormState.ERROR>;
+    };
+
+export interface RepositoryPullRequestCreateFormProps
+  extends RepositoryPullRequestCreateFormPropsCommon {
+  variant: RepositoryPullRequestCreateFormVariant;
+}
+
+// PullRequestFormState.CONFIGURE
+const isConfigureState = (
+  s: PullRequestFormState
+): s is PullRequestFormState.CONFIGURE =>
+  typeof s !== "undefined" && s != null && s === PullRequestFormState.CONFIGURE;
+const isConfigureStateData = (
+  s: PullRequestFormState,
+  i: unknown
+): i is RepositoryPullRequestCreateFormPropsByState<PullRequestFormState.CONFIGURE> =>
+  typeof i !== "undefined" && i != null && s === PullRequestFormState.CONFIGURE;
+// PullRequestFormState.COMPARE
+const isCompareState = (
+  s: PullRequestFormState
+): s is PullRequestFormState.COMPARE =>
+  typeof s !== "undefined" && s != null && s === PullRequestFormState.COMPARE;
+const isCompareStateData = (
+  s: PullRequestFormState,
+  i: unknown
+): i is RepositoryPullRequestCreateFormPropsByState<PullRequestFormState.COMPARE> =>
+  typeof i !== "undefined" && i != null && s === PullRequestFormState.COMPARE;
+// PullRequestFormState.DETAILS
+const isDetailsState = (
+  s: PullRequestFormState
+): s is PullRequestFormState.DETAILS =>
+  typeof s !== "undefined" && s != null && s === PullRequestFormState.DETAILS;
+const isDetailsStateData = (
+  s: PullRequestFormState,
+  i: unknown
+): i is RepositoryPullRequestCreateFormPropsByState<PullRequestFormState.DETAILS> =>
+  typeof i !== "undefined" && i != null && s === PullRequestFormState.DETAILS;
+// PullRequestFormState.ERROR
+const isErrorState = (
+  s: PullRequestFormState
+): s is PullRequestFormState.ERROR =>
+  typeof s !== "undefined" && s != null && s === PullRequestFormState.ERROR;
+const isErrorStateData = (
+  s: PullRequestFormState,
+  i: unknown
+): i is RepositoryPullRequestCreateFormPropsByState<PullRequestFormState.ERROR> =>
+  typeof i !== "undefined" && i != null && s === PullRequestFormState.ERROR;
+
+const RepositoryPullRequestCreateForm: ReactIsland<RepositoryPullRequestCreateFormProps> =
+  ({ variant: { state, data } }) => {
+    // PullRequestFormState.CONFIGURE
+    if (isConfigureState(state) && isConfigureStateData(state, data)) {
+      return (
+        <Grid.Col fluid nowrap>
+          <h1>Configure input and output (org, repo, branch)</h1>
+          <form
+            method={"POST"}
+            action={`/${data.source.parentOrg.slug}/${data.source.repo.slug}/pulls/new`}
+          >
+            <input type={"hidden"} name={"state_from"} value={state} />
+            <input
+              type={"hidden"}
+              name={"state_dest"}
+              value={PullRequestFormState.COMPARE}
+            />
+            <button type={"submit"}>Go to compare</button>
+          </form>
+          <div>
+            <pre>
+              <code>{JSON.stringify(data, null, 2)}</code>
+            </pre>
+          </div>
+        </Grid.Col>
+      );
+    }
+    // PullRequestFormState.COMPARE
+    if (isCompareState(state) && isCompareStateData(state, data)) {
+      return (
+        <Grid.Col fluid nowrap>
+          <h1>Compare input and output (org, repo, branch)</h1>
+          <form
+            method={"POST"}
+            action={`/${data.source.parentOrg.slug}/${data.source.repo.slug}/pulls/new`}
+          >
+            <input type={"hidden"} name={"state_from"} value={state} />
+            <input
+              type={"hidden"}
+              name={"state_dest"}
+              value={PullRequestFormState.DETAILS}
+            />
+            <button type={"submit"}>Go to add details</button>
+          </form>
+          <div>
+            <pre>
+              <code>{JSON.stringify(data, null, 2)}</code>
+            </pre>
+          </div>
+        </Grid.Col>
+      );
+    }
+    // PullRequestFormState.DETAILS
+    if (isDetailsState(state) && isDetailsStateData(state, data)) {
+      return (
+        <Grid.Col fluid nowrap>
+          <h1>Add details about the Pull Request:</h1>
+          <div>
+            <pre>
+              <code>{JSON.stringify(data, null, 2)}</code>
+            </pre>
+          </div>
+        </Grid.Col>
+      );
+    }
+    // PullRequestFormState.ERROR
+    if (isErrorState(state) && isErrorStateData(state, data)) {
+      const { errorMessage } = data;
+      return (
+        <Grid.Col fluid nowrap>
+          <h1>Woops, an error occurred:</h1>
+          <div>{errorMessage}</div>
+        </Grid.Col>
+      );
+    }
+    return null;
+  };
+
+RepositoryPullRequestCreateForm.displayName = "RepositoryPullRequestCreateForm";
+export default RepositoryPullRequestCreateForm;