import type { ReactIsland } from "@ethicdevs/react-monolith";
import React, { useCallback, useEffect, useState } from "react";
import type { Organization } from "@prisma/client";
import type { WithThemeSchemeProp } from "../types";
import { slugify } from "../utils/shared";
import { Button } from "../components/Button.styled";
import { Card } from "../components/Card.styled";
import { Grid } from "../components/Grid";
import { Select, TextArea, TextInput } from "../components";
export interface RepositoryCreateFormProps extends WithThemeSchemeProp {
availableParentOrgs: Organization[];
editMode?: boolean;
initialValues?: {
parent_org_slug?: string;
repo_slug?: string;
repo_display_name?: string;
repo_short_description?: string;
repo_website_url?: string;
repo_keywords?: string[];
};
}
const SHORT_DESCRIPTION_MAX_LENGTH = 140;
const RepositoryCreateForm: ReactIsland<RepositoryCreateFormProps> = ({
themeScheme,
availableParentOrgs,
editMode = false,
initialValues = undefined,
}) => {
const [displayName, setDisplayName] = useState<string>(
initialValues?.repo_display_name || ""
);
const [slug, setSlug] = useState<string>(
initialValues?.repo_slug || slugify(initialValues?.repo_display_name || "")
);
const [slugInputDirty, setSlugInputDirty] = useState<boolean>(false);
const [shortDescription, setShortDescription] = useState<string>(
initialValues?.repo_short_description || ""
);
const [keywords, setKeywords] = useState<string[]>([
...(initialValues?.repo_keywords || []),
]);
const [repoInitLicenseFileChecked, setRepoInitLicenseFileChecked] =
useState<boolean>(false);
editMode;
const onDisplayNameInputChange = useCallback(
(ev: React.ChangeEvent<HTMLInputElement>) => {
if (ev == null || ev.target == null || ev.target.value == null) {
return undefined;
}
setDisplayName(ev.target.value);
return undefined;
},
[setDisplayName]
);
const onSlugInputChange = useCallback(
(ev: React.ChangeEvent<HTMLInputElement>) => {
if (ev == null || ev.target == null || ev.target.value == null) {
return undefined;
}
setSlug(ev.target.value);
if (slugInputDirty === false) {
setSlugInputDirty(true);
}
return undefined;
},
[slugInputDirty, setSlug, setSlugInputDirty]
);
const onShortDescriptionTextAreaChange = useCallback(
(ev: React.ChangeEvent<HTMLTextAreaElement>) => {
if (
ev == null ||
ev.target == null ||
ev.target.value == null ||
ev.target.value.length > SHORT_DESCRIPTION_MAX_LENGTH
) {
return undefined;
}
setShortDescription(ev.target.value);
return undefined;
},
[setShortDescription]
);
const onKeywordsInputChange = useCallback(
(ev: React.ChangeEvent<HTMLInputElement>) => {
if (ev == null || ev.target == null || ev.target.value == null) {
return undefined;
}
setKeywords(ev.target.value.split(",").map((w) => w.trim()));
return undefined;
},
[setKeywords]
);
const onRepoInitLicenseFileInputChange = useCallback(
(ev: React.ChangeEvent<HTMLInputElement>) => {
if (ev == null || ev.target == null || ev.target.value == null) {
return undefined;
}
setRepoInitLicenseFileChecked(ev.target.checked);
return undefined;
},
[setRepoInitLicenseFileChecked]
);
useEffect(() => {
const nextSlug = slugify(displayName);
if (
slugInputDirty === false &&
displayName != null &&
nextSlug !== slug
) {
setSlug(nextSlug);
}
}, [displayName, slug, slugInputDirty, setSlug]);
return (
<Grid.Col fluid nowrap gap={24}>
<Grid.Col fluid nowrap gap={8}>
<label>
<strong>Repository details</strong>
</label>
<Card themeScheme={themeScheme} style={{ gap: 16 }}>
{}
<Grid.Col fluid nowrap gap={4}>
<label htmlFor={"repo_display_name"}>
Repository Name<span style={{ color: "red" }}>*</span>:
</label>
<TextInput
themeScheme={themeScheme}
name={"repo_display_name"}
onChange={onDisplayNameInputChange}
placeholder={"i.e. My Super Project"}
required
style={styles.inputMaxWidth}
type={"text"}
value={displayName}
/>
</Grid.Col>
<Grid.Row fluid nowrap gap={16} alignItems={"center"}>
{}
<Grid.Col fluid nowrap gap={4}>
<label htmlFor={"repo_display_name"}>
Owner Organization<span style={{ color: "red" }}>*</span>:
</label>
<Select
required
themeScheme={themeScheme}
name={"parent_org_slug"}
defaultValue={
availableParentOrgs.length >= 1
? availableParentOrgs[0].slug
: initialValues?.parent_org_slug
}
style={styles.inputMaxWidth}
>
{availableParentOrgs.map((org) => (
<option key={org.id} value={org.slug}>
{org.displayName || org.slug}
</option>
))}
</Select>
</Grid.Col>
{}
<Grid.Col fluid nowrap gap={4}>
<label htmlFor={"repo_slug"}>
Repository Slug<span style={{ color: "red" }}>*</span>:
</label>
<TextInput
themeScheme={themeScheme}
name={"repo_slug"}
onChange={onSlugInputChange}
placeholder={"i.e. my-super-project"}
required
style={styles.inputMaxWidth}
type={"text"}
value={slug}
/>
</Grid.Col>
</Grid.Row>
{}
<Grid.Col fluid nowrap gap={4}>
<label htmlFor={"repo_visibility"}>
Repository Visibility<span style={{ color: "red" }}>*</span>:
</label>
<Select
themeScheme={themeScheme}
name={"repo_visibility"}
defaultValue={"PRIVATE"}
style={styles.inputMaxWidth}
>
<option key={"private"} value={"PRIVATE"}>
Private
</option>
<option key={"unlisted"} value={"UNLISTED"}>
Unlisted
</option>
<option key={"public"} value={"PUBLIC"}>
Public
</option>
</Select>
</Grid.Col>
</Card>
</Grid.Col>
<Grid.Col fluid nowrap gap={8}>
<label>
<strong>Repository description</strong>
</label>
<Card themeScheme={themeScheme} style={{ gap: 16 }}>
{}
<Grid.Col fluid nowrap gap={4}>
<label htmlFor={"repo_short_description"}>Short Description:</label>
<TextArea
themeScheme={themeScheme}
name={"repo_short_description"}
maxLength={SHORT_DESCRIPTION_MAX_LENGTH}
onChange={onShortDescriptionTextAreaChange}
placeholder={"i.e. A super project about things that are super!"}
style={shortDescriptionStyles}
value={shortDescription}
></TextArea>
<span style={styles.alignSelfEnd}>
{shortDescription.length}/{SHORT_DESCRIPTION_MAX_LENGTH}
</span>
</Grid.Col>
{}
<Grid.Col fluid nowrap gap={4}>
<label htmlFor={"repo_website_url"}>Website URL:</label>
<TextInput
themeScheme={themeScheme}
name={"repo_website_url"}
defaultValue={initialValues?.repo_website_url}
placeholder={"i.e. https://www.super-project.com"}
style={styles.inputMaxWidth}
type={"text"}
/>
</Grid.Col>
{}
<Grid.Col fluid nowrap gap={4}>
<label htmlFor={"repo_keywords_add"}>Keywords:</label>
<input type={"hidden"} name={"repo_keywords"} value={keywords} />
<TextInput
themeScheme={themeScheme}
name={"repo_keywords_add"}
onChange={onKeywordsInputChange}
placeholder={"Keywords separated by a coma (,)..."}
style={styles.inputMaxWidth}
type={"text"}
/>
{keywords.map((word, idx) => (
<TextInput
themeScheme={themeScheme}
disabled
key={[idx, word].join(":")}
style={styles.inputMaxWidth}
type={"text"}
value={word}
/>
))}
</Grid.Col>
</Card>
</Grid.Col>
<Grid.Col fluid nowrap gap={8}>
<label>
<strong>Repository setup</strong>
</label>
<Card themeScheme={themeScheme} style={{ gap: 16 }}>
{}
<Grid.Row fluid nowrap gap={16}>
<label
htmlFor={"repo_init_readme_file"}
style={styles.labelFlexOne}
>
Initialize with empty README.md file?
</label>
<input
defaultChecked={false}
name={"repo_init_readme_file"}
type={"checkbox"}
/>
</Grid.Row>
{}
<Grid.Row fluid nowrap gap={16} alignItems={"center"}>
<label
htmlFor={"repo_init_readme_file"}
style={styles.labelFlexOne}
>
Initialize with a LICENSE file?
</label>
<Select
themeScheme={themeScheme}
name={"repo_init_license_kind"}
defaultValue={"MIT"}
disabled={repoInitLicenseFileChecked === false}
style={{ width: "auto" }}
>
<option key={"license:mit"} value={"mit"}>
MIT License
</option>
<option key={"license:gnu-gpl-v3"} value={"gnu-gpl-v3"}>
GNU GPL v.3.0
</option>
<option key={"license:gnu-agpl-v3"} value={"gnu-agpl-v3"}>
AGPL v.3.0
</option>
<option key={"license:gnu-lgpl-v3"} value={"gnu-lgpl-v3"}>
LGPL v.3.0
</option>
</Select>
<input
checked={repoInitLicenseFileChecked}
name={"repo_init_license_file"}
onChange={onRepoInitLicenseFileInputChange}
type={"checkbox"}
/>
</Grid.Row>
</Card>
</Grid.Col>
{}
<Button style={styles.inputMaxWidth} type={"submit"}>
Create Repository
</Button>
</Grid.Col>
);
};
const styles = {
alignSelfEnd: {
alignSelf: "flex-end",
},
inputMaxWidth: {
minWidth: "100%",
width: "100%",
maxWidth: "100%",
},
labelFlexOne: {
flex: 1,
marginRight: 16,
},
shortDescriptionTextArea: {
minHeight: 75,
height: 75,
maxHeight: 75,
},
};
const shortDescriptionStyles = {
...styles.inputMaxWidth,
...styles.shortDescriptionTextArea,
};
RepositoryCreateForm.displayName = "RepositoryCreateForm";
export default RepositoryCreateForm;