diff --git a/.vscode/settings.json b/.vscode/settings.json
index 6711284..2cec82b 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -1,9 +1,10 @@
{
"cSpell.words": [
- "Formik",
"clsx",
"fontsource",
+ "Formik",
"notistack",
+ "Swipeable",
"vditor"
]
}
\ No newline at end of file
diff --git a/src/articles/article-editor-history.tsx b/src/articles/article-editor-history.tsx
new file mode 100644
index 0000000..4f8f632
--- /dev/null
+++ b/src/articles/article-editor-history.tsx
@@ -0,0 +1,5 @@
+import { FC } from "react";
+
+export const ArticleEditorHistory: FC = () => {
+ return
ArticleEditorHistory
;
+};
diff --git a/src/articles/article-editor.tsx b/src/articles/article-editor.tsx
index 5fb4eb1..d324760 100644
--- a/src/articles/article-editor.tsx
+++ b/src/articles/article-editor.tsx
@@ -1,21 +1,39 @@
-import { Button, Grid, Paper } from "@mui/material";
-import createStyles from '@mui/styles/createStyles';
-import makeStyles from '@mui/styles/makeStyles';
+import {
+ Badge,
+ Box,
+ Button,
+ Grid,
+ IconButton,
+ Paper,
+ SwipeableDrawer,
+ Typography,
+ useTheme,
+} from "@mui/material";
+import createStyles from "@mui/styles/createStyles";
+import makeStyles from "@mui/styles/makeStyles";
import { Field, Form, Formik, FormikHelpers, FormikProps } from "formik";
-import { FC, useCallback, useMemo, useRef, useState } from "react";
+import { FC, useCallback, useRef, useState } from "react";
import { Editor } from "../commons/editor/vditor";
import * as Yup from "yup";
import {
Article,
CreateArticleInput,
- CreateDraftArticleInput,
- DraftArticle,
+ ArticleHistory,
UpdateArticleInput,
} from "../generated/graphql";
import { DateTimePicker } from "formik-material-ui-pickers";
import { gql, useMutation } from "@apollo/client";
import * as R from "ramda";
import { useNavigate } from "react-router-dom";
+import { useDrafts } from "./hooks/useDrafts";
+import Timeline from "@mui/lab/Timeline";
+import TimelineItem from "@mui/lab/TimelineItem";
+import TimelineSeparator from "@mui/lab/TimelineSeparator";
+import TimelineConnector from "@mui/lab/TimelineConnector";
+import TimelineContent from "@mui/lab/TimelineContent";
+import TimelineDot from "@mui/lab/TimelineDot";
+import { HistoryEduOutlined } from "@mui/icons-material";
+import { format } from "date-fns";
const CREATE_ARTICLE = gql`
mutation createArticle($createArticleInput: CreateArticleInput!) {
@@ -52,15 +70,17 @@ const useStyles = makeStyles((theme) =>
const fields = ["title", "content", "publishedAt", "tags"] as const;
type Values = Pick &
- Partial>;
+ Partial>;
interface Props {
article?: Values;
+ draftKey: string;
}
-export const ArticleEditor: FC = ({ article }) => {
+export const ArticleEditor: FC = ({ article, draftKey }) => {
const classes = useStyles();
const navigate = useNavigate();
const formik = useRef>(null);
+ const { draft, putDraft, drafts, getDraft } = useDrafts(draftKey);
const validationSchema = Yup.object({
content: Yup.string()
@@ -100,30 +120,6 @@ export const ArticleEditor: FC = ({ article }) => {
updateArticleInput: UpdateArticleInput;
}>(UPDATE_ARTICLE);
- const [putDraft] = useMutation<
- {
- draft: DraftArticle;
- },
- {
- draft: CreateDraftArticleInput;
- }
- >(gql`
- mutation ($draft: CreateDraftArticleInput!) {
- draft: putDraftForArticle(draft: $draft) {
- id
- createdAt
- }
- }
- `);
-
- const tempKey = useMemo(() => {
- if (article?.key) {
- return article.key;
- } else {
- return new Date().valueOf().toString();
- }
- }, [article]);
-
const submitForm = async (
values: Values,
{ setSubmitting }: FormikHelpers
@@ -174,33 +170,15 @@ export const ArticleEditor: FC = ({ article }) => {
setSubmitting(false);
};
- const [drafts, setDrafts] = useState([]);
const handleSave = useCallback(
async (automatically = true) => {
- const { data } = await putDraft({
- variables: {
- draft: {
- key: tempKey,
- payload: formik.current?.values,
- automatically,
- },
- },
- });
- if (!data?.draft) {
- return;
- }
- setDrafts((drafts) => {
- const index = drafts.findIndex((it) => it.id === data.draft.id);
- if (index === -1) {
- return [data.draft, ...drafts];
- } else {
- const arr = [...drafts];
- arr[index] = data.draft;
- return arr;
- }
+ await putDraft({
+ key: draftKey,
+ payload: formik.current?.values,
+ automatically,
});
},
- [tempKey, putDraft, formik, drafts]
+ [draftKey, putDraft, formik]
);
return (
@@ -251,6 +229,11 @@ export const ArticleEditor: FC = ({ article }) => {
>
Publish
+
+ getDraft(draft.id)}
+ />
@@ -260,6 +243,78 @@ export const ArticleEditor: FC = ({ article }) => {
);
};
+interface HistoryDrawerProps {
+ drafts?: DraftArticle[];
+ setCurrentDraft: (draft: DraftArticle) => void;
+}
+
+const HistoryDrawer: FC = ({ drafts, setCurrentDraft }) => {
+ const theme = useTheme();
+ const [open, setOpen] = useState(false);
+ return (
+ <>
+ setOpen(!open)}>
+
+
+
+
+ setOpen(false)}
+ onOpen={() => setOpen(true)}
+ swipeAreaWidth={50}
+ disableSwipeToOpen={false}
+ ModalProps={{
+ keepMounted: true,
+ BackdropProps: {
+ sx: {
+ backgroundColor: "transparent",
+ },
+ },
+ }}
+ >
+
+
+ {drafts?.map((draft, index) => (
+
+
+
+
+
+
+
+ {index + 1} - {format(draft.updatedAt, "yyyy-MM-dd")}
+
+ {format(draft.updatedAt, "HH:mm:ss")}
+
+
+ ))}
+
+
+
+ >
+ );
+};
+
ArticleEditor.defaultProps = {
article: {
title: "",
diff --git a/src/articles/articles.constants.tsx b/src/articles/articles.constants.tsx
index 80b838a..9185a44 100644
--- a/src/articles/articles.constants.tsx
+++ b/src/articles/articles.constants.tsx
@@ -7,6 +7,11 @@ export const ARTICLE = gql`
title
content
publishedAt
+ histories {
+ updatedAt
+ payload
+ automatically
+ }
}
}
`;
diff --git a/src/articles/hooks/useDrafts.tsx b/src/articles/hooks/useDrafts.tsx
new file mode 100644
index 0000000..0f6a897
--- /dev/null
+++ b/src/articles/hooks/useDrafts.tsx
@@ -0,0 +1,147 @@
+import { gql, useLazyQuery, useMutation, useQuery } from "@apollo/client";
+import { useCallback, useState } from "react";
+import { CreateDraftArticleInput, DraftArticle } from "../../generated/graphql";
+
+export interface useDraftsProps {
+ key: string;
+}
+
+export interface useDraftsReturn {
+ drafts?: DraftArticle[];
+ draft?: DraftArticle;
+ listLoading: boolean;
+ itemLoading: boolean;
+ getDraft: (id: string) => Promise;
+ putDraft: (input: CreateDraftArticleInput) => Promise;
+}
+
+export const useDrafts = (key: string): useDraftsReturn => {
+ const { data: { drafts, lastDraft: last } = {}, loading: listLoading } =
+ useQuery<
+ { drafts: DraftArticle[]; lastDraft: DraftArticle },
+ { key: string }
+ >(
+ gql`
+ query DraftArticles($key: String!) {
+ drafts: listDraftForArticle(key: $key) {
+ id
+ key
+ automatically
+ updatedAt
+ }
+ lastDraft: lastDraftForArticle(key: $key) {
+ id
+ key
+ payload
+ automatically
+ updatedAt
+ }
+ }
+ `,
+ {
+ variables: { key },
+ }
+ );
+ const [
+ get,
+ { data: { draft } = { draft: undefined }, loading: itemLoading },
+ ] = useLazyQuery<
+ {
+ draft: DraftArticle;
+ },
+ {
+ id: string;
+ }
+ >(
+ gql`
+ query DraftArticles($id: String!) {
+ draft: lastDraftForArticle(id: $id) {
+ id
+ key
+ payload
+ automatically
+ updatedAt
+ }
+ }
+ `
+ );
+
+ const [put] = useMutation<
+ {
+ draft: DraftArticle;
+ },
+ {
+ draft: CreateDraftArticleInput;
+ }
+ >(
+ gql`
+ mutation PutDraftForArticle($draft: CreateDraftArticleInput!) {
+ draft: putDraftForArticle(draft: $draft) {
+ id
+ key
+ payload
+ automatically
+ updatedAt
+ }
+ }
+ `,
+ {
+ update(cache, { data }) {
+ if (data?.draft) {
+ const newDraftRef = cache.writeFragment({
+ data: data.draft,
+ fragment: gql`
+ fragment NewDraftArticle on DraftArticle {
+ id
+ key
+ payload
+ automatically
+ updatedAt
+ }
+ `,
+ });
+ cache.modify({
+ fields: {
+ drafts(existingDrafts: DraftArticle[] = [], { readField }) {
+ return [
+ newDraftRef,
+ ...existingDrafts.filter(
+ (draft: any) => readField("id", draft) !== data.draft.id
+ ),
+ ];
+ },
+ lastDraft() {
+ return newDraftRef;
+ },
+ },
+ });
+ }
+ },
+ }
+ );
+
+ const getDraft = useCallback(
+ async (id: string) => {
+ return get({ variables: { id } }).then(({ data }) => data!.draft);
+ },
+ [get]
+ );
+
+ const putDraft = useCallback(
+ async (input: CreateDraftArticleInput) => {
+ return put({ variables: { draft: input } }).then(
+ ({ data }) => data!.draft
+ );
+ },
+ [put]
+ );
+
+ return {
+ drafts,
+ draft: draft ?? last,
+ listLoading,
+ itemLoading,
+ getDraft,
+ putDraft,
+ };
+};
diff --git a/src/generated/graphql.schema.json b/src/generated/graphql.schema.json
index 34ee416..b4b4a9f 100644
--- a/src/generated/graphql.schema.json
+++ b/src/generated/graphql.schema.json
@@ -97,18 +97,6 @@
"isDeprecated": false,
"deprecationReason": null
},
- {
- "name": "key",
- "description": null,
- "args": [],
- "type": {
- "kind": "SCALAR",
- "name": "String",
- "ofType": null
- },
- "isDeprecated": false,
- "deprecationReason": null
- },
{
"name": "html",
"description": null,
@@ -136,6 +124,30 @@
},
"isDeprecated": false,
"deprecationReason": null
+ },
+ {
+ "name": "histories",
+ "description": null,
+ "args": [],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "ArticleHistory",
+ "ofType": null
+ }
+ }
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
}
],
"inputFields": null,
@@ -163,6 +175,91 @@
"enumValues": null,
"possibleTypes": null
},
+ {
+ "kind": "OBJECT",
+ "name": "ArticleHistory",
+ "description": null,
+ "fields": [
+ {
+ "name": "payload",
+ "description": null,
+ "args": [],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Object",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "updatedAt",
+ "description": null,
+ "args": [],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "DateTime",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "automatic",
+ "description": null,
+ "args": [],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "published",
+ "description": null,
+ "args": [],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "description": "The `Boolean` scalar type represents `true` or `false`.",
+ "fields": null,
+ "inputFields": null,
+ "interfaces": null,
+ "enumValues": null,
+ "possibleTypes": null
+ },
{
"kind": "INPUT_OBJECT",
"name": "CreateArticleInput",
@@ -242,71 +339,6 @@
"enumValues": null,
"possibleTypes": null
},
- {
- "kind": "INPUT_OBJECT",
- "name": "CreateDraftArticleInput",
- "description": null,
- "fields": null,
- "inputFields": [
- {
- "name": "payload",
- "description": null,
- "type": {
- "kind": "NON_NULL",
- "name": null,
- "ofType": {
- "kind": "SCALAR",
- "name": "Object",
- "ofType": null
- }
- },
- "defaultValue": null,
- "isDeprecated": false,
- "deprecationReason": null
- },
- {
- "name": "key",
- "description": null,
- "type": {
- "kind": "NON_NULL",
- "name": null,
- "ofType": {
- "kind": "SCALAR",
- "name": "String",
- "ofType": null
- }
- },
- "defaultValue": null,
- "isDeprecated": false,
- "deprecationReason": null
- },
- {
- "name": "automatically",
- "description": null,
- "type": {
- "kind": "SCALAR",
- "name": "Boolean",
- "ofType": null
- },
- "defaultValue": "true",
- "isDeprecated": false,
- "deprecationReason": null
- }
- ],
- "interfaces": null,
- "enumValues": null,
- "possibleTypes": null
- },
- {
- "kind": "SCALAR",
- "name": "Boolean",
- "description": "The `Boolean` scalar type represents `true` or `false`.",
- "fields": null,
- "inputFields": null,
- "interfaces": null,
- "enumValues": null,
- "possibleTypes": null
- },
{
"kind": "INPUT_OBJECT",
"name": "CreateTagInput",
@@ -344,81 +376,6 @@
"enumValues": null,
"possibleTypes": null
},
- {
- "kind": "OBJECT",
- "name": "DraftArticle",
- "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": "payload",
- "description": null,
- "args": [],
- "type": {
- "kind": "NON_NULL",
- "name": null,
- "ofType": {
- "kind": "SCALAR",
- "name": "Object",
- "ofType": null
- }
- },
- "isDeprecated": false,
- "deprecationReason": null
- },
- {
- "name": "key",
- "description": null,
- "args": [],
- "type": {
- "kind": "NON_NULL",
- "name": null,
- "ofType": {
- "kind": "SCALAR",
- "name": "String",
- "ofType": null
- }
- },
- "isDeprecated": false,
- "deprecationReason": null
- },
- {
- "name": "automatically",
- "description": null,
- "args": [],
- "type": {
- "kind": "NON_NULL",
- "name": null,
- "ofType": {
- "kind": "SCALAR",
- "name": "Boolean",
- "ofType": null
- }
- },
- "isDeprecated": false,
- "deprecationReason": null
- }
- ],
- "inputFields": null,
- "interfaces": [],
- "enumValues": null,
- "possibleTypes": null
- },
{
"kind": "OBJECT",
"name": "Hello",
@@ -550,113 +507,6 @@
"isDeprecated": false,
"deprecationReason": null
},
- {
- "name": "putDraftForArticle",
- "description": null,
- "args": [
- {
- "name": "draft",
- "description": null,
- "type": {
- "kind": "NON_NULL",
- "name": null,
- "ofType": {
- "kind": "INPUT_OBJECT",
- "name": "CreateDraftArticleInput",
- "ofType": null
- }
- },
- "defaultValue": null,
- "isDeprecated": false,
- "deprecationReason": null
- }
- ],
- "type": {
- "kind": "NON_NULL",
- "name": null,
- "ofType": {
- "kind": "OBJECT",
- "name": "DraftArticle",
- "ofType": null
- }
- },
- "isDeprecated": false,
- "deprecationReason": null
- },
- {
- "name": "listDraftForArticle",
- "description": null,
- "args": [
- {
- "name": "key",
- "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": "LIST",
- "name": null,
- "ofType": {
- "kind": "NON_NULL",
- "name": null,
- "ofType": {
- "kind": "OBJECT",
- "name": "DraftArticle",
- "ofType": null
- }
- }
- }
- },
- "isDeprecated": false,
- "deprecationReason": null
- },
- {
- "name": "lastDraftForArticle",
- "description": null,
- "args": [
- {
- "name": "key",
- "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": "DraftArticle",
- "ofType": null
- }
- },
- "isDeprecated": false,
- "deprecationReason": null
- },
{
"name": "createTag",
"description": null,
@@ -775,7 +625,7 @@
{
"kind": "SCALAR",
"name": "Object",
- "description": "The `JSON` scalar type represents JSON values as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf).",
+ "description": "The `JSONObject` scalar type represents JSON objects as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf).",
"fields": null,
"inputFields": null,
"interfaces": null,
diff --git a/src/generated/graphql.tsx b/src/generated/graphql.tsx
index 931c182..7ecf9c5 100644
--- a/src/generated/graphql.tsx
+++ b/src/generated/graphql.tsx
@@ -12,7 +12,7 @@ export type Scalars = {
Float: number;
/** A date-time string at UTC, such as 2019-12-03T09:54:33Z, compliant with the date-time format. */
DateTime: any;
- /** The `JSON` scalar type represents JSON values as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf). */
+ /** The `JSONObject` scalar type represents JSON objects as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf). */
Object: any;
};
@@ -23,9 +23,17 @@ export type Article = {
content: Scalars['String'];
publishedAt?: Maybe;
tags: Array;
- key?: Maybe;
html: Scalars['String'];
description?: Maybe;
+ histories: Array;
+};
+
+export type ArticleHistory = {
+ __typename?: 'ArticleHistory';
+ payload: Scalars['Object'];
+ updatedAt: Scalars['DateTime'];
+ automatic: Scalars['Boolean'];
+ published: Scalars['Boolean'];
};
export type CreateArticleInput = {
@@ -35,25 +43,11 @@ export type CreateArticleInput = {
tags: Array;
};
-export type CreateDraftArticleInput = {
- payload: Scalars['Object'];
- key: Scalars['String'];
- automatically?: Maybe;
-};
-
export type CreateTagInput = {
name: Scalars['String'];
};
-export type DraftArticle = {
- __typename?: 'DraftArticle';
- id: Scalars['ID'];
- payload: Scalars['Object'];
- key: Scalars['String'];
- automatically: Scalars['Boolean'];
-};
-
export type Hello = {
__typename?: 'Hello';
message: Scalars['String'];
@@ -64,9 +58,6 @@ export type Mutation = {
createArticle: Article;
updateArticle: Article;
removeArticle: Scalars['Int'];
- putDraftForArticle: DraftArticle;
- listDraftForArticle: Array;
- lastDraftForArticle: DraftArticle;
createTag: Tag;
updateTag: Tag;
removeTag: Tag;
@@ -88,21 +79,6 @@ export type MutationRemoveArticleArgs = {
};
-export type MutationPutDraftForArticleArgs = {
- draft: CreateDraftArticleInput;
-};
-
-
-export type MutationListDraftForArticleArgs = {
- key: Scalars['String'];
-};
-
-
-export type MutationLastDraftForArticleArgs = {
- key: Scalars['String'];
-};
-
-
export type MutationCreateTagArgs = {
createTagInput: CreateTagInput;
};
diff --git a/src/routes.tsx b/src/routes.tsx
index 12dbb35..02d98b7 100644
--- a/src/routes.tsx
+++ b/src/routes.tsx
@@ -1,18 +1,31 @@
import { useApolloClient } from "@apollo/client";
import * as R from "ramda";
import { FC, lazy, useMemo } from "react";
-import { useParams, useRoutes } from "react-router-dom";
+import {
+ Navigate,
+ useParams,
+ useRoutes,
+ useSearchParams,
+} from "react-router-dom";
import { ARTICLE } from "./articles";
import { Article } from "./generated/graphql";
import { DefaultLayout } from "./layouts";
-const CreateArticle = lazy(() =>
- import(
- /* webpackChunkName: "article-editor" */ "./articles/article-editor"
- ).then((m) => ({
- default: m.ArticleEditor,
- }))
-);
+const CreateArticle = () => {
+ const ArticleEditor = lazy(() =>
+ import(
+ /* webpackChunkName: "article-editor" */ "./articles/article-editor"
+ ).then((m) => ({
+ default: m.ArticleEditor,
+ }))
+ );
+ const [search] = useSearchParams();
+ if (!search.has("key")) {
+ search.set("key", new Date().toISOString());
+ return ;
+ }
+ return ;
+};
const ModifyArticle: FC = () => {
const { id } = useParams();
@@ -33,7 +46,11 @@ const ModifyArticle: FC = () => {
return R.omit(["__typename"], data.article);
}),
]);
- return { default: () => };
+ return {
+ default: () => (
+
+ ),
+ };
}),
[id, client]
);