feat(repository): make the "RepositoryCreateForm" island completely interactive@@ -1,5 +1,5 @@
{
- "_generatedAtUnix": 1663465895278,
+ "_generatedAtUnix": 1663468360993,
"_hashAlgorithm": "sha1",
"_version": 2,
"islands": {
@@ -10,7 +10,7 @@
"pathSourceMap": "./public/.islands/InstantRouterIndicator.bundle.js.map"
},
"RepositoryCreateForm": {
- "hash": "538d1b96e2d3672a2071c79aae841b71dd3f9975",
+ "hash": "8ef66a3025d8c4f72d871d32035bbf47f8e47809",
"pathSource": "./app/islands/RepositoryCreateForm.tsx",
"pathBundle": "./public/.islands/RepositoryCreateForm.bundle.js",
"pathSourceMap": "./public/.islands/RepositoryCreateForm.bundle.js.map"
@@ -44,7 +44,7 @@
"pathSource": "./app/views/auth/RegisterView.tsx"
},
"RepositoryCreateView": {
- "hash": "c3f2c6e08686690cb466628aa7d0715e99d9ba70",
+ "hash": "cb01e6394094a287f6084a43d556277abbbc5b05",
"pathSource": "./app/views/repository/RepositoryCreateView.tsx"
},
"RepositoryExploreView": {
@@ -1,11 +1,13 @@
// 1st-party
import type { ReactIsland } from "@ethicdevs/react-monolith";
// 3rd-party
-import React, { useCallback, useState } from "react";
+import React, { useCallback, useEffect, useState } from "react";
// generated via script[generate:prisma]
import type { Organization } from "@prisma/client";
// app
+import { Button } from "../components/Button.styled";
import { Grid } from "../components/Grid";
+import { slugify } from "../utils/shared";
export interface RepositoryCreateFormProps {
availableParentOrgs: Organization[];
@@ -20,11 +22,23 @@ export interface RepositoryCreateFormProps {
};
}
+const SHORT_DESCRIPTION_MAX_LENGTH = 140;
+
const RepositoryCreateForm: ReactIsland<RepositoryCreateFormProps> = ({
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 || []),
]);
@@ -33,6 +47,48 @@ const RepositoryCreateForm: ReactIsland<RepositoryCreateFormProps> = ({
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);
+ // only once, avoid useless re-renders for nothing
+ 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) {
@@ -44,7 +100,7 @@ const RepositoryCreateForm: ReactIsland<RepositoryCreateFormProps> = ({
[setKeywords]
);
- const onRepoInitLicenseFileChange = useCallback(
+ const onRepoInitLicenseFileInputChange = useCallback(
(ev: React.ChangeEvent<HTMLInputElement>) => {
if (ev == null || ev.target == null || ev.target.value == null) {
return undefined;
@@ -55,6 +111,20 @@ const RepositoryCreateForm: ReactIsland<RepositoryCreateFormProps> = ({
[setRepoInitLicenseFileChecked]
);
+ // An effect that will update the slug if it isn't dirty when the display name
+ // input value (displayName) has changed.
+ useEffect(() => {
+ const nextSlug = slugify(displayName);
+ if (
+ slugInputDirty === false &&
+ displayName != null &&
+ displayName.trim() !== "" &&
+ nextSlug !== slug
+ ) {
+ setSlug(nextSlug);
+ }
+ }, [displayName, slug, slugInputDirty, setSlug]);
+
return (
<div>
<fieldset>
@@ -65,10 +135,10 @@ const RepositoryCreateForm: ReactIsland<RepositoryCreateFormProps> = ({
Owner Organization <span>(*)</span>:
</label>
<select
- style={styles.inputMaxWidth}
defaultValue={initialValues?.parent_org_slug}
name={"parent_org_slug"}
required
+ style={styles.inputMaxWidth}
>
{availableParentOrgs.map((org) => (
<option key={org.id} value={org.slug}>
@@ -83,12 +153,13 @@ const RepositoryCreateForm: ReactIsland<RepositoryCreateFormProps> = ({
Repository Name <span>(*)</span>:
</label>
<input
- style={styles.inputMaxWidth}
- defaultValue={initialValues?.repo_display_name}
name={"repo_display_name"}
+ onChange={onDisplayNameInputChange}
placeholder={"i.e. My Super Project"}
required
+ style={styles.inputMaxWidth}
type={"text"}
+ value={displayName}
/>
</Grid.Col>
{/* Repository Slug */}
@@ -97,12 +168,13 @@ const RepositoryCreateForm: ReactIsland<RepositoryCreateFormProps> = ({
Repository Slug <span>(*)</span>:
</label>
<input
- style={styles.inputMaxWidth}
- defaultValue={initialValues?.repo_slug}
name={"repo_slug"}
+ onChange={onSlugInputChange}
placeholder={"i.e. my-super-project"}
required
+ style={styles.inputMaxWidth}
type={"text"}
+ value={slug}
/>
</Grid.Col>
</fieldset>
@@ -112,20 +184,25 @@ const RepositoryCreateForm: ReactIsland<RepositoryCreateFormProps> = ({
<Grid.Col fluid nowrap>
<label htmlFor={"repo_short_description"}>Short Description:</label>
<textarea
- style={styles.inputMaxWidth}
- defaultValue={initialValues?.repo_short_description}
+ maxLength={SHORT_DESCRIPTION_MAX_LENGTH}
name={"repo_short_description"}
+ 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>
{/* Website URL */}
<Grid.Col fluid nowrap>
<label htmlFor={"repo_website_url"}>Website URL:</label>
<input
- style={styles.inputMaxWidth}
defaultValue={initialValues?.repo_website_url}
name={"repo_website_url"}
placeholder={"i.e. https://www.super-project.com"}
+ style={styles.inputMaxWidth}
type={"text"}
/>
</Grid.Col>
@@ -134,17 +211,17 @@ const RepositoryCreateForm: ReactIsland<RepositoryCreateFormProps> = ({
<label htmlFor={"repo_keywords_add"}>Keywords:</label>
<input type={"hidden"} name={"repo_keywords"} value={keywords} />
<input
- style={styles.inputMaxWidth}
name={"repo_keywords_add"}
+ onChange={onKeywordsInputChange}
placeholder={"Keywords separated by a coma (,)..."}
+ style={styles.inputMaxWidth}
type={"text"}
- onChange={onKeywordsInputChange}
/>
{keywords.map((word, idx) => (
<input
- style={styles.inputMaxWidth}
- key={[idx, word].join(":")}
disabled
+ key={[idx, word].join(":")}
+ style={styles.inputMaxWidth}
type={"text"}
value={word}
/>
@@ -155,24 +232,24 @@ const RepositoryCreateForm: ReactIsland<RepositoryCreateFormProps> = ({
<legend>Repository setup</legend>
{/* Initialise Read Me file? */}
<Grid.Row fluid nowrap>
- <label htmlFor={"repo_init_readme_file"} style={styles.inputMaxWidth}>
+ <label htmlFor={"repo_init_readme_file"} style={styles.labelFlexOne}>
Initialize with empty README.md file?
</label>
<input
- type={"checkbox"}
- name={"repo_init_readme_file"}
defaultChecked={false}
+ name={"repo_init_readme_file"}
+ type={"checkbox"}
/>
</Grid.Row>
<Grid.Row fluid nowrap>
{/* Initialise License file? */}
- <label htmlFor={"repo_init_readme_file"} style={styles.inputMaxWidth}>
+ <label htmlFor={"repo_init_readme_file"} style={styles.labelFlexOne}>
Initialize with a LICENSE file?
</label>
<select
+ defaultValue={"MIT"}
disabled={repoInitLicenseFileChecked === false}
name={"repo_init_license_kind"}
- defaultValue={"MIT"}
>
<option key={"license:mit"} value={"mit"}>
MIT License
@@ -188,21 +265,42 @@ const RepositoryCreateForm: ReactIsland<RepositoryCreateFormProps> = ({
</option>
</select>
<input
- type={"checkbox"}
- name={"repo_init_license_file"}
- onChange={onRepoInitLicenseFileChange}
checked={repoInitLicenseFileChecked}
+ name={"repo_init_license_file"}
+ onChange={onRepoInitLicenseFileInputChange}
+ type={"checkbox"}
/>
</Grid.Row>
</fieldset>
+ {/* Submit Button */}
+ <Button type={"submit"}>Create Repository</Button>
</div>
);
};
const styles = {
+ alignSelfEnd: {
+ alignSelf: "flex-end",
+ },
inputMaxWidth: {
+ minWidth: "100%",
width: "100%",
+ maxWidth: "100%",
+ },
+ labelFlexOne: {
+ flex: 1,
+ marginRight: 8,
},
+ shortDescriptionTextArea: {
+ minHeight: 75,
+ height: 75,
+ maxHeight: 75,
+ },
+};
+
+const shortDescriptionStyles = {
+ ...styles.inputMaxWidth,
+ ...styles.shortDescriptionTextArea,
};
RepositoryCreateForm.displayName = "RepositoryCreateForm";
@@ -0,0 +1 @@
+export { default as slugify } from "./slugify";
@@ -0,0 +1,10 @@
+export default function slugify(str: string): string {
+ return str
+ .toString()
+ .toLowerCase()
+ .replace(/\s+/g, "-") // Replace spaces with -
+ .replace(/[^\w\-]+/g, "") // Remove all non-word chars
+ .replace(/\-\-+/g, "-") // Replace multiple - with single -
+ .replace(/^-+/, "") // Trim - from start of text
+ .replace(/-+$/, ""); // Trim - from end of text
+}
@@ -6,7 +6,7 @@ import React from "react";
import { Organization } from "@prisma/client";
// app
import type { CommonProps } from "../../types";
-import { Button, Layout, PageWrapper } from "../../components";
+import { Layout, PageWrapper } from "../../components";
// app islands
import RepositoryCreateForm from "../../islands/RepositoryCreateForm";
@@ -44,8 +44,6 @@ const RepositoryCreateView: ReactView<RepositoryCreateViewProps> = ({
initialValues={initialValues}
/>
</div>
- {/* Submit Button */}
- <Button type={"submit"}>Create Repository</Button>
</form>
</PageWrapper>
</Layout>