feat: tags list.

This commit is contained in:
Ivan Li 2021-07-24 10:33:32 +08:00
parent 9490d874a9
commit c5ca8819e7
7 changed files with 613 additions and 3 deletions

View File

@ -230,6 +230,33 @@
"enumValues": null, "enumValues": null,
"possibleTypes": null "possibleTypes": null
}, },
{
"kind": "INPUT_OBJECT",
"name": "CreateTagInput",
"description": null,
"fields": null,
"inputFields": [
{
"name": "name",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{ {
"kind": "SCALAR", "kind": "SCALAR",
"name": "DateTime", "name": "DateTime",
@ -370,6 +397,105 @@
}, },
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null
},
{
"name": "createTag",
"description": null,
"args": [
{
"name": "createTagInput",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "INPUT_OBJECT",
"name": "CreateTagInput",
"ofType": null
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "Tag",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "updateTag",
"description": null,
"args": [
{
"name": "updateTagInput",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "INPUT_OBJECT",
"name": "UpdateTagInput",
"ofType": null
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "Tag",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "removeTag",
"description": null,
"args": [
{
"name": "id",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "Tag",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
} }
], ],
"inputFields": null, "inputFields": null,
@ -464,6 +590,106 @@
}, },
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null
},
{
"name": "tags",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "Tag",
"ofType": null
}
}
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "tag",
"description": null,
"args": [
{
"name": "id",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "Tag",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "Tag",
"description": null,
"fields": [
{
"name": "id",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "name",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
} }
], ],
"inputFields": null, "inputFields": null,
@ -554,6 +780,45 @@
"enumValues": null, "enumValues": null,
"possibleTypes": null "possibleTypes": null
}, },
{
"kind": "INPUT_OBJECT",
"name": "UpdateTagInput",
"description": null,
"fields": null,
"inputFields": [
{
"name": "name",
"description": null,
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "id",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{ {
"kind": "SCALAR", "kind": "SCALAR",
"name": "Boolean", "name": "Boolean",

View File

@ -32,6 +32,10 @@ export type CreateArticleInput = {
tags: Array<Scalars['String']>; tags: Array<Scalars['String']>;
}; };
export type CreateTagInput = {
name: Scalars['String'];
};
export type Hello = { export type Hello = {
__typename?: 'Hello'; __typename?: 'Hello';
@ -43,6 +47,9 @@ export type Mutation = {
createArticle: Article; createArticle: Article;
updateArticle: Article; updateArticle: Article;
removeArticle: Scalars['Int']; removeArticle: Scalars['Int'];
createTag: Tag;
updateTag: Tag;
removeTag: Tag;
}; };
@ -60,11 +67,28 @@ export type MutationRemoveArticleArgs = {
id: Scalars['String']; id: Scalars['String'];
}; };
export type MutationCreateTagArgs = {
createTagInput: CreateTagInput;
};
export type MutationUpdateTagArgs = {
updateTagInput: UpdateTagInput;
};
export type MutationRemoveTagArgs = {
id: Scalars['String'];
};
export type Query = { export type Query = {
__typename?: 'Query'; __typename?: 'Query';
hello: Hello; hello: Hello;
articles: Array<Article>; articles: Array<Article>;
article: Article; article: Article;
tags: Array<Tag>;
tag: Tag;
}; };
@ -72,6 +96,17 @@ export type QueryArticleArgs = {
id: Scalars['String']; id: Scalars['String'];
}; };
export type QueryTagArgs = {
id: Scalars['String'];
};
export type Tag = {
__typename?: 'Tag';
id: Scalars['ID'];
name: Scalars['String'];
};
export type UpdateArticleInput = { export type UpdateArticleInput = {
title?: Maybe<Scalars['String']>; title?: Maybe<Scalars['String']>;
content?: Maybe<Scalars['String']>; content?: Maybe<Scalars['String']>;
@ -79,3 +114,8 @@ export type UpdateArticleInput = {
tags?: Maybe<Array<Scalars['String']>>; tags?: Maybe<Array<Scalars['String']>>;
id: Scalars['String']; id: Scalars['String'];
}; };
export type UpdateTagInput = {
name?: Maybe<Scalars['String']>;
id: Scalars['String'];
};

View File

@ -33,7 +33,7 @@ export default prepareRoutes([
import(/* webpackChunkName: "article-editor" */ "./articles").then( import(/* webpackChunkName: "article-editor" */ "./articles").then(
(m) => m.ArticleEditor (m) => m.ArticleEditor
), ),
client.query<{article: Article}, { id: string }>({ client.query<{ article: Article }, { id: string }>({
query: ARTICLE, query: ARTICLE,
variables: { id: matched!.params.id }, variables: { id: matched!.params.id },
}), }),
@ -62,8 +62,13 @@ export default prepareRoutes([
{ {
name: "tags", name: "tags",
path: "tags", path: "tags",
respond() { resolve() {
return { body: () => <div>Tags</div> }; return import(/* webpackChunkName: "tags" */ "./tags").then(
(m) => m.TagsIndex
);
},
respond({ resolved }) {
return { body: resolved };
}, },
}, },
]); ]);

201
src/tags/article-editor.tsx Normal file
View File

@ -0,0 +1,201 @@
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,
};

3
src/tags/index.ts Normal file
View File

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

80
src/tags/tags-index.tsx Normal file
View File

@ -0,0 +1,80 @@
import { useMutation, useQuery } from "@apollo/client";
import {
IconButton,
Paper,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
} from "@material-ui/core";
import { FC } from "react";
import { Tag } from "../generated/graphql";
import EditIcon from "@material-ui/icons/Edit";
import { useRouter } from "@curi/react-dom";
import { Delete } from "@material-ui/icons";
import { REMOVE_TAG, TAGS } from "./tags.constants";
export const TagsIndex: FC = () => {
const { data } = useQuery<{
tags: Tag[];
}>(TAGS, {});
const [removeArticle] = useMutation<any, { id: string }>(REMOVE_TAG);
const router = useRouter();
return (
<section>
<TableContainer component={Paper}>
<Table aria-label="articles table">
<TableHead>
<TableRow>
<TableCell>Tag Name</TableCell>
<TableCell>Views</TableCell>
<TableCell>Articles</TableCell>
<TableCell>Actions</TableCell>
</TableRow>
</TableHead>
<TableBody>
{data?.tags.map((tag) => (
<TableRow key={tag.id}>
<TableCell component="th" scope="row">
{tag.name}
</TableCell>
<TableCell align="right"> -- </TableCell>
<TableCell align="right"> -- </TableCell>
<TableCell>
<IconButton
aria-label="edit"
onClick={() =>
router.navigate({
url: router.url({
name: "modify-tag",
params: tag,
}),
})
}
>
<EditIcon />
</IconButton>
<IconButton
aria-label="delete"
onClick={() =>
removeArticle({
variables: tag,
})
}
>
<Delete />
</IconButton>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</section>
);
};

View File

@ -0,0 +1,16 @@
import { gql } from "@apollo/client";
export const TAGS = gql`
query Tags {
tags {
id
name
}
}
`;
export const REMOVE_TAG = gql`
mutation RemoveTag($id: ID!) {
removeTag(id: $id)
}
`;