feat(repository): add a "RepositoryCreateForm" island that is ugly but functional yet@@ -1,5 +1,5 @@
{
- "_generatedAtUnix": 1663457839996,
+ "_generatedAtUnix": 1663465895278,
"_hashAlgorithm": "sha1",
"_version": 2,
"islands": {
@@ -9,6 +9,12 @@
"pathBundle": "./public/.islands/InstantRouterIndicator.bundle.js",
"pathSourceMap": "./public/.islands/InstantRouterIndicator.bundle.js.map"
},
+ "RepositoryCreateForm": {
+ "hash": "538d1b96e2d3672a2071c79aae841b71dd3f9975",
+ "pathSource": "./app/islands/RepositoryCreateForm.tsx",
+ "pathBundle": "./public/.islands/RepositoryCreateForm.bundle.js",
+ "pathSourceMap": "./public/.islands/RepositoryCreateForm.bundle.js.map"
+ },
"SideMenu": {
"hash": "5d01374da1cbee58e081b9022aa56f0624209e27",
"pathSource": "./app/islands/SideMenu.tsx",
@@ -37,6 +43,10 @@
"hash": "3e2c7053b529624ed6cfa9ea8631b4480bd9775b",
"pathSource": "./app/views/auth/RegisterView.tsx"
},
+ "RepositoryCreateView": {
+ "hash": "c3f2c6e08686690cb466628aa7d0715e99d9ba70",
+ "pathSource": "./app/views/repository/RepositoryCreateView.tsx"
+ },
"RepositoryExploreView": {
"hash": "53c3c318d1f6dd198a9627e9024dfc5737091a80",
"pathSource": "./app/views/repository/RepositoryExploreView.tsx"
@@ -0,0 +1,43 @@
+import styled, { css, CSSProperties, StyledComponent } from "styled-components";
+
+type GridCommonTypes = {
+ /**
+ * If set, will flex: 1.
+ * Takes precedence over `flex`.
+ */
+ fluid?: boolean;
+ flex?: CSSProperties["flex"];
+ gap?: CSSProperties["gap"];
+ alignItems?: CSSProperties["alignItems"];
+ justifyContent?: CSSProperties["justifyContent"];
+ nowrap?: boolean;
+};
+
+type GridTypes = {
+ Row: StyledComponent<"div", object, GridCommonTypes, never>;
+ Col: StyledComponent<"div", object, GridCommonTypes, never>;
+};
+
+const gridCommonCss = css<GridCommonTypes>`
+ display: flex;
+ justify-content: ${({ justifyContent }) => justifyContent || "flex-start"};
+ align-items: ${({ alignItems }) => alignItems || "flex-start"};
+
+ width: 100%;
+ height: 100%;
+
+ flex: ${({ flex, fluid }) => (fluid ? 1 : flex || 0)};
+ gap: ${({ gap }) =>
+ gap != null ? (typeof gap === "number" ? `${gap}px` : gap) : "auto"};
+`;
+
+export const Grid: GridTypes = {
+ Row: styled.div<GridCommonTypes>`
+ ${gridCommonCss};
+ flex-flow: row ${({ nowrap }) => (nowrap ? "nowrap" : "wrap")};
+ `,
+ Col: styled.div<GridCommonTypes>`
+ ${gridCommonCss};
+ flex-flow: column ${({ nowrap }) => (nowrap ? "nowrap" : "wrap")};
+ `,
+};
@@ -5,11 +5,9 @@ import styled, { css } from "styled-components";
import type { CommonProps, WithThemeSchemeProp } from "../types";
import { Const } from "../const";
import { NamedColors } from "../utils/style";
-// import { ReactMonolithLogo } from "./icons";
interface PageHeaderProps extends CommonProps {}
-// const LOGO_HEIGHT = 36;
const SIDE_MENU_WIDTH = 320;
export const PageHeader: VFC<PageHeaderProps & WithThemeSchemeProp> = ({
@@ -21,9 +19,11 @@ export const PageHeader: VFC<PageHeaderProps & WithThemeSchemeProp> = ({
const pageHeaderActions = useMemo(() => {
if (commonProps.authenticated) {
return (
- <a aria-label={"Log off your account"} href={"/auth/logout"}>
- Logout
- </a>
+ <>
+ <a aria-label={"Log off your account"} href={"/auth/logout"}>
+ Logout
+ </a>
+ </>
);
}
@@ -43,16 +43,20 @@ export const PageHeader: VFC<PageHeaderProps & WithThemeSchemeProp> = ({
<StyledPageHeader themeScheme={themeScheme}>
<StyledLogoArea themeScheme={themeScheme}>
<a href={"/"}>
- {/*<ReactMonolithLogo size={LOGO_HEIGHT} />*/}
<h1 style={{ margin: 0, marginLeft: 20 }}>{Const.APP_NAME}</h1>
</a>
</StyledLogoArea>
<StyledPageHeaderNav>
- <a aria-label={"Explore Repositories"} href={"/repositories/explore"}>
+ <a aria-label={"Explore Repositories"} href={"/repo/explore"}>
Explore Repositories
</a>
</StyledPageHeaderNav>
<StyledActionsArea>
+ {commonProps.authenticated && (
+ <a aria-label={"Create a new Repository"} href={"/repo/new"}>
+ New Repository
+ </a>
+ )}
<a
href={`/theme/${invertThemeScheme}`}
style={{ color: NamedColors.TEXT_MUTED[themeScheme] }}
@@ -80,11 +84,12 @@ const StyledPageHeader = styled.header<WithThemeSchemeProp>`
gap: 16px;
& a {
+ transition: color 140ms ease-in-out 0s;
+ text-decoration: none;
+
${({ themeScheme }) => css`
color: ${NamedColors.TEXT_MUTED[themeScheme]};
`};
- transition: color 140ms ease-in-out 0s;
- text-decoration: none;
&:hover {
${({ themeScheme }) => css`
@@ -104,6 +109,7 @@ const StyledLogoArea = styled.div<WithThemeSchemeProp>`
flex-flow: row nowrap;
justify-content: flex-start;
align-items: center;
+
${({ themeScheme }) => css`
color: ${NamedColors.TEXT_DEFAULT[themeScheme]};
`};
@@ -1,5 +1,6 @@
export { Button, ButtonAnchor } from "./Button.styled";
export { Card } from "./Card.styled";
+export { Grid } from "./Grid";
export * as Icons from "./icons";
export { Layout } from "./Layout";
export { MarkdownToJsx } from "./MarkdownToJsx";
@@ -0,0 +1,18 @@
+// 1st-party
+import { ReqHandler } from "@ethicdevs/react-monolith";
+// app
+// import { makeRepositoryService } from "../../services/repository";
+// app views
+import RepositoryCreateView, {
+ RepositoryCreateViewProps,
+} from "../../views/repository/RepositoryCreateView";
+
+const getRepositoryCreateView: ReqHandler = async (request, reply) => {
+ // const repoService = makeRepositoryService({ request });
+ const reqHandler = reply.makeRequestHandler(request, reply);
+ return reqHandler<RepositoryCreateViewProps>(RepositoryCreateView.name, {
+ availableParentOrgs: [],
+ });
+};
+
+export default getRepositoryCreateView;
@@ -1,5 +1,7 @@
+import { default as getRepositoryCreateView } from "./getRepositoryCreateView";
import { default as getRepositoryExploreView } from "./getRepositoryExploreView";
export const RepositoryController = {
+ getRepositoryCreateView,
getRepositoryExploreView,
};
@@ -0,0 +1,209 @@
+// 1st-party
+import type { ReactIsland } from "@ethicdevs/react-monolith";
+// 3rd-party
+import React, { useCallback, useState } from "react";
+// generated via script[generate:prisma]
+import type { Organization } from "@prisma/client";
+// app
+import { Grid } from "../components/Grid";
+
+export interface RepositoryCreateFormProps {
+ 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 RepositoryCreateForm: ReactIsland<RepositoryCreateFormProps> = ({
+ availableParentOrgs,
+ editMode = false,
+ initialValues = undefined,
+}) => {
+ const [keywords, setKeywords] = useState<string[]>([
+ ...(initialValues?.repo_keywords || []),
+ ]);
+ const [repoInitLicenseFileChecked, setRepoInitLicenseFileChecked] =
+ useState<boolean>(false);
+
+ editMode;
+
+ 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 onRepoInitLicenseFileChange = useCallback(
+ (ev: React.ChangeEvent<HTMLInputElement>) => {
+ if (ev == null || ev.target == null || ev.target.value == null) {
+ return undefined;
+ }
+ setRepoInitLicenseFileChecked(ev.target.checked);
+ return undefined;
+ },
+ [setRepoInitLicenseFileChecked]
+ );
+
+ return (
+ <div>
+ <fieldset>
+ <legend>Repository details</legend>
+ {/* Parent Organization Select */}
+ <Grid.Col fluid nowrap>
+ <label htmlFor={"repo_display_name"}>
+ Owner Organization <span>(*)</span>:
+ </label>
+ <select
+ style={styles.inputMaxWidth}
+ defaultValue={initialValues?.parent_org_slug}
+ name={"parent_org_slug"}
+ required
+ >
+ {availableParentOrgs.map((org) => (
+ <option key={org.id} value={org.slug}>
+ {org.displayName || org.slug}
+ </option>
+ ))}
+ </select>
+ </Grid.Col>
+ {/* Repository Name */}
+ <Grid.Col fluid nowrap>
+ <label htmlFor={"repo_display_name"}>
+ Repository Name <span>(*)</span>:
+ </label>
+ <input
+ style={styles.inputMaxWidth}
+ defaultValue={initialValues?.repo_display_name}
+ name={"repo_display_name"}
+ placeholder={"i.e. My Super Project"}
+ required
+ type={"text"}
+ />
+ </Grid.Col>
+ {/* Repository Slug */}
+ <Grid.Col fluid nowrap>
+ <label htmlFor={"repo_slug"}>
+ Repository Slug <span>(*)</span>:
+ </label>
+ <input
+ style={styles.inputMaxWidth}
+ defaultValue={initialValues?.repo_slug}
+ name={"repo_slug"}
+ placeholder={"i.e. my-super-project"}
+ required
+ type={"text"}
+ />
+ </Grid.Col>
+ </fieldset>
+ <fieldset>
+ <legend>Repository description</legend>
+ {/* Short Description */}
+ <Grid.Col fluid nowrap>
+ <label htmlFor={"repo_short_description"}>Short Description:</label>
+ <textarea
+ style={styles.inputMaxWidth}
+ defaultValue={initialValues?.repo_short_description}
+ name={"repo_short_description"}
+ placeholder={"i.e. A super project about things that are super!"}
+ ></textarea>
+ </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"}
+ type={"text"}
+ />
+ </Grid.Col>
+ {/* Keywords */}
+ <Grid.Col fluid nowrap>
+ <label htmlFor={"repo_keywords_add"}>Keywords:</label>
+ <input type={"hidden"} name={"repo_keywords"} value={keywords} />
+ <input
+ style={styles.inputMaxWidth}
+ name={"repo_keywords_add"}
+ placeholder={"Keywords separated by a coma (,)..."}
+ type={"text"}
+ onChange={onKeywordsInputChange}
+ />
+ {keywords.map((word, idx) => (
+ <input
+ style={styles.inputMaxWidth}
+ key={[idx, word].join(":")}
+ disabled
+ type={"text"}
+ value={word}
+ />
+ ))}
+ </Grid.Col>
+ </fieldset>
+ <fieldset>
+ <legend>Repository setup</legend>
+ {/* Initialise Read Me file? */}
+ <Grid.Row fluid nowrap>
+ <label htmlFor={"repo_init_readme_file"} style={styles.inputMaxWidth}>
+ Initialize with empty README.md file?
+ </label>
+ <input
+ type={"checkbox"}
+ name={"repo_init_readme_file"}
+ defaultChecked={false}
+ />
+ </Grid.Row>
+ <Grid.Row fluid nowrap>
+ {/* Initialise License file? */}
+ <label htmlFor={"repo_init_readme_file"} style={styles.inputMaxWidth}>
+ Initialize with a LICENSE file?
+ </label>
+ <select
+ disabled={repoInitLicenseFileChecked === false}
+ name={"repo_init_license_kind"}
+ defaultValue={"MIT"}
+ >
+ <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
+ type={"checkbox"}
+ name={"repo_init_license_file"}
+ onChange={onRepoInitLicenseFileChange}
+ checked={repoInitLicenseFileChecked}
+ />
+ </Grid.Row>
+ </fieldset>
+ </div>
+ );
+};
+
+const styles = {
+ inputMaxWidth: {
+ width: "100%",
+ },
+};
+
+RepositoryCreateForm.displayName = "RepositoryCreateForm";
+export default RepositoryCreateForm;
@@ -156,10 +156,16 @@ const RootAppRouter: AppRouter = () => {
<Router.Route
name={AppRoute.REPOSITORY_EXPLORE}
method={"GET"}
- path={"/repositories/explore"}
- preHandler={authenticatedOrLogin()}
+ path={"/repo/explore"}
handler={RepositoryController.getRepositoryExploreView}
/>
+ <Router.Route
+ name={AppRoute.REPOSITORY_EXPLORE}
+ method={"GET"}
+ path={"/repo/new"}
+ preHandler={authenticatedOrLogin()}
+ handler={RepositoryController.getRepositoryCreateView}
+ />
</Router.Group>
</Router.Root>
);
@@ -0,0 +1,56 @@
+// 1st-party
+import type { ReactView } from "@ethicdevs/react-monolith";
+// 3rd-party
+import React from "react";
+// generated via script[generate:prisma]
+import { Organization } from "@prisma/client";
+// app
+import type { CommonProps } from "../../types";
+import { Button, Layout, PageWrapper } from "../../components";
+// app islands
+import RepositoryCreateForm from "../../islands/RepositoryCreateForm";
+
+export interface RepositoryCreateViewProps extends CommonProps {
+ availableParentOrgs: Organization[];
+ errorMessage?: null | string;
+ initialValues?: {
+ parent_org_slug?: string;
+ repo_slug?: string;
+ repo_display_name?: string;
+ repo_short_description?: string;
+ repo_website_url?: string;
+ repo_keywords?: string[];
+ };
+}
+
+const RepositoryCreateView: ReactView<RepositoryCreateViewProps> = ({
+ availableParentOrgs,
+ commonProps,
+ errorMessage = undefined,
+ initialValues = undefined,
+}) => {
+ return (
+ <Layout {...commonProps} showSideMenu={false}>
+ <PageWrapper>
+ {errorMessage && (
+ <div className={"error_message"}>
+ <p>{errorMessage}</p>
+ </div>
+ )}
+ <form action={`/repo/new`} method={"POST"}>
+ <div data-islandid={`${RepositoryCreateForm.name}$$0`}>
+ <RepositoryCreateForm
+ availableParentOrgs={availableParentOrgs}
+ initialValues={initialValues}
+ />
+ </div>
+ {/* Submit Button */}
+ <Button type={"submit"}>Create Repository</Button>
+ </form>
+ </PageWrapper>
+ </Layout>
+ );
+};
+
+RepositoryCreateView.displayName = "RepositoryCreateView";
+export default RepositoryCreateView;
@@ -156,10 +156,16 @@ const RootAppRouter: AppRouter = () => {
<Router.Route
name={AppRoute.REPOSITORY_EXPLORE}
method={"GET"}
- path={"/repositories/explore"}
- preHandler={authenticatedOrLogin()}
+ path={"/repo/explore"}
handler={RepositoryController.getRepositoryExploreView}
/>
+ <Router.Route
+ name={AppRoute.REPOSITORY_EXPLORE}
+ method={"GET"}
+ path={"/repo/new"}
+ preHandler={authenticatedOrLogin()}
+ handler={RepositoryController.getRepositoryCreateView}
+ />
</Router.Group>
</Router.Root>
);
@@ -0,0 +1,56 @@
+// 1st-party
+import type { ReactView } from "@ethicdevs/react-monolith";
+// 3rd-party
+import React from "react";
+// generated via script[generate:prisma]
+import { Organization } from "@prisma/client";
+// app
+import type { CommonProps } from "../../types";
+import { Button, Layout, PageWrapper } from "../../components";
+// app islands
+import RepositoryCreateForm from "../../islands/RepositoryCreateForm";
+
+export interface RepositoryCreateViewProps extends CommonProps {
+ availableParentOrgs: Organization[];
+ errorMessage?: null | string;
+ initialValues?: {
+ parent_org_slug?: string;
+ repo_slug?: string;
+ repo_display_name?: string;
+ repo_short_description?: string;
+ repo_website_url?: string;
+ repo_keywords?: string[];
+ };
+}
+
+const RepositoryCreateView: ReactView<RepositoryCreateViewProps> = ({
+ availableParentOrgs,
+ commonProps,
+ errorMessage = undefined,
+ initialValues = undefined,
+}) => {
+ return (
+ <Layout {...commonProps} showSideMenu={false}>
+ <PageWrapper>
+ {errorMessage && (
+ <div className={"error_message"}>
+ <p>{errorMessage}</p>
+ </div>
+ )}
+ <form action={`/repo/new`} method={"POST"}>
+ <div data-islandid={`${RepositoryCreateForm.name}$$0`}>
+ <RepositoryCreateForm
+ availableParentOrgs={availableParentOrgs}
+ initialValues={initialValues}
+ />
+ </div>
+ {/* Submit Button */}
+ <Button type={"submit"}>Create Repository</Button>
+ </form>
+ </PageWrapper>
+ </Layout>
+ );
+};
+
+RepositoryCreateView.displayName = "RepositoryCreateView";
+export default RepositoryCreateView;