import type { ReactIsland } from "@ethicdevs/react-monolith";
import React, { useCallback, useEffect, useMemo, useState } from "react";
import type { Organization } from "@prisma/client";
import { RepositoryWithParentAndForkedFromRepos } from "../types";
export interface PullRequestSourceSelection {
selectedOrg: {
org: Organization;
repos: {
[repoSlug: string]: {
repo: RepositoryWithParentAndForkedFromRepos;
branches: string[];
};
};
};
selectedRepo: {
repo: RepositoryWithParentAndForkedFromRepos;
branches: string[];
};
selectedBranch: string;
}
export interface PullRequestSourceSelectionChangeEvent {
data: PullRequestSourceSelection;
}
export interface PullRequestSourceSelectProps {
defaultSource: {
org: Organization;
repo: RepositoryWithParentAndForkedFromRepos;
branch: string;
};
sources: {
[orgSlug: string]: {
org: Organization;
repos: {
[repoKey: string]: {
repo: RepositoryWithParentAndForkedFromRepos;
branches: string[];
};
};
};
};
namePrefix: "source" | "target";
onSelectionChange?: (
ev: PullRequestSourceSelectionChangeEvent
) => void | Promise<void>;
}
const PullRequestSourceSelect: ReactIsland<PullRequestSourceSelectProps> = ({
defaultSource,
sources,
namePrefix,
onSelectionChange,
}) => {
const [selectedOrgSlug, setSelectedOrgSlug] = useState<string>(
defaultSource.org.slug
);
const [selectedRepoSlug, setSelectedRepoSlug] = useState<string>(
defaultSource.repo.slug
);
const [selectedBranch, setSelectedBranch] = useState<string>(
defaultSource.branch
);
const allOrganizations = useMemo(() => {
const orgsBySlug = Object.entries(sources);
return orgsBySlug.reduce((acc, [_, { org }]) => {
acc = [
...acc,
{
key: org.slug,
value: org.displayName || org.slug,
},
];
return acc;
}, [] as { key: string; value: string }[]);
}, [sources]);
const allReposBySelectedOrgSlug = useMemo(() => {
const orgsBySlug = Object.entries(sources);
const org = orgsBySlug.find(([_, o]) => o.org.slug === selectedOrgSlug);
if (org == null) return [];
const [_selectedOrgSlug, { repos: orgRepositories }] = org;
const reposBySlug = Object.entries(orgRepositories);
return reposBySlug.reduce((acc, [_, { repo }]) => {
acc = [
...acc,
{
key: repo.slug,
value: repo.displayName || repo.slug,
},
];
return acc;
}, [] as { key: string; value: string }[]);
}, [sources, selectedOrgSlug]);
const allBranchesBySelectedOrgAndRepoSlugs = useMemo(() => {
const orgsBySlug = Object.entries(sources);
const org = orgsBySlug.find(([_, o]) => o.org.slug === selectedOrgSlug);
if (org == null) return [];
const [_selectedOrgSlug, { repos: orgRepositories }] = org;
const reposByKey = Object.entries(orgRepositories);
const repo = reposByKey.find(
(r) =>
r[1].repo.organization.slug === selectedOrgSlug &&
r[1].repo.slug === selectedRepoSlug
);
if (repo == null) return [];
return repo[1].branches.map((branch) => ({
key: branch,
value: branch,
}));
}, [sources, selectedOrgSlug, selectedRepoSlug]);
const selectedOrg = useMemo(() => {
const orgsBySlug = Object.entries(sources);
const org = orgsBySlug.find(([_, o]) => o.org.slug === selectedOrgSlug);
return org != null ? org[1] : null;
}, [sources, selectedOrgSlug]);
const selectedRepo = useMemo(() => {
const orgsBySlug = Object.entries(sources);
const parentOrg = orgsBySlug.find(
([_, o]) => o.org.slug === selectedOrgSlug
);
if (parentOrg == null) return null;
const reposBySlug = Object.entries(parentOrg[1].repos);
const repo = reposBySlug.find(
(r) =>
r[1].repo.organization.slug === selectedOrgSlug &&
r[1].repo.slug === selectedRepoSlug
);
if (repo == null) return null;
return repo[1];
}, [sources, selectedOrgSlug, selectedRepoSlug]);
const selectedBranchResult = useMemo(() => {
const orgsBySlug = Object.entries(sources);
const parentOrg = orgsBySlug.find(
([_, o]) => o.org.slug === selectedOrgSlug
);
if (parentOrg == null) return selectedBranch;
const reposBySlug = Object.entries(parentOrg[1].repos);
const repo = reposBySlug.find(
(r) =>
r[1].repo.organization.slug === selectedOrgSlug &&
r[1].repo.slug === selectedRepoSlug
);
if (repo == null) return selectedBranch;
const { branches } = repo[1];
return selectedBranch || branches[0];
}, [sources, selectedOrgSlug, selectedRepoSlug, selectedBranch]);
const emitSelectionChangeEvent = useCallback(() => {
if (
onSelectionChange != null &&
typeof onSelectionChange === "function" &&
selectedOrg != null &&
selectedRepo != null &&
selectedBranchResult != null
) {
onSelectionChange({
data: {
selectedOrg,
selectedRepo,
selectedBranch: selectedBranchResult,
},
});
}
}, [onSelectionChange, selectedOrg, selectedRepo, selectedBranchResult]);
const onOrgSlugChange = useCallback(
(ev: React.ChangeEvent<HTMLSelectElement>) => {
if (ev == null) return;
if (ev.target == null) return;
if (ev.target.value == null) return;
const orgSlug = ev.target.value;
setSelectedOrgSlug(orgSlug);
if (orgSlug in sources) {
const { repos } = sources[orgSlug];
if (Object.keys(repos).length >= 1) {
const { repo, branches } = Object.values(repos)[0];
setSelectedRepoSlug(repo.slug);
if (branches.length >= 1) {
setSelectedBranch(branches[0]);
}
}
}
emitSelectionChangeEvent();
},
[
emitSelectionChangeEvent,
sources,
setSelectedOrgSlug,
setSelectedRepoSlug,
setSelectedBranch,
]
);
const onRepoSlugChange = useCallback(
(ev: React.ChangeEvent<HTMLSelectElement>) => {
if (ev == null) return;
if (ev.target == null) return;
if (ev.target.value == null) return;
const repoSlug = ev.target.value;
setSelectedRepoSlug(repoSlug);
if (repoSlug in sources[selectedOrgSlug].repos) {
const { branches } = sources[selectedOrgSlug].repos[repoSlug];
if (branches.length >= 1) {
setSelectedBranch(branches[0]);
}
}
emitSelectionChangeEvent();
},
[
emitSelectionChangeEvent,
sources,
selectedOrgSlug,
setSelectedRepoSlug,
setSelectedBranch,
]
);
const onBranchChange = useCallback(
(ev: React.ChangeEvent<HTMLSelectElement>) => {
if (ev == null) return;
if (ev.target == null) return;
if (ev.target.value == null) return;
setSelectedBranch(ev.target.value);
emitSelectionChangeEvent();
},
[emitSelectionChangeEvent, setSelectedBranch]
);
const keyValueToOptionMapFn = useMemo(
() =>
({ key, value }: { key: string; value: string }, idx: number) =>
(
<option key={[key, idx].join(":")} value={key}>
{value}
</option>
),
[]
);
useEffect(() => {
emitSelectionChangeEvent();
}, []);
return (
<div>
<select
name={`${namePrefix}_parent_org_slug`}
value={selectedOrgSlug}
onChange={onOrgSlugChange}
>
{allOrganizations.map(keyValueToOptionMapFn)}
</select>
{" / "}
<select
name={`${namePrefix}_repository_slug`}
value={selectedRepoSlug}
onChange={onRepoSlugChange}
>
{allReposBySelectedOrgSlug.map(keyValueToOptionMapFn)}
</select>
{" / "}
<select
name={`${namePrefix}_repository_${
namePrefix === "source" ? "from" : "dest"
}_branch`}
value={selectedBranch}
onChange={onBranchChange}
>
{allBranchesBySelectedOrgAndRepoSlugs.map(keyValueToOptionMapFn)}
</select>
</div>
);
};
PullRequestSourceSelect.displayName = "PullRequestSourceSelect";
export default PullRequestSourceSelect;