feat(projects): list, create and update.
This commit is contained in:
		
							
								
								
									
										2
									
								
								.vscode/launch.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.vscode/launch.json
									
									
									
									
										vendored
									
									
								
							@@ -9,7 +9,7 @@
 | 
			
		||||
      "type": "chrome",
 | 
			
		||||
      "request": "launch",
 | 
			
		||||
      "reAttach": true,
 | 
			
		||||
      "url": "http://admin.blog.localhost/",
 | 
			
		||||
      "url": "http://fennec.localhost/",
 | 
			
		||||
      "webRoot": "${workspaceFolder}",
 | 
			
		||||
      "userDataDir": "/Users/ivan/Projects/.chrome"
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,8 @@
 | 
			
		||||
module.exports = {
 | 
			
		||||
  client: {
 | 
			
		||||
    service: {
 | 
			
		||||
      name: 'blog-be',
 | 
			
		||||
      url: 'http://api.blog.localhost/graphql'
 | 
			
		||||
      name: 'fennec-be',
 | 
			
		||||
      url: 'http://api.fennec.localhost/graphql'
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
overwrite: true
 | 
			
		||||
schema: "http://localhost:7132/graphql"
 | 
			
		||||
schema: "http://api.fennec.localhost/graphql"
 | 
			
		||||
# documents: "src/**/*.graphql"
 | 
			
		||||
generates:
 | 
			
		||||
  src/generated/graphql.tsx:
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
module.exports = {
 | 
			
		||||
  apps: [
 | 
			
		||||
    {
 | 
			
		||||
      name: "blog-bs",
 | 
			
		||||
      name: "fennec-bs",
 | 
			
		||||
      script: "serve",
 | 
			
		||||
      args: "",
 | 
			
		||||
      watch: false,
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1847
									
								
								graphql.schema.json
									
									
									
									
									
								
							
							
						
						
									
										1847
									
								
								graphql.schema.json
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										944
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										944
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										11
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								package.json
									
									
									
									
									
								
							@@ -1,5 +1,5 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "blog-bs",
 | 
			
		||||
  "name": "fennec-bs",
 | 
			
		||||
  "version": "0.1.0",
 | 
			
		||||
  "private": true,
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
@@ -35,7 +35,7 @@
 | 
			
		||||
    "yup": "^0.32.9"
 | 
			
		||||
  },
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "start": "PORT=7135 BROWSER=none react-scripts start",
 | 
			
		||||
    "start": "PORT=7123 BROWSER=none react-scripts start",
 | 
			
		||||
    "build": "react-scripts build",
 | 
			
		||||
    "test": "react-scripts test",
 | 
			
		||||
    "eject": "react-scripts eject",
 | 
			
		||||
@@ -66,10 +66,15 @@
 | 
			
		||||
    "@graphql-codegen/typescript": "1.21.1",
 | 
			
		||||
    "@graphql-codegen/typescript-operations": "1.17.15",
 | 
			
		||||
    "@graphql-codegen/typescript-react-apollo": "2.2.3",
 | 
			
		||||
    "@types/autoprefixer": "^10.2.0",
 | 
			
		||||
    "@types/graphql": "^14.5.0",
 | 
			
		||||
    "@types/ramda": "^0.27.40",
 | 
			
		||||
    "@types/sass": "^1.16.0",
 | 
			
		||||
    "@types/tailwindcss": "^2.0.2",
 | 
			
		||||
    "@types/yup": "^0.29.11",
 | 
			
		||||
    "sass": "^1.32.11"
 | 
			
		||||
    "autoprefixer": "^9.8.6",
 | 
			
		||||
    "postcss": "^7.0.35",
 | 
			
		||||
    "sass": "^1.32.11",
 | 
			
		||||
    "tailwindcss": "npm:@tailwindcss/postcss7-compat@^2.1.2"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,201 +0,0 @@
 | 
			
		||||
import {
 | 
			
		||||
  Button,
 | 
			
		||||
  createStyles,
 | 
			
		||||
  Grid,
 | 
			
		||||
  makeStyles,
 | 
			
		||||
  Paper,
 | 
			
		||||
} from "@material-ui/core";
 | 
			
		||||
import { Field, Form, Formik, FormikHelpers } from "formik";
 | 
			
		||||
import { FC } from "react";
 | 
			
		||||
import { Editor } from "../commons/editor/vditor";
 | 
			
		||||
import * as Yup from "yup";
 | 
			
		||||
import { Article, CreateArticleInput, UpdateArticleInput } from "../generated/graphql";
 | 
			
		||||
import { DateTimePicker } from "formik-material-ui-pickers";
 | 
			
		||||
import { gql, useMutation } from "@apollo/client";
 | 
			
		||||
import { useRouter } from "@curi/react-universal";
 | 
			
		||||
 | 
			
		||||
const CREATE_ARTICLE = gql`
 | 
			
		||||
  mutation createArticle($createArticleInput: CreateArticleInput!) {
 | 
			
		||||
    createArticle(createArticleInput: $createArticleInput) {
 | 
			
		||||
      id
 | 
			
		||||
      title
 | 
			
		||||
      content
 | 
			
		||||
      publishedAt
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const UPDATE_ARTICLE = gql`
 | 
			
		||||
  mutation updateArticle($updateArticleInput: UpdateArticleInput!) {
 | 
			
		||||
    updateArticle(updateArticleInput: $updateArticleInput) {
 | 
			
		||||
      id
 | 
			
		||||
      title
 | 
			
		||||
      content
 | 
			
		||||
      publishedAt
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const useStyles = makeStyles((theme) =>
 | 
			
		||||
  createStyles({
 | 
			
		||||
    form: {
 | 
			
		||||
      padding: "15px 30px",
 | 
			
		||||
    },
 | 
			
		||||
    editor: {
 | 
			
		||||
      height: "700px",
 | 
			
		||||
    },
 | 
			
		||||
  })
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
type Values = CreateArticleInput | UpdateArticleInput;
 | 
			
		||||
interface Props {
 | 
			
		||||
  article?: Values;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const ArticleEditor: FC<Props> = ({ article }) => {
 | 
			
		||||
  const classes = useStyles();
 | 
			
		||||
 | 
			
		||||
  const validationSchema = Yup.object({
 | 
			
		||||
    content: Yup.string()
 | 
			
		||||
      .max(65535, "文章内容不得超过 65535 个字符")
 | 
			
		||||
      .required("文章内容不得为空"),
 | 
			
		||||
    publishedAt: Yup.date(),
 | 
			
		||||
  });
 | 
			
		||||
  const router = useRouter();
 | 
			
		||||
 | 
			
		||||
  const [createArticle] = useMutation<{createArticle: Article} ,{
 | 
			
		||||
    createArticleInput: CreateArticleInput;
 | 
			
		||||
  }>(CREATE_ARTICLE, {
 | 
			
		||||
    update(cache, { data }) {
 | 
			
		||||
      cache.modify({
 | 
			
		||||
        fields: {
 | 
			
		||||
          articles(existingArticles = []) {
 | 
			
		||||
            const newArticleRef = cache.writeFragment({
 | 
			
		||||
              data: data!.createArticle,
 | 
			
		||||
              fragment: gql`
 | 
			
		||||
                fragment NewArticle on Article {
 | 
			
		||||
                  id
 | 
			
		||||
                  title
 | 
			
		||||
                  content
 | 
			
		||||
                  publishedAt
 | 
			
		||||
                }
 | 
			
		||||
              `,
 | 
			
		||||
            });
 | 
			
		||||
            return [newArticleRef, ...existingArticles];
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      });
 | 
			
		||||
    },
 | 
			
		||||
  });
 | 
			
		||||
  const [updateArticle] = useMutation<{
 | 
			
		||||
    updateArticleInput: UpdateArticleInput;
 | 
			
		||||
  }>(UPDATE_ARTICLE);
 | 
			
		||||
 | 
			
		||||
  const SubmitForm = (
 | 
			
		||||
    values: Values,
 | 
			
		||||
    { setSubmitting }: FormikHelpers<Values>
 | 
			
		||||
  ) => {
 | 
			
		||||
    if ("id" in article!) {
 | 
			
		||||
      const title = /# (.+)$/m.exec(values.content ?? "")?.[1];
 | 
			
		||||
      if (!title) {
 | 
			
		||||
        console.log("no title");
 | 
			
		||||
        setSubmitting(false);
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      updateArticle({
 | 
			
		||||
        variables: {
 | 
			
		||||
          updateArticleInput: {
 | 
			
		||||
            ...values,
 | 
			
		||||
            title,
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      })
 | 
			
		||||
        .then(() => {
 | 
			
		||||
          router.navigate({
 | 
			
		||||
            url: router.url({ name: "articles" }),
 | 
			
		||||
            method: "replace",
 | 
			
		||||
          });
 | 
			
		||||
        })
 | 
			
		||||
        .finally(() => {
 | 
			
		||||
          setSubmitting(false);
 | 
			
		||||
        });
 | 
			
		||||
    } else {
 | 
			
		||||
      const title = /# (.+)$/m.exec(values.content ?? "")?.[1];
 | 
			
		||||
      if (!title) {
 | 
			
		||||
        console.log("no title");
 | 
			
		||||
        setSubmitting(false);
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      createArticle({
 | 
			
		||||
        variables: {
 | 
			
		||||
          createArticleInput: {
 | 
			
		||||
            ...values as CreateArticleInput,
 | 
			
		||||
            title,
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      })
 | 
			
		||||
        .then(() => {
 | 
			
		||||
          router.navigate({
 | 
			
		||||
            url: router.url({ name: "articles" }),
 | 
			
		||||
            method: "replace",
 | 
			
		||||
          });
 | 
			
		||||
        })
 | 
			
		||||
        .finally(() => {
 | 
			
		||||
          setSubmitting(false);
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
    setSubmitting(false);
 | 
			
		||||
  };
 | 
			
		||||
  return (
 | 
			
		||||
    <Paper>
 | 
			
		||||
      <Formik
 | 
			
		||||
        initialValues={article!}
 | 
			
		||||
        validationSchema={validationSchema}
 | 
			
		||||
        autoComplete="off"
 | 
			
		||||
        onSubmit={SubmitForm}
 | 
			
		||||
      >
 | 
			
		||||
        {({ submitForm, isSubmitting }) => (
 | 
			
		||||
          <Form className={classes.form}>
 | 
			
		||||
            <Grid container spacing={3}>
 | 
			
		||||
              <Grid item xs={12}>
 | 
			
		||||
                <Editor
 | 
			
		||||
                  className={classes.editor}
 | 
			
		||||
                  name="content"
 | 
			
		||||
                  label="Content"
 | 
			
		||||
                />
 | 
			
		||||
              </Grid>
 | 
			
		||||
              <Grid item xs={12}>
 | 
			
		||||
                <Field
 | 
			
		||||
                  ampm={false}
 | 
			
		||||
                  component={DateTimePicker}
 | 
			
		||||
                  label="Published At"
 | 
			
		||||
                  name="publishedAt"
 | 
			
		||||
                  format="yyyy-MM-dd HH:mm:ss"
 | 
			
		||||
                />
 | 
			
		||||
              </Grid>
 | 
			
		||||
              <Grid item xs={12}>
 | 
			
		||||
                <Button
 | 
			
		||||
                  variant="contained"
 | 
			
		||||
                  color="primary"
 | 
			
		||||
                  disabled={isSubmitting}
 | 
			
		||||
                  onClick={submitForm}
 | 
			
		||||
                >
 | 
			
		||||
                  Submit
 | 
			
		||||
                </Button>
 | 
			
		||||
              </Grid>
 | 
			
		||||
            </Grid>
 | 
			
		||||
          </Form>
 | 
			
		||||
        )}
 | 
			
		||||
      </Formik>
 | 
			
		||||
    </Paper>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
ArticleEditor.defaultProps = {
 | 
			
		||||
  article: {
 | 
			
		||||
    title: "",
 | 
			
		||||
    content: "",
 | 
			
		||||
    publishedAt: new Date(),
 | 
			
		||||
    tags: [],
 | 
			
		||||
  } as CreateArticleInput,
 | 
			
		||||
};
 | 
			
		||||
@@ -1,82 +0,0 @@
 | 
			
		||||
import { useMutation, useQuery } from "@apollo/client";
 | 
			
		||||
import {
 | 
			
		||||
  IconButton,
 | 
			
		||||
  Paper,
 | 
			
		||||
  Table,
 | 
			
		||||
  TableBody,
 | 
			
		||||
  TableCell,
 | 
			
		||||
  TableContainer,
 | 
			
		||||
  TableHead,
 | 
			
		||||
  TableRow,
 | 
			
		||||
} from "@material-ui/core";
 | 
			
		||||
import React, { FC } from "react";
 | 
			
		||||
import { Article } from "../generated/graphql";
 | 
			
		||||
import EditIcon from "@material-ui/icons/Edit";
 | 
			
		||||
import { useRouter } from "@curi/react-dom";
 | 
			
		||||
import { ARTICLES, REMOVE_ARTICLE } from './articles.constants';
 | 
			
		||||
import { Delete } from '@material-ui/icons';
 | 
			
		||||
 | 
			
		||||
export const ArticleIndex: FC = () => {
 | 
			
		||||
  const { data } = useQuery<{
 | 
			
		||||
    articles: Article[];
 | 
			
		||||
  }>(ARTICLES, {});
 | 
			
		||||
 | 
			
		||||
  const [removeArticle] = useMutation<any, { id: string }>(REMOVE_ARTICLE);
 | 
			
		||||
 | 
			
		||||
  const router = useRouter();
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <section>
 | 
			
		||||
      <TableContainer component={Paper}>
 | 
			
		||||
        <Table aria-label="articles table">
 | 
			
		||||
          <TableHead>
 | 
			
		||||
            <TableRow>
 | 
			
		||||
              <TableCell>Article Title</TableCell>
 | 
			
		||||
              <TableCell>Published At</TableCell>
 | 
			
		||||
              <TableCell>Views</TableCell>
 | 
			
		||||
              <TableCell>Comments</TableCell>
 | 
			
		||||
              <TableCell>Actions</TableCell>
 | 
			
		||||
            </TableRow>
 | 
			
		||||
          </TableHead>
 | 
			
		||||
          <TableBody>
 | 
			
		||||
            {data?.articles.map((article) => (
 | 
			
		||||
              <TableRow key={article.id}>
 | 
			
		||||
                <TableCell component="th" scope="row">
 | 
			
		||||
                  {article.title}
 | 
			
		||||
                </TableCell>
 | 
			
		||||
                <TableCell>{article.publishedAt}</TableCell>
 | 
			
		||||
                <TableCell align="right"> -- </TableCell>
 | 
			
		||||
                <TableCell align="right"> -- </TableCell>
 | 
			
		||||
                <TableCell>
 | 
			
		||||
                  <IconButton
 | 
			
		||||
                    aria-label="edit"
 | 
			
		||||
                    onClick={() =>
 | 
			
		||||
                      router.navigate({
 | 
			
		||||
                        url: router.url({
 | 
			
		||||
                          name: "modify-article",
 | 
			
		||||
                          params: article,
 | 
			
		||||
                        }),
 | 
			
		||||
                      })
 | 
			
		||||
                    }
 | 
			
		||||
                  >
 | 
			
		||||
                    <EditIcon />
 | 
			
		||||
                  </IconButton>
 | 
			
		||||
                  <IconButton
 | 
			
		||||
                    aria-label="delete"
 | 
			
		||||
                    onClick={() =>
 | 
			
		||||
                      removeArticle({
 | 
			
		||||
                        variables: article,
 | 
			
		||||
                      })
 | 
			
		||||
                    }
 | 
			
		||||
                  >
 | 
			
		||||
                    <Delete />
 | 
			
		||||
                  </IconButton>
 | 
			
		||||
                </TableCell>
 | 
			
		||||
              </TableRow>
 | 
			
		||||
            ))}
 | 
			
		||||
          </TableBody>
 | 
			
		||||
        </Table>
 | 
			
		||||
      </TableContainer>
 | 
			
		||||
    </section>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
@@ -1,26 +0,0 @@
 | 
			
		||||
import { gql } from "@apollo/client";
 | 
			
		||||
 | 
			
		||||
export const ARTICLE = gql`
 | 
			
		||||
  query Article($id: String!) {
 | 
			
		||||
    article(id: $id) {
 | 
			
		||||
      id
 | 
			
		||||
      title
 | 
			
		||||
      content
 | 
			
		||||
      publishedAt
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
`;
 | 
			
		||||
export const ARTICLES = gql`
 | 
			
		||||
  query Articles {
 | 
			
		||||
    articles {
 | 
			
		||||
      id
 | 
			
		||||
      title
 | 
			
		||||
      publishedAt
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
`;
 | 
			
		||||
export const REMOVE_ARTICLE = gql`
 | 
			
		||||
  mutation RemoveArticle($id: String!) {
 | 
			
		||||
    removeArticle(id: $id)
 | 
			
		||||
  }
 | 
			
		||||
`;
 | 
			
		||||
@@ -1,3 +0,0 @@
 | 
			
		||||
export * from './article-editor';
 | 
			
		||||
export * from './articles-index';
 | 
			
		||||
export * from './articles.constants'
 | 
			
		||||
@@ -1,54 +0,0 @@
 | 
			
		||||
import { createStyles, FormControl, FormHelperText, FormLabel, makeStyles } from '@material-ui/core';
 | 
			
		||||
import { FieldHookConfig, useField } from 'formik';
 | 
			
		||||
import React, { FC, useEffect, useRef, useState } from 'react';
 | 
			
		||||
import Vditor from 'vditor';
 | 
			
		||||
import 'vditor/src/assets/scss/index.scss';
 | 
			
		||||
 | 
			
		||||
type Props = (FieldHookConfig<string>) & {
 | 
			
		||||
  label?: string;
 | 
			
		||||
  className?: string;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const useStyles = makeStyles((theme) =>
 | 
			
		||||
  createStyles({
 | 
			
		||||
    formControl: {
 | 
			
		||||
      width: '100%',
 | 
			
		||||
    },
 | 
			
		||||
  })
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
export const Editor: FC<Props> = ({ label, className, ...props }) => {
 | 
			
		||||
  const [field, meta, helpers] = useField(props);
 | 
			
		||||
  const editor = useRef<HTMLDivElement>(null);
 | 
			
		||||
  const [instance, setInstance] = useState<Vditor | null>(() => null);
 | 
			
		||||
  const [containerKey] = useState(() => Math.random().toString(36).slice(2, 8));
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (!editor.current) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    const _instance = new Vditor(editor.current, {
 | 
			
		||||
      cache: {
 | 
			
		||||
        id: containerKey,
 | 
			
		||||
      },
 | 
			
		||||
      fullscreen: {
 | 
			
		||||
        index: 1500,
 | 
			
		||||
      },
 | 
			
		||||
      value: meta.initialValue,
 | 
			
		||||
      input: (val) => helpers.setValue(val),
 | 
			
		||||
      blur: () => helpers.setTouched(true),
 | 
			
		||||
    });
 | 
			
		||||
    setInstance(_instance);
 | 
			
		||||
    return () => {
 | 
			
		||||
      instance?.destroy();
 | 
			
		||||
    };
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  const classes = useStyles();
 | 
			
		||||
  return (
 | 
			
		||||
    <FormControl className={classes.formControl}>
 | 
			
		||||
      <FormLabel>{label}</FormLabel>
 | 
			
		||||
      <div className={className} ref={editor} key={containerKey}></div>
 | 
			
		||||
      {meta.error && <FormHelperText error={true}>{meta.error}</FormHelperText>}
 | 
			
		||||
    </FormControl>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
@@ -14,20 +14,25 @@ export type Scalars = {
 | 
			
		||||
  DateTime: any;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type Article = {
 | 
			
		||||
  __typename?: 'Article';
 | 
			
		||||
  id: Scalars['ID'];
 | 
			
		||||
  title: Scalars['String'];
 | 
			
		||||
  content: Scalars['String'];
 | 
			
		||||
  publishedAt?: Maybe<Scalars['DateTime']>;
 | 
			
		||||
  tags: Array<Scalars['String']>;
 | 
			
		||||
export type CreatePipelineInput = {
 | 
			
		||||
  projectId: Scalars['String'];
 | 
			
		||||
  branch: Scalars['String'];
 | 
			
		||||
  name: Scalars['String'];
 | 
			
		||||
  workUnitMetadata: WorkUnitMetadataInput;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type CreateArticleInput = {
 | 
			
		||||
  title: Scalars['String'];
 | 
			
		||||
  content: Scalars['String'];
 | 
			
		||||
  publishedAt?: Maybe<Scalars['DateTime']>;
 | 
			
		||||
  tags: Array<Scalars['String']>;
 | 
			
		||||
export type CreatePipelineTaskInput = {
 | 
			
		||||
  pipelineId: Scalars['String'];
 | 
			
		||||
  commit: Scalars['String'];
 | 
			
		||||
  units: Array<PipelineUnits>;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type CreateProjectInput = {
 | 
			
		||||
  name: Scalars['String'];
 | 
			
		||||
  comment: Scalars['String'];
 | 
			
		||||
  sshUrl: Scalars['String'];
 | 
			
		||||
  webUrl?: Maybe<Scalars['String']>;
 | 
			
		||||
  webHookSecret?: Maybe<Scalars['String']>;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -36,44 +41,231 @@ export type Hello = {
 | 
			
		||||
  message: Scalars['String'];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type LogFields = {
 | 
			
		||||
  __typename?: 'LogFields';
 | 
			
		||||
  hash: Scalars['String'];
 | 
			
		||||
  date: Scalars['String'];
 | 
			
		||||
  message: Scalars['String'];
 | 
			
		||||
  refs: Scalars['String'];
 | 
			
		||||
  body: Scalars['String'];
 | 
			
		||||
  author_name: Scalars['String'];
 | 
			
		||||
  author_email: Scalars['String'];
 | 
			
		||||
  tasks: Array<PipelineTask>;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type LogList = {
 | 
			
		||||
  __typename?: 'LogList';
 | 
			
		||||
  all: Array<LogFields>;
 | 
			
		||||
  total: Scalars['Float'];
 | 
			
		||||
  latest: LogFields;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type Mutation = {
 | 
			
		||||
  __typename?: 'Mutation';
 | 
			
		||||
  createArticle: Article;
 | 
			
		||||
  updateArticle: Article;
 | 
			
		||||
  removeArticle: Scalars['Int'];
 | 
			
		||||
  createProject: Project;
 | 
			
		||||
  updateProject: Project;
 | 
			
		||||
  removeProject: Scalars['Float'];
 | 
			
		||||
  createPipeline: Pipeline;
 | 
			
		||||
  modifyPipeline: Pipeline;
 | 
			
		||||
  deletePipeline: Scalars['Float'];
 | 
			
		||||
  createPipelineTask: PipelineTask;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export type MutationCreateArticleArgs = {
 | 
			
		||||
  createArticleInput: CreateArticleInput;
 | 
			
		||||
export type MutationCreateProjectArgs = {
 | 
			
		||||
  project: CreateProjectInput;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export type MutationUpdateArticleArgs = {
 | 
			
		||||
  updateArticleInput: UpdateArticleInput;
 | 
			
		||||
export type MutationUpdateProjectArgs = {
 | 
			
		||||
  project: UpdateProjectInput;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export type MutationRemoveArticleArgs = {
 | 
			
		||||
export type MutationRemoveProjectArgs = {
 | 
			
		||||
  id: Scalars['String'];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export type MutationCreatePipelineArgs = {
 | 
			
		||||
  pipeline: CreatePipelineInput;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export type MutationModifyPipelineArgs = {
 | 
			
		||||
  Pipeline: UpdatePipelineInput;
 | 
			
		||||
  id: Scalars['String'];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export type MutationDeletePipelineArgs = {
 | 
			
		||||
  id: Scalars['String'];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export type MutationCreatePipelineTaskArgs = {
 | 
			
		||||
  task: CreatePipelineTaskInput;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type Pipeline = {
 | 
			
		||||
  __typename?: 'Pipeline';
 | 
			
		||||
  id: Scalars['ID'];
 | 
			
		||||
  project: Project;
 | 
			
		||||
  projectId: Scalars['String'];
 | 
			
		||||
  branch: Scalars['String'];
 | 
			
		||||
  name: Scalars['String'];
 | 
			
		||||
  workUnitMetadata: WorkUnitMetadata;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type PipelineTask = {
 | 
			
		||||
  __typename?: 'PipelineTask';
 | 
			
		||||
  id: Scalars['ID'];
 | 
			
		||||
  pipeline: Pipeline;
 | 
			
		||||
  pipelineId: Scalars['String'];
 | 
			
		||||
  commit: Scalars['String'];
 | 
			
		||||
  units: Array<PipelineUnits>;
 | 
			
		||||
  logs: Array<PipelineTaskLogs>;
 | 
			
		||||
  status: TaskStatuses;
 | 
			
		||||
  startedAt?: Maybe<Scalars['DateTime']>;
 | 
			
		||||
  endedAt?: Maybe<Scalars['DateTime']>;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type PipelineTaskLogMessage = {
 | 
			
		||||
  __typename?: 'PipelineTaskLogMessage';
 | 
			
		||||
  unit?: Maybe<PipelineUnits>;
 | 
			
		||||
  time: Scalars['DateTime'];
 | 
			
		||||
  message: Scalars['String'];
 | 
			
		||||
  isError: Scalars['Boolean'];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type PipelineTaskLogs = {
 | 
			
		||||
  __typename?: 'PipelineTaskLogs';
 | 
			
		||||
  unit: PipelineUnits;
 | 
			
		||||
  status: TaskStatuses;
 | 
			
		||||
  startedAt?: Maybe<Scalars['DateTime']>;
 | 
			
		||||
  endedAt?: Maybe<Scalars['DateTime']>;
 | 
			
		||||
  logs: Scalars['String'];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/** 流水线单元 */
 | 
			
		||||
export enum PipelineUnits {
 | 
			
		||||
  Checkout = 'checkout',
 | 
			
		||||
  InstallDependencies = 'installDependencies',
 | 
			
		||||
  Test = 'test',
 | 
			
		||||
  Deploy = 'deploy',
 | 
			
		||||
  CleanUp = 'cleanUp'
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type Project = {
 | 
			
		||||
  __typename?: 'Project';
 | 
			
		||||
  id: Scalars['ID'];
 | 
			
		||||
  name: Scalars['String'];
 | 
			
		||||
  comment: Scalars['String'];
 | 
			
		||||
  sshUrl: Scalars['String'];
 | 
			
		||||
  webUrl?: Maybe<Scalars['String']>;
 | 
			
		||||
  webHookSecret?: Maybe<Scalars['String']>;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type Query = {
 | 
			
		||||
  __typename?: 'Query';
 | 
			
		||||
  hello: Hello;
 | 
			
		||||
  articles: Array<Article>;
 | 
			
		||||
  article: Article;
 | 
			
		||||
  projects: Array<Project>;
 | 
			
		||||
  project: Project;
 | 
			
		||||
  listPipelines: Array<Pipeline>;
 | 
			
		||||
  findPipeline: Pipeline;
 | 
			
		||||
  listPipelineTaskByPipelineId: Array<PipelineTask>;
 | 
			
		||||
  findPipelineTask: PipelineTask;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export type QueryArticleArgs = {
 | 
			
		||||
export type QueryProjectArgs = {
 | 
			
		||||
  id: Scalars['String'];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type UpdateArticleInput = {
 | 
			
		||||
  title?: Maybe<Scalars['String']>;
 | 
			
		||||
  content?: Maybe<Scalars['String']>;
 | 
			
		||||
  publishedAt?: Maybe<Scalars['DateTime']>;
 | 
			
		||||
  tags?: Maybe<Array<Scalars['String']>>;
 | 
			
		||||
 | 
			
		||||
export type QueryListPipelinesArgs = {
 | 
			
		||||
  projectId?: Maybe<Scalars['String']>;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export type QueryFindPipelineArgs = {
 | 
			
		||||
  id: Scalars['String'];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export type QueryListPipelineTaskByPipelineIdArgs = {
 | 
			
		||||
  pipelineId: Scalars['String'];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export type QueryFindPipelineTaskArgs = {
 | 
			
		||||
  id: Scalars['String'];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type Subscription = {
 | 
			
		||||
  __typename?: 'Subscription';
 | 
			
		||||
  listLogsForPipeline: LogList;
 | 
			
		||||
  pipelineTaskLog: PipelineTaskLogMessage;
 | 
			
		||||
  pipelineTaskChanged: PipelineTask;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export type SubscriptionListLogsForPipelineArgs = {
 | 
			
		||||
  id: Scalars['String'];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export type SubscriptionPipelineTaskLogArgs = {
 | 
			
		||||
  taskId: Scalars['String'];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export type SubscriptionPipelineTaskChangedArgs = {
 | 
			
		||||
  id: Scalars['String'];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/** 任务状态 */
 | 
			
		||||
export enum TaskStatuses {
 | 
			
		||||
  Success = 'success',
 | 
			
		||||
  Failed = 'failed',
 | 
			
		||||
  Working = 'working',
 | 
			
		||||
  Pending = 'pending'
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type UpdatePipelineInput = {
 | 
			
		||||
  projectId: Scalars['String'];
 | 
			
		||||
  branch: Scalars['String'];
 | 
			
		||||
  name: Scalars['String'];
 | 
			
		||||
  workUnitMetadata: WorkUnitMetadataInput;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type UpdateProjectInput = {
 | 
			
		||||
  name: Scalars['String'];
 | 
			
		||||
  comment: Scalars['String'];
 | 
			
		||||
  sshUrl: Scalars['String'];
 | 
			
		||||
  webUrl?: Maybe<Scalars['String']>;
 | 
			
		||||
  webHookSecret?: Maybe<Scalars['String']>;
 | 
			
		||||
  id: Scalars['String'];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type WorkUnit = {
 | 
			
		||||
  __typename?: 'WorkUnit';
 | 
			
		||||
  type: PipelineUnits;
 | 
			
		||||
  scripts: Array<Scalars['String']>;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type WorkUnitInput = {
 | 
			
		||||
  type: PipelineUnits;
 | 
			
		||||
  scripts: Array<Scalars['String']>;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type WorkUnitMetadata = {
 | 
			
		||||
  __typename?: 'WorkUnitMetadata';
 | 
			
		||||
  version: Scalars['Float'];
 | 
			
		||||
  units: Array<WorkUnit>;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type WorkUnitMetadataInput = {
 | 
			
		||||
  version?: Maybe<Scalars['Float']>;
 | 
			
		||||
  units: Array<WorkUnitInput>;
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,6 @@ import {
 | 
			
		||||
import Drawer from "@material-ui/core/Drawer";
 | 
			
		||||
import AppBar from "@material-ui/core/AppBar";
 | 
			
		||||
import Toolbar from "@material-ui/core/Toolbar";
 | 
			
		||||
import List from "@material-ui/core/List";
 | 
			
		||||
import CssBaseline from "@material-ui/core/CssBaseline";
 | 
			
		||||
import Typography from "@material-ui/core/Typography";
 | 
			
		||||
import Divider from "@material-ui/core/Divider";
 | 
			
		||||
@@ -17,11 +16,7 @@ import IconButton from "@material-ui/core/IconButton";
 | 
			
		||||
import MenuIcon from "@material-ui/icons/Menu";
 | 
			
		||||
import ChevronLeftIcon from "@material-ui/icons/ChevronLeft";
 | 
			
		||||
import ChevronRightIcon from "@material-ui/icons/ChevronRight";
 | 
			
		||||
import ListItem from "@material-ui/core/ListItem";
 | 
			
		||||
import ListItemIcon from "@material-ui/core/ListItemIcon";
 | 
			
		||||
import ListItemText from "@material-ui/core/ListItemText";
 | 
			
		||||
import { AddCircle, Description, LocalOffer } from '@material-ui/icons';
 | 
			
		||||
import { Link } from '@curi/react-dom';
 | 
			
		||||
import { ProjectPanel } from '../projects/project-panel';
 | 
			
		||||
const drawerWidth = 240;
 | 
			
		||||
 | 
			
		||||
const useStyles = makeStyles((theme: Theme) =>
 | 
			
		||||
@@ -68,10 +63,7 @@ const useStyles = makeStyles((theme: Theme) =>
 | 
			
		||||
        duration: theme.transitions.duration.leavingScreen,
 | 
			
		||||
      }),
 | 
			
		||||
      overflowX: "hidden",
 | 
			
		||||
      width: theme.spacing(7) + 1,
 | 
			
		||||
      [theme.breakpoints.up("sm")]: {
 | 
			
		||||
        width: theme.spacing(9) + 1,
 | 
			
		||||
      },
 | 
			
		||||
      width: 0,
 | 
			
		||||
    },
 | 
			
		||||
    toolbar: {
 | 
			
		||||
      display: "flex",
 | 
			
		||||
@@ -91,7 +83,7 @@ const useStyles = makeStyles((theme: Theme) =>
 | 
			
		||||
export const DefaultLayout: FC = ({children}) => {
 | 
			
		||||
  const classes = useStyles();
 | 
			
		||||
  const theme = useTheme();
 | 
			
		||||
  const [open, setOpen] = React.useState(false);
 | 
			
		||||
  const [open, setOpen] = React.useState(true);
 | 
			
		||||
 | 
			
		||||
  const handleDrawerOpen = () => {
 | 
			
		||||
    setOpen(true);
 | 
			
		||||
@@ -150,32 +142,7 @@ export const DefaultLayout: FC = ({children}) => {
 | 
			
		||||
            </IconButton>
 | 
			
		||||
          </div>
 | 
			
		||||
          <Divider />
 | 
			
		||||
          <List>
 | 
			
		||||
            <Link name="create-article">
 | 
			
		||||
              <ListItem button>
 | 
			
		||||
                <ListItemIcon>
 | 
			
		||||
                  <AddCircle />
 | 
			
		||||
                </ListItemIcon>
 | 
			
		||||
                <ListItemText primary="New Article" />
 | 
			
		||||
              </ListItem>
 | 
			
		||||
            </Link>
 | 
			
		||||
            <Link name="articles">
 | 
			
		||||
              <ListItem button>
 | 
			
		||||
                <ListItemIcon>
 | 
			
		||||
                  <Description />
 | 
			
		||||
                </ListItemIcon>
 | 
			
		||||
                <ListItemText primary="Articles" />
 | 
			
		||||
              </ListItem>
 | 
			
		||||
            </Link>
 | 
			
		||||
            <Link name="tags">
 | 
			
		||||
              <ListItem button>
 | 
			
		||||
                <ListItemIcon>
 | 
			
		||||
                  <LocalOffer />
 | 
			
		||||
                </ListItemIcon>
 | 
			
		||||
                <ListItemText primary="Tags" />
 | 
			
		||||
              </ListItem>
 | 
			
		||||
            </Link>
 | 
			
		||||
          </List>
 | 
			
		||||
          <ProjectPanel />
 | 
			
		||||
          <Divider />
 | 
			
		||||
        </Drawer>
 | 
			
		||||
        <main className={classes.content}>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										203
									
								
								src/projects/project-editor.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										203
									
								
								src/projects/project-editor.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,203 @@
 | 
			
		||||
import { gql, useMutation } from "@apollo/client";
 | 
			
		||||
import { Button, LinearProgress, makeStyles, Paper } from "@material-ui/core";
 | 
			
		||||
import { Form, Formik, Field, FormikHelpers } from "formik";
 | 
			
		||||
import { TextField } from "formik-material-ui";
 | 
			
		||||
import { not } from "ramda";
 | 
			
		||||
import React, { FC } from "react";
 | 
			
		||||
import {
 | 
			
		||||
  CreateProjectInput,
 | 
			
		||||
  UpdateProjectInput,
 | 
			
		||||
  Project,
 | 
			
		||||
} from "../generated/graphql";
 | 
			
		||||
import * as Yup from "yup";
 | 
			
		||||
import { useRouter } from "@curi/react-dom";
 | 
			
		||||
 | 
			
		||||
type Values = Partial<Project>;
 | 
			
		||||
 | 
			
		||||
interface Props {
 | 
			
		||||
  project: Values;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const useStyles = makeStyles({
 | 
			
		||||
  root: {
 | 
			
		||||
    overflow: "hidden",
 | 
			
		||||
  },
 | 
			
		||||
  form: {
 | 
			
		||||
    margin: 16,
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const CREATE_PROJECT = gql`
 | 
			
		||||
  mutation CreateProject($input: CreateProjectInput!) {
 | 
			
		||||
    createProject(project: $input) {
 | 
			
		||||
      id
 | 
			
		||||
      name
 | 
			
		||||
      comment
 | 
			
		||||
      webUrl
 | 
			
		||||
      webHookSecret
 | 
			
		||||
      sshUrl
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const UPDATE_PROJECT = gql`
 | 
			
		||||
  mutation UpdateProject($input: UpdateProjectInput!) {
 | 
			
		||||
    updateProject(project: $input) {
 | 
			
		||||
      id
 | 
			
		||||
      name
 | 
			
		||||
      comment
 | 
			
		||||
      webUrl
 | 
			
		||||
      webHookSecret
 | 
			
		||||
      sshUrl
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
export const ProjectEditor: FC<Props> = ({ project }) => {
 | 
			
		||||
  const isCreate = not("id" in project);
 | 
			
		||||
 | 
			
		||||
  const [createProject] = useMutation<{ createProject: Project }>(
 | 
			
		||||
    CREATE_PROJECT,
 | 
			
		||||
    {
 | 
			
		||||
      update(cache, { data }) {
 | 
			
		||||
        cache.modify({
 | 
			
		||||
          fields: {
 | 
			
		||||
            findProjects(exitingProjects = []) {
 | 
			
		||||
              const newProjectRef = cache.writeFragment({
 | 
			
		||||
                data: data!.createProject,
 | 
			
		||||
                fragment: gql`
 | 
			
		||||
                  fragment newProject on Project {
 | 
			
		||||
                    id
 | 
			
		||||
                    name
 | 
			
		||||
                    comment
 | 
			
		||||
                    webUrl
 | 
			
		||||
                    sshUrl
 | 
			
		||||
                    webHookSecret
 | 
			
		||||
                  }
 | 
			
		||||
                `,
 | 
			
		||||
              });
 | 
			
		||||
              return [newProjectRef, ...exitingProjects];
 | 
			
		||||
            },
 | 
			
		||||
          },
 | 
			
		||||
        });
 | 
			
		||||
      },
 | 
			
		||||
    }
 | 
			
		||||
  );
 | 
			
		||||
  const [updateProject] = useMutation(UPDATE_PROJECT);
 | 
			
		||||
 | 
			
		||||
  const router = useRouter();
 | 
			
		||||
  const submitForm = async (
 | 
			
		||||
    values: Values,
 | 
			
		||||
    formikHelpers: FormikHelpers<Values>
 | 
			
		||||
  ) => {
 | 
			
		||||
    try {
 | 
			
		||||
      let projectId: string | undefined = project.id;
 | 
			
		||||
      if (isCreate) {
 | 
			
		||||
        await createProject({
 | 
			
		||||
          variables: {
 | 
			
		||||
            input: values,
 | 
			
		||||
          },
 | 
			
		||||
        }).then(({ data }) => (projectId = data!.createProject.id));
 | 
			
		||||
      } else {
 | 
			
		||||
        await updateProject({
 | 
			
		||||
          variables: {
 | 
			
		||||
            id: (project as Project).id,
 | 
			
		||||
            input: values,
 | 
			
		||||
          },
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
      router.navigate({
 | 
			
		||||
        url: router.url({
 | 
			
		||||
          name: "project-detail",
 | 
			
		||||
          params: {
 | 
			
		||||
            projectId,
 | 
			
		||||
          },
 | 
			
		||||
        }),
 | 
			
		||||
        method: "replace",
 | 
			
		||||
      });
 | 
			
		||||
    } finally {
 | 
			
		||||
      formikHelpers.setSubmitting(false);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const classes = useStyles();
 | 
			
		||||
  return (
 | 
			
		||||
    <Paper className={classes.root}>
 | 
			
		||||
      <Formik
 | 
			
		||||
        initialValues={project}
 | 
			
		||||
        validationSchema={Yup.object({
 | 
			
		||||
          name: Yup.string()
 | 
			
		||||
            .max(32, "Must be 32 characters or less")
 | 
			
		||||
            .required("Required"),
 | 
			
		||||
          comment: Yup.string()
 | 
			
		||||
            .max(32, "Must be 32 characters or less")
 | 
			
		||||
            .required("Required"),
 | 
			
		||||
          webUrl: Yup.string()
 | 
			
		||||
            .matches(
 | 
			
		||||
              /^(https?:\/\/)?([\da-z.-]+\.[a-z.]{2,6}|[\d.]+)([\\/:?=&#]{1}[\da-z.-]+)*[/\\?]?$/i,
 | 
			
		||||
              "Enter correct url!"
 | 
			
		||||
            )
 | 
			
		||||
            .max(256, "Must be 256 characters or less")
 | 
			
		||||
            .required("Required"),
 | 
			
		||||
          sshUrl: Yup.string()
 | 
			
		||||
            .matches(
 | 
			
		||||
              /^(?:ssh:\/\/)?(?:[\w\d-_]+@)?(?:[\w\d-_]+\.)*\w{2,10}(?::\d{1,5})?(?:\/[\w\d-_.]+)*$/,
 | 
			
		||||
              "Enter correct url!"
 | 
			
		||||
            )
 | 
			
		||||
            .max(256, "Must be 256 characters or less")
 | 
			
		||||
            .required("Required"),
 | 
			
		||||
          webHookSecret: Yup.string()
 | 
			
		||||
            .max(16, "Must be 16 characters or less")
 | 
			
		||||
            .required("Required"),
 | 
			
		||||
        })}
 | 
			
		||||
        onSubmit={submitForm}
 | 
			
		||||
      >
 | 
			
		||||
        {({ submitForm, isSubmitting }) => (
 | 
			
		||||
          <Form className={classes.form}>
 | 
			
		||||
            <Field component={TextField} name="name" label="Name" fullWidth />
 | 
			
		||||
            <Field
 | 
			
		||||
              component={TextField}
 | 
			
		||||
              name="comment"
 | 
			
		||||
              label="Comment"
 | 
			
		||||
              fullWidth
 | 
			
		||||
              margin="normal"
 | 
			
		||||
            />
 | 
			
		||||
            <Field
 | 
			
		||||
              component={TextField}
 | 
			
		||||
              name="webUrl"
 | 
			
		||||
              label="Project URL"
 | 
			
		||||
              placeholder="The project website url"
 | 
			
		||||
              fullWidth
 | 
			
		||||
              margin="normal"
 | 
			
		||||
            />
 | 
			
		||||
            <Field
 | 
			
		||||
              component={TextField}
 | 
			
		||||
              name="sshUrl"
 | 
			
		||||
              label="Git SSH URL"
 | 
			
		||||
              placeholder="The Git remote SSH URL"
 | 
			
		||||
              fullWidth
 | 
			
		||||
              margin="normal"
 | 
			
		||||
            />
 | 
			
		||||
            <Field
 | 
			
		||||
              component={TextField}
 | 
			
		||||
              name="webHookSecret"
 | 
			
		||||
              label="Webhook Secret"
 | 
			
		||||
              fullWidth
 | 
			
		||||
              margin="normal"
 | 
			
		||||
            />
 | 
			
		||||
            {isSubmitting && <LinearProgress />}
 | 
			
		||||
            <Button
 | 
			
		||||
              variant="contained"
 | 
			
		||||
              color="primary"
 | 
			
		||||
              disabled={isSubmitting}
 | 
			
		||||
              onClick={submitForm}
 | 
			
		||||
              fullWidth
 | 
			
		||||
            >
 | 
			
		||||
              Submit
 | 
			
		||||
            </Button>
 | 
			
		||||
          </Form>
 | 
			
		||||
        )}
 | 
			
		||||
      </Formik>
 | 
			
		||||
    </Paper>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										82
									
								
								src/projects/project-panel.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								src/projects/project-panel.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,82 @@
 | 
			
		||||
import { gql, useQuery } from '@apollo/client';
 | 
			
		||||
import { Link, useRouter } from '@curi/react-dom';
 | 
			
		||||
import { Box, List, ListItem } from '@material-ui/core';
 | 
			
		||||
import { makeStyles, Theme, createStyles } from '@material-ui/core';
 | 
			
		||||
import { find, propEq } from 'ramda';
 | 
			
		||||
import React, { useState, FC, useEffect } from 'react';
 | 
			
		||||
import { Project } from '../generated/graphql';
 | 
			
		||||
import { ListItemText } from '@material-ui/core';
 | 
			
		||||
import { Button } from '@material-ui/core';
 | 
			
		||||
import { AddBox } from '@material-ui/icons';
 | 
			
		||||
 | 
			
		||||
const PROJECTS = gql`
 | 
			
		||||
  query Projects {
 | 
			
		||||
    projects {
 | 
			
		||||
      id
 | 
			
		||||
      name
 | 
			
		||||
      comment
 | 
			
		||||
      sshUrl
 | 
			
		||||
      webUrl
 | 
			
		||||
      webHookSecret
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const useStyles = makeStyles((theme: Theme) =>
 | 
			
		||||
  createStyles({
 | 
			
		||||
    
 | 
			
		||||
  })
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
export function ProjectPanel() {
 | 
			
		||||
  return (
 | 
			
		||||
    <section>
 | 
			
		||||
      <Box m={2}>
 | 
			
		||||
        <Link
 | 
			
		||||
          name="create-project"
 | 
			
		||||
        >
 | 
			
		||||
          <Button
 | 
			
		||||
            variant="contained"
 | 
			
		||||
            color="primary"
 | 
			
		||||
            title="New Project"
 | 
			
		||||
            startIcon={<AddBox />}
 | 
			
		||||
          >
 | 
			
		||||
            New Project
 | 
			
		||||
          </Button>
 | 
			
		||||
        </Link>
 | 
			
		||||
      </Box>
 | 
			
		||||
      <ProjectList />
 | 
			
		||||
    </section>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const ProjectList: FC<{}> = () => {
 | 
			
		||||
  const { data, refetch } = useQuery<{
 | 
			
		||||
    projects: Project[];
 | 
			
		||||
  }>(PROJECTS);
 | 
			
		||||
  const projects = data?.projects;
 | 
			
		||||
  const [currentProject, setCurrentProject] = useState<Project | undefined>(
 | 
			
		||||
    undefined
 | 
			
		||||
  );
 | 
			
		||||
  const { current } = useRouter();
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const currId = current()?.response?.params.projectId;
 | 
			
		||||
    console.log(currId);
 | 
			
		||||
    setCurrentProject(find(propEq("id", currId), projects ?? []));
 | 
			
		||||
  }, [current, projects]);
 | 
			
		||||
 | 
			
		||||
  const items = projects?.map((item) => (
 | 
			
		||||
    <Link
 | 
			
		||||
      name="edit-project"
 | 
			
		||||
      params={{ projectId: item.id }}
 | 
			
		||||
      key={item.id}
 | 
			
		||||
      onNav={() => setCurrentProject(item)}
 | 
			
		||||
    >
 | 
			
		||||
      <ListItem button>
 | 
			
		||||
        <ListItemText primary={item.name} secondary={item.comment} />
 | 
			
		||||
      </ListItem>
 | 
			
		||||
    </Link>
 | 
			
		||||
  ));
 | 
			
		||||
  return <List>{items}</List>;
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										14
									
								
								src/projects/queries.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								src/projects/queries.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,14 @@
 | 
			
		||||
import { gql } from '@apollo/client';
 | 
			
		||||
 | 
			
		||||
export const PROJECT = gql`
 | 
			
		||||
query Project($id:String!) {
 | 
			
		||||
  project(id: $id) {
 | 
			
		||||
    id
 | 
			
		||||
    name
 | 
			
		||||
    comment
 | 
			
		||||
    webUrl
 | 
			
		||||
    sshUrl
 | 
			
		||||
    webHookSecret
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
`
 | 
			
		||||
@@ -1,8 +1,9 @@
 | 
			
		||||
import { ApolloClient } from "@apollo/client";
 | 
			
		||||
import { ApolloClient, InMemoryCache } from "@apollo/client";
 | 
			
		||||
import { prepareRoutes } from "@curi/router";
 | 
			
		||||
import { omit } from 'ramda';
 | 
			
		||||
import { ARTICLE } from "./articles";
 | 
			
		||||
import { Article } from './generated/graphql';
 | 
			
		||||
import { CreateProjectInput, Project } from './generated/graphql';
 | 
			
		||||
import { ProjectEditor } from './projects/project-editor';
 | 
			
		||||
import { PROJECT } from './projects/queries';
 | 
			
		||||
 | 
			
		||||
export default prepareRoutes([
 | 
			
		||||
  {
 | 
			
		||||
@@ -13,57 +14,48 @@ export default prepareRoutes([
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    name: "create-article",
 | 
			
		||||
    path: "articles/create",
 | 
			
		||||
    name: "create-project",
 | 
			
		||||
    path: "projects/create",
 | 
			
		||||
    respond({ resolved }) {
 | 
			
		||||
      const input: CreateProjectInput = {
 | 
			
		||||
        name: "",
 | 
			
		||||
        comment: "",
 | 
			
		||||
        webHookSecret: "",
 | 
			
		||||
        sshUrl: "",
 | 
			
		||||
        webUrl: "",
 | 
			
		||||
      };
 | 
			
		||||
      return { body: () => <ProjectEditor project={input} /> };
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    name: "edit-project",
 | 
			
		||||
    path: "projects/:projectId/edit",
 | 
			
		||||
    async resolve(matched, { client }: { client: ApolloClient<InMemoryCache> }) {
 | 
			
		||||
      const { data } = await client.query<{ project: Project }>({
 | 
			
		||||
        query: PROJECT,
 | 
			
		||||
        variables: { id: matched?.params.projectId },
 | 
			
		||||
      });
 | 
			
		||||
      return {
 | 
			
		||||
        body: () => (
 | 
			
		||||
          <ProjectEditor project={omit(["__typename"], data.project)} />
 | 
			
		||||
        ),
 | 
			
		||||
      };
 | 
			
		||||
    },
 | 
			
		||||
    respond({ resolved }) {
 | 
			
		||||
      return resolved;
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    name: "project-detail",
 | 
			
		||||
    path: "projects/:projectId",
 | 
			
		||||
    resolve() {
 | 
			
		||||
      const body = import(
 | 
			
		||||
        /* webpackChunkName: "article-editor" */ "./articles"
 | 
			
		||||
      ).then((m) => m.ArticleEditor);
 | 
			
		||||
        /* webpackChunkName: "article-editor" */ "./projects/project-panel"
 | 
			
		||||
      ).then((m) => m.ProjectPanel);
 | 
			
		||||
      return body;
 | 
			
		||||
    },
 | 
			
		||||
    respond({ resolved }) {
 | 
			
		||||
      return { body: resolved };
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    name: "modify-article",
 | 
			
		||||
    path: "articles/:id",
 | 
			
		||||
    async resolve(matched, { client }: { client: ApolloClient<any> }) {
 | 
			
		||||
      const [ArticleEditor, result] = await Promise.all([
 | 
			
		||||
        import(/* webpackChunkName: "article-editor" */ "./articles").then(
 | 
			
		||||
          (m) => m.ArticleEditor
 | 
			
		||||
        ),
 | 
			
		||||
        client.query<{article: Article}, { id: string }>({
 | 
			
		||||
          query: ARTICLE,
 | 
			
		||||
          variables: { id: matched!.params.id },
 | 
			
		||||
        }),
 | 
			
		||||
      ]);
 | 
			
		||||
      console.log(ArticleEditor, result);
 | 
			
		||||
      return () => (
 | 
			
		||||
        <ArticleEditor article={omit(["__typename"], result.data.article)} />
 | 
			
		||||
      );
 | 
			
		||||
    },
 | 
			
		||||
    respond({ resolved }) {
 | 
			
		||||
      return { body: resolved };
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    name: "articles",
 | 
			
		||||
    path: "articles",
 | 
			
		||||
    resolve() {
 | 
			
		||||
      return import(/* webpackChunkName: "articles" */ "./articles").then(
 | 
			
		||||
        (m) => m.ArticleIndex
 | 
			
		||||
      );
 | 
			
		||||
    },
 | 
			
		||||
    respond({ resolved }) {
 | 
			
		||||
      return { body: resolved };
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    name: "tags",
 | 
			
		||||
    path: "tags",
 | 
			
		||||
    respond() {
 | 
			
		||||
      return { body: () => <div>Tags</div> };
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
]);
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user