feat(projects): list, create and update.

This commit is contained in:
Ivan Li 2021-05-04 21:08:00 +08:00
parent 7d04d53d02
commit f810aedfaa
18 changed files with 3199 additions and 633 deletions

2
.vscode/launch.json vendored
View File

@ -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"
}

View File

@ -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'
}
}
};

View File

@ -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:

View File

@ -1,7 +1,7 @@
module.exports = {
apps: [
{
name: "blog-bs",
name: "fennec-bs",
script: "serve",
args: "",
watch: false,

File diff suppressed because it is too large Load Diff

922
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -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"
}
}

View File

@ -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,
};

View File

@ -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>
);
};

View File

@ -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)
}
`;

View File

@ -1,3 +0,0 @@
export * from './article-editor';
export * from './articles-index';
export * from './articles.constants'

View File

@ -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>
);
};

View File

@ -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>;
};

View File

@ -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}>

View 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>
);
};

View 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
View 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
}
}
`

View File

@ -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> };
},
},
]);