diff --git a/src/generated/graphql.schema.json b/src/generated/graphql.schema.json index 572ffa2..dc4a7f8 100644 --- a/src/generated/graphql.schema.json +++ b/src/generated/graphql.schema.json @@ -230,6 +230,33 @@ "enumValues": 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", "name": "DateTime", @@ -370,6 +397,105 @@ }, "isDeprecated": false, "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, @@ -464,6 +590,106 @@ }, "isDeprecated": false, "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, @@ -554,6 +780,45 @@ "enumValues": 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", "name": "Boolean", diff --git a/src/generated/graphql.tsx b/src/generated/graphql.tsx index 5906ab1..74f0db2 100644 --- a/src/generated/graphql.tsx +++ b/src/generated/graphql.tsx @@ -32,6 +32,10 @@ export type CreateArticleInput = { tags: Array; }; +export type CreateTagInput = { + name: Scalars['String']; +}; + export type Hello = { __typename?: 'Hello'; @@ -43,6 +47,9 @@ export type Mutation = { createArticle: Article; updateArticle: Article; removeArticle: Scalars['Int']; + createTag: Tag; + updateTag: Tag; + removeTag: Tag; }; @@ -60,11 +67,28 @@ export type MutationRemoveArticleArgs = { id: Scalars['String']; }; + +export type MutationCreateTagArgs = { + createTagInput: CreateTagInput; +}; + + +export type MutationUpdateTagArgs = { + updateTagInput: UpdateTagInput; +}; + + +export type MutationRemoveTagArgs = { + id: Scalars['String']; +}; + export type Query = { __typename?: 'Query'; hello: Hello; articles: Array
; article: Article; + tags: Array; + tag: Tag; }; @@ -72,6 +96,17 @@ export type QueryArticleArgs = { id: Scalars['String']; }; + +export type QueryTagArgs = { + id: Scalars['String']; +}; + +export type Tag = { + __typename?: 'Tag'; + id: Scalars['ID']; + name: Scalars['String']; +}; + export type UpdateArticleInput = { title?: Maybe; content?: Maybe; @@ -79,3 +114,8 @@ export type UpdateArticleInput = { tags?: Maybe>; id: Scalars['String']; }; + +export type UpdateTagInput = { + name?: Maybe; + id: Scalars['String']; +}; diff --git a/src/routes.tsx b/src/routes.tsx index a1955bc..5403532 100644 --- a/src/routes.tsx +++ b/src/routes.tsx @@ -33,7 +33,7 @@ export default prepareRoutes([ import(/* webpackChunkName: "article-editor" */ "./articles").then( (m) => m.ArticleEditor ), - client.query<{article: Article}, { id: string }>({ + client.query<{ article: Article }, { id: string }>({ query: ARTICLE, variables: { id: matched!.params.id }, }), @@ -62,8 +62,13 @@ export default prepareRoutes([ { name: "tags", path: "tags", - respond() { - return { body: () =>
Tags
}; + resolve() { + return import(/* webpackChunkName: "tags" */ "./tags").then( + (m) => m.TagsIndex + ); + }, + respond({ resolved }) { + return { body: resolved }; }, }, ]); diff --git a/src/tags/article-editor.tsx b/src/tags/article-editor.tsx new file mode 100644 index 0000000..cf4ec97 --- /dev/null +++ b/src/tags/article-editor.tsx @@ -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 = ({ 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 + ) => { + 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 ( + + + {({ submitForm, isSubmitting }) => ( +
+ + + + + + + + + + + +
+ )} +
+
+ ); +}; + +ArticleEditor.defaultProps = { + article: { + title: "", + content: "", + publishedAt: new Date(), + tags: [], + } as CreateArticleInput, +}; diff --git a/src/tags/index.ts b/src/tags/index.ts new file mode 100644 index 0000000..0a13245 --- /dev/null +++ b/src/tags/index.ts @@ -0,0 +1,3 @@ +export * from "./article-editor"; +export * from "./tags-index"; +export * from "./tags.constants"; diff --git a/src/tags/tags-index.tsx b/src/tags/tags-index.tsx new file mode 100644 index 0000000..c940e5a --- /dev/null +++ b/src/tags/tags-index.tsx @@ -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(REMOVE_TAG); + + const router = useRouter(); + + return ( +
+ + + + + Tag Name + Views + Articles + Actions + + + + {data?.tags.map((tag) => ( + + + {tag.name} + + -- + -- + + + router.navigate({ + url: router.url({ + name: "modify-tag", + params: tag, + }), + }) + } + > + + + + removeArticle({ + variables: tag, + }) + } + > + + + + + ))} + +
+
+
+ ); +}; diff --git a/src/tags/tags.constants.tsx b/src/tags/tags.constants.tsx new file mode 100644 index 0000000..d34232c --- /dev/null +++ b/src/tags/tags.constants.tsx @@ -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) + } +`;