import type { ReactIsland } from "@ethicdevs/react-monolith";
import React, { useCallback, useMemo, useState } from "react";
import type { Organization, PullRequest, Repository } from "@prisma/client";
import type {
AppThemeScheme,
RepositoryFileDiff,
RepositoryWithParentAndForkedFromRepos,
} from "../types";
import { buildRouteLink } from "../utils/shared";
import { AppRoute } from "../routes.defs";
import { Button, Card, Grid, IslandWrapper } from "../components";
import RepositoryFilesDiffsList from "./RepositoryFilesDiffsList";
import PullRequestSourceSelect, {
PullRequestSourceSelection,
PullRequestSourceSelectionChangeEvent,
} from "./PullRequestSourceSelect";
export enum PullRequestFormState {
CONFIGURE = "configure",
COMPARE = "compare",
DETAILS = "input_details",
ERROR = "error",
}
export interface RepositoryPullRequestCreateFormPropsCommon {
errorMessage?: string | null;
}
export interface RepositoryPullRequestTarget {
parentOrg: Organization;
repo: Repository;
branch: string;
}
export type RepositoryPullRequestCreateFormPropsByState<
S extends PullRequestFormState = PullRequestFormState
> = S extends PullRequestFormState.CONFIGURE
? {
sources: {
[orgSlug: string]: {
org: Organization;
repos: {
[repoKey: string]: {
repo: RepositoryWithParentAndForkedFromRepos;
branches: string[];
};
};
};
};
target: RepositoryPullRequestTarget;
}
: S extends PullRequestFormState.COMPARE
? {
fileDiffs: RepositoryFileDiff[];
source: RepositoryPullRequestTarget;
target: RepositoryPullRequestTarget;
}
: S extends PullRequestFormState.DETAILS
? {
canCurrentUserSubmitPullRequest: boolean;
pullRequest: PullRequest;
}
: 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 {
parentOrgSlug: string;
repoSlug: string;
themeScheme: AppThemeScheme;
variant: RepositoryPullRequestCreateFormVariant;
}
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;
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;
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;
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
> = ({ parentOrgSlug, repoSlug, themeScheme, variant: { state, data } }) => {
const [selectedSource, setSelectedSource] =
useState<null | PullRequestSourceSelection>(null);
const [selectedTarget, setSelectedTarget] =
useState<null | PullRequestSourceSelection>(null);
const onSourceSelectionChange = useCallback(
(ev: PullRequestSourceSelectionChangeEvent) => {
setSelectedSource(ev.data);
},
[setSelectedSource]
);
const onTargetSelectionChange = useCallback(
(ev: PullRequestSourceSelectionChangeEvent) => {
setSelectedTarget(ev.data);
},
[setSelectedTarget]
);
const isCompareButtonEnabled = useMemo(() => {
const isSameRepoInSameOrg =
selectedSource != null &&
selectedTarget != null &&
selectedSource.selectedRepo.repo.organization.slug ===
selectedTarget.selectedRepo.repo.organization.slug &&
selectedSource.selectedRepo.repo.slug ===
selectedTarget.selectedRepo.repo.slug;
return !!(
selectedSource != null &&
selectedTarget != null &&
isSameRepoInSameOrg === false &&
selectedSource.selectedBranch != null &&
selectedTarget.selectedBranch != null
);
}, [selectedSource, selectedTarget]);
isCompareButtonEnabled;
if (isConfigureState(state) && isConfigureStateData(state, data)) {
return (
<Grid.Col fluid nowrap>
<form
method={"POST"}
action={buildRouteLink(
AppRoute.REPOSITORY_PULL_REQUEST_CREATE_ACTION,
{ orgSlug: parentOrgSlug, repoSlug }
)}
style={{ width: "100%" }}
>
<Card themeScheme={themeScheme}>
<input type={"hidden"} name={"state_from"} value={state} />
<input
type={"hidden"}
name={"state_dest"}
value={PullRequestFormState.COMPARE}
/>
<Grid.Row fluid alignItems={"center"} gap={8}>
<Grid.Row nowrap fluid gap={8} alignItems={"center"}>
<label style={{ display: "block", minWidth: 60 }}>
Target:
</label>
<div
data-islandid={`${PullRequestSourceSelect.name}$$1`}
style={{ width: "100%" }}
>
<PullRequestSourceSelect
themeScheme={themeScheme}
namePrefix={"target"}
defaultSource={{
org: Object.values(data.sources)[0].org,
repo: Object.values(
Object.values(data.sources)[0].repos
)[0].repo,
branch: Object.values(
Object.values(data.sources)[0].repos
)[0].branches[0],
}}
sources={data.sources}
onSelectionChange={onTargetSelectionChange}
/>
</div>
</Grid.Row>
<Grid.Row
justifyContent={"center"}
alignItems={"center"}
style={{ margin: "0 8px", width: 80 }}
>
{}
</Grid.Row>
<Grid.Row nowrap fluid gap={8} alignItems={"center"}>
<label style={{ display: "block", minWidth: 60 }}>
Source:
</label>
<div
data-islandid={`${PullRequestSourceSelect.name}$$0`}
style={{ width: "100%" }}
>
<PullRequestSourceSelect
themeScheme={themeScheme}
namePrefix={"source"}
defaultSource={{
org: data.target.parentOrg,
repo: data.target
.repo as RepositoryWithParentAndForkedFromRepos,
branch: data.target.branch,
}}
sources={data.sources}
onSelectionChange={onSourceSelectionChange}
/>
</div>
</Grid.Row>
<Button
style={{ justifySelf: "flex-end" }}
type={"submit"}
>
Compare
</Button>
</Grid.Row>
</Card>
</form>
</Grid.Col>
);
}
if (isCompareState(state) && isCompareStateData(state, data)) {
if (data.fileDiffs.length <= 0) {
return (
<Grid.Col fluid nowrap>
<h1>Nothing to compare from... 🤔</h1>
<p>
The source and the target you've just selected seems to be equals.
</p>
<p>
Try comparing two branches that have some differences before you can
create a pull request from the produced diff.
</p>
<a
href={buildRouteLink(AppRoute.REPOSITORY_PULL_REQUEST_CREATE, {
orgSlug: parentOrgSlug,
repoSlug,
})}
>
Try again
</a>
</Grid.Col>
);
}
return (
<Grid.Col fluid nowrap>
<form
method={"POST"}
action={buildRouteLink(
AppRoute.REPOSITORY_PULL_REQUEST_CREATE_ACTION,
{ orgSlug: parentOrgSlug, repoSlug }
)}
style={{ width: "100%" }}
>
<input type={"hidden"} name={"state_from"} value={state} />
<input
type={"hidden"}
name={"state_dest"}
value={PullRequestFormState.DETAILS}
/>
<input
type={"hidden"}
name={"source_parent_org_slug"}
value={data.source.parentOrg.slug}
/>
<input
type={"hidden"}
name={"source_repository_slug"}
value={data.source.repo.slug}
/>
<input
type={"hidden"}
name={"source_repository_from_branch"}
value={data.source.branch}
/>
<input
type={"hidden"}
name={"target_parent_org_slug"}
value={data.target.parentOrg.slug}
/>
<input
type={"hidden"}
name={"target_repository_slug"}
value={data.target.repo.slug}
/>
<input
type={"hidden"}
name={"target_repository_dest_branch"}
value={data.target.branch}
/>
<Grid.Row fluid>
<Grid.Col fluid>
<Card themeScheme={themeScheme} style={{ width: "100%" }}>
<Grid.Col fluid>
<label htmlFor={"summary"}>Summary</label>
<input
type={"text"}
name={"summary"}
style={{ width: "100%" }}
/>
</Grid.Col>
<Grid.Col fluid style={{ marginTop: 8 }}>
<label htmlFor={"description"}>Description</label>
<textarea
name={"description"}
style={{
width: "100%",
maxWidth: "100%",
minWidth: "100%",
minHeight: 180,
}}
></textarea>
</Grid.Col>
<Grid.Col
fluid
style={{ marginTop: 8 }}
alignItems={"flex-end"}
>
<button type={"submit"}>Create Pull Request</button>
</Grid.Col>
</Card>
</Grid.Col>
<Grid.Col flex={0.3} style={{ marginLeft: 16 }}>
<Card themeScheme={themeScheme} style={{ width: "100%" }}>
<span></span>
</Card>
</Grid.Col>
</Grid.Row>
</form>
<IslandWrapper
data-islandid={`${RepositoryFilesDiffsList.name}$$0`}
style={{ marginTop: 16 }}
>
<RepositoryFilesDiffsList
filesDiffs={data.fileDiffs}
themeScheme={themeScheme}
orgSlug={data.source.parentOrg.slug}
repoSlug={data.source.repo.slug}
commitHash={""}
/>
</IslandWrapper>
</Grid.Col>
);
}
if (isDetailsState(state) && isDetailsStateData(state, data)) {
return (
<Grid.Col fluid nowrap>
<form
method={"POST"}
action={buildRouteLink(
AppRoute.REPOSITORY_PULL_REQUEST_CREATE_ACTION,
{ orgSlug: parentOrgSlug, repoSlug }
)}
>
<input type={"hidden"} name={"state_from"} value={state} />
<button type={"submit"}>Submit the Pull Request</button>
</form>
<div>
<pre>
<code>{JSON.stringify(data, null, 2)}</code>
</pre>
</div>
</Grid.Col>
);
}
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;