Compare commits
4 Commits
develop
...
migration-
Author | SHA1 | Date | |
---|---|---|---|
6fabc1243b | |||
48dd4a88bf | |||
fcd72690d5 | |||
218a6c4c3a |
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@ -1,9 +1,10 @@
|
|||||||
{
|
{
|
||||||
"cSpell.words": [
|
"cSpell.words": [
|
||||||
"Formik",
|
|
||||||
"clsx",
|
"clsx",
|
||||||
"fontsource",
|
"fontsource",
|
||||||
|
"Formik",
|
||||||
"notistack",
|
"notistack",
|
||||||
|
"Swipeable",
|
||||||
"vditor"
|
"vditor"
|
||||||
]
|
]
|
||||||
}
|
}
|
8375
package-lock.json
generated
8375
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
14
package.json
14
package.json
@ -5,15 +5,15 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@apollo/client": "^3.3.15",
|
"@apollo/client": "^3.3.15",
|
||||||
"@craco/craco": "^6.4.0",
|
"@craco/craco": "^6.4.0",
|
||||||
"@curi/react-dom": "^2.0.4",
|
|
||||||
"@curi/router": "^2.1.2",
|
|
||||||
"@date-io/date-fns": "^1.3.13",
|
"@date-io/date-fns": "^1.3.13",
|
||||||
|
"@emotion/react": "^11.6.0",
|
||||||
|
"@emotion/styled": "^11.6.0",
|
||||||
"@fennec/configuration": "^0.0.1",
|
"@fennec/configuration": "^0.0.1",
|
||||||
"@hickory/browser": "^2.1.0",
|
"@hickory/browser": "^2.1.0",
|
||||||
"@material-ui/core": "^4.11.3",
|
"@mui/icons-material": "^5.2.0",
|
||||||
"@material-ui/icons": "^4.11.2",
|
"@mui/lab": "^5.0.0-alpha.56",
|
||||||
"@material-ui/lab": "*",
|
"@mui/material": "^5.2.0",
|
||||||
"@material-ui/pickers": "^3.3.10",
|
"@mui/styles": "^5.2.0",
|
||||||
"@nestjs-lib/auth": "^0.1.1",
|
"@nestjs-lib/auth": "^0.1.1",
|
||||||
"@testing-library/jest-dom": "^5.11.10",
|
"@testing-library/jest-dom": "^5.11.10",
|
||||||
"@testing-library/react": "^11.2.6",
|
"@testing-library/react": "^11.2.6",
|
||||||
@ -39,6 +39,7 @@
|
|||||||
"ramda": "^0.27.1",
|
"ramda": "^0.27.1",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
|
"react-router-dom": "^6.0.2",
|
||||||
"react-scripts": "4.0.3",
|
"react-scripts": "4.0.3",
|
||||||
"subscriptions-transport-ws": "^0.9.19",
|
"subscriptions-transport-ws": "^0.9.19",
|
||||||
"typescript": "^4.2.4",
|
"typescript": "^4.2.4",
|
||||||
@ -79,6 +80,7 @@
|
|||||||
"@graphql-codegen/typescript-react-apollo": "2.2.3",
|
"@graphql-codegen/typescript-react-apollo": "2.2.3",
|
||||||
"@types/graphql": "^14.5.0",
|
"@types/graphql": "^14.5.0",
|
||||||
"@types/ramda": "^0.27.40",
|
"@types/ramda": "^0.27.40",
|
||||||
|
"@types/react-router-dom": "^5.3.2",
|
||||||
"@types/sass": "^1.16.0",
|
"@types/sass": "^1.16.0",
|
||||||
"@types/yup": "^0.29.11",
|
"@types/yup": "^0.29.11",
|
||||||
"sass": "^1.32.11"
|
"sass": "^1.32.11"
|
||||||
|
15
src/App.tsx
15
src/App.tsx
@ -1,15 +1,12 @@
|
|||||||
import { useResponse } from '@curi/react-dom';
|
import { BrowserRouter } from "react-router-dom";
|
||||||
import './App.css';
|
import "./App.css";
|
||||||
import { DefaultLayout } from './layouts';
|
import { AppRoutes } from "./routes";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const { response } = useResponse();
|
|
||||||
|
|
||||||
const { body: Body } = response;
|
|
||||||
return (
|
return (
|
||||||
<DefaultLayout>
|
<BrowserRouter>
|
||||||
<Body response={response} />
|
<AppRoutes />
|
||||||
</DefaultLayout>
|
</BrowserRouter>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
5
src/articles/article-editor-history.tsx
Normal file
5
src/articles/article-editor-history.tsx
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { FC } from "react";
|
||||||
|
|
||||||
|
export const ArticleEditorHistory: FC = () => {
|
||||||
|
return <div>ArticleEditorHistory</div>;
|
||||||
|
};
|
@ -1,25 +1,39 @@
|
|||||||
import {
|
import {
|
||||||
|
Badge,
|
||||||
|
Box,
|
||||||
Button,
|
Button,
|
||||||
createStyles,
|
|
||||||
Grid,
|
Grid,
|
||||||
makeStyles,
|
IconButton,
|
||||||
Paper,
|
Paper,
|
||||||
} from "@material-ui/core";
|
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 { 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 { Editor } from "../commons/editor/vditor";
|
||||||
import * as Yup from "yup";
|
import * as Yup from "yup";
|
||||||
import {
|
import {
|
||||||
Article,
|
Article,
|
||||||
CreateArticleInput,
|
CreateArticleInput,
|
||||||
CreateDraftArticleInput,
|
ArticleHistory,
|
||||||
DraftArticle,
|
|
||||||
UpdateArticleInput,
|
UpdateArticleInput,
|
||||||
} from "../generated/graphql";
|
} from "../generated/graphql";
|
||||||
import { DateTimePicker } from "formik-material-ui-pickers";
|
import { DateTimePicker } from "formik-material-ui-pickers";
|
||||||
import { gql, useMutation } from "@apollo/client";
|
import { gql, useMutation } from "@apollo/client";
|
||||||
import { useRouter } from "@curi/react-universal";
|
|
||||||
import * as R from "ramda";
|
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`
|
const CREATE_ARTICLE = gql`
|
||||||
mutation createArticle($createArticleInput: CreateArticleInput!) {
|
mutation createArticle($createArticleInput: CreateArticleInput!) {
|
||||||
@ -56,15 +70,17 @@ const useStyles = makeStyles((theme) =>
|
|||||||
|
|
||||||
const fields = ["title", "content", "publishedAt", "tags"] as const;
|
const fields = ["title", "content", "publishedAt", "tags"] as const;
|
||||||
type Values = Pick<Article, typeof fields[number]> &
|
type Values = Pick<Article, typeof fields[number]> &
|
||||||
Partial<Pick<Article, "id" | "key">>;
|
Partial<Pick<Article, "id">>;
|
||||||
interface Props {
|
interface Props {
|
||||||
article?: Values;
|
article?: Values;
|
||||||
|
draftKey: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ArticleEditor: FC<Props> = ({ article }) => {
|
export const ArticleEditor: FC<Props> = ({ article, draftKey }) => {
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
const router = useRouter();
|
const navigate = useNavigate();
|
||||||
const formik = useRef<FormikProps<Values>>(null);
|
const formik = useRef<FormikProps<Values>>(null);
|
||||||
|
const { draft, putDraft, drafts, getDraft } = useDrafts(draftKey);
|
||||||
|
|
||||||
const validationSchema = Yup.object({
|
const validationSchema = Yup.object({
|
||||||
content: Yup.string()
|
content: Yup.string()
|
||||||
@ -104,30 +120,6 @@ export const ArticleEditor: FC<Props> = ({ article }) => {
|
|||||||
updateArticleInput: UpdateArticleInput;
|
updateArticleInput: UpdateArticleInput;
|
||||||
}>(UPDATE_ARTICLE);
|
}>(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 (
|
const submitForm = async (
|
||||||
values: Values,
|
values: Values,
|
||||||
{ setSubmitting }: FormikHelpers<Values>
|
{ setSubmitting }: FormikHelpers<Values>
|
||||||
@ -148,10 +140,7 @@ export const ArticleEditor: FC<Props> = ({ article }) => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
router.navigate({
|
navigate("/articles", { replace: true });
|
||||||
url: router.url({ name: "articles" }),
|
|
||||||
method: "replace",
|
|
||||||
});
|
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
@ -172,10 +161,7 @@ export const ArticleEditor: FC<Props> = ({ article }) => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
router.navigate({
|
navigate("/articles", { replace: true });
|
||||||
url: router.url({ name: "articles" }),
|
|
||||||
method: "replace",
|
|
||||||
});
|
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
@ -184,33 +170,15 @@ export const ArticleEditor: FC<Props> = ({ article }) => {
|
|||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const [drafts, setDrafts] = useState<DraftArticle[]>([]);
|
|
||||||
const handleSave = useCallback(
|
const handleSave = useCallback(
|
||||||
async (automatically = true) => {
|
async (automatically = true) => {
|
||||||
const { data } = await putDraft({
|
await putDraft({
|
||||||
variables: {
|
key: draftKey,
|
||||||
draft: {
|
payload: formik.current?.values,
|
||||||
key: tempKey,
|
automatically,
|
||||||
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;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[tempKey, putDraft, formik, drafts]
|
[draftKey, putDraft, formik]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -261,6 +229,11 @@ export const ArticleEditor: FC<Props> = ({ article }) => {
|
|||||||
>
|
>
|
||||||
Publish
|
Publish
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
<HistoryDrawer
|
||||||
|
drafts={drafts}
|
||||||
|
setCurrentDraft={(draft) => getDraft(draft.id)}
|
||||||
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Form>
|
</Form>
|
||||||
@ -270,6 +243,78 @@ export const ArticleEditor: FC<Props> = ({ article }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
interface HistoryDrawerProps {
|
||||||
|
drafts?: DraftArticle[];
|
||||||
|
setCurrentDraft: (draft: DraftArticle) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const HistoryDrawer: FC<HistoryDrawerProps> = ({ drafts, setCurrentDraft }) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<IconButton aria-label="history" onClick={() => setOpen(!open)}>
|
||||||
|
<Badge color="secondary" badgeContent={drafts?.length}>
|
||||||
|
<HistoryEduOutlined />
|
||||||
|
</Badge>
|
||||||
|
</IconButton>
|
||||||
|
<SwipeableDrawer
|
||||||
|
container={window.document.body}
|
||||||
|
anchor="right"
|
||||||
|
open={open}
|
||||||
|
onClose={() => setOpen(false)}
|
||||||
|
onOpen={() => setOpen(true)}
|
||||||
|
swipeAreaWidth={50}
|
||||||
|
disableSwipeToOpen={false}
|
||||||
|
ModalProps={{
|
||||||
|
keepMounted: true,
|
||||||
|
BackdropProps: {
|
||||||
|
sx: {
|
||||||
|
backgroundColor: "transparent",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
borderTopLeftRadius: 8,
|
||||||
|
borderTopRightRadius: 8,
|
||||||
|
visibility: "visible",
|
||||||
|
height: "100%",
|
||||||
|
width: "200px",
|
||||||
|
overflowY: "auto",
|
||||||
|
overflowX: "hidden",
|
||||||
|
mt: theme.mixins.toolbar.minHeight,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Timeline>
|
||||||
|
{drafts?.map((draft, index) => (
|
||||||
|
<TimelineItem
|
||||||
|
sx={{
|
||||||
|
"&:before": {
|
||||||
|
content: "unset",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TimelineSeparator>
|
||||||
|
<TimelineDot />
|
||||||
|
<TimelineConnector />
|
||||||
|
</TimelineSeparator>
|
||||||
|
<TimelineContent>
|
||||||
|
<Typography>
|
||||||
|
{index + 1} - {format(draft.updatedAt, "yyyy-MM-dd")}
|
||||||
|
</Typography>
|
||||||
|
<Typography>{format(draft.updatedAt, "HH:mm:ss")}</Typography>
|
||||||
|
</TimelineContent>
|
||||||
|
</TimelineItem>
|
||||||
|
))}
|
||||||
|
</Timeline>
|
||||||
|
</Box>
|
||||||
|
</SwipeableDrawer>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
ArticleEditor.defaultProps = {
|
ArticleEditor.defaultProps = {
|
||||||
article: {
|
article: {
|
||||||
title: "",
|
title: "",
|
||||||
|
@ -8,14 +8,14 @@ import {
|
|||||||
TableContainer,
|
TableContainer,
|
||||||
TableHead,
|
TableHead,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "@material-ui/core";
|
} from "@mui/material";
|
||||||
import React, { FC } from "react";
|
import React, { FC } from "react";
|
||||||
import { Article } from "../generated/graphql";
|
import { Article } from "../generated/graphql";
|
||||||
import EditIcon from "@material-ui/icons/Edit";
|
import EditIcon from "@mui/icons-material/Edit";
|
||||||
import { useRouter } from "@curi/react-dom";
|
import { ARTICLES, REMOVE_ARTICLE } from "./articles.constants";
|
||||||
import { ARTICLES, REMOVE_ARTICLE } from './articles.constants';
|
import { Delete } from "@mui/icons-material";
|
||||||
import { Delete } from '@material-ui/icons';
|
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
export const ArticleIndex: FC = () => {
|
export const ArticleIndex: FC = () => {
|
||||||
const { data } = useQuery<{
|
const { data } = useQuery<{
|
||||||
@ -24,7 +24,7 @@ export const ArticleIndex: FC = () => {
|
|||||||
|
|
||||||
const [removeArticle] = useMutation<any, { id: string }>(REMOVE_ARTICLE);
|
const [removeArticle] = useMutation<any, { id: string }>(REMOVE_ARTICLE);
|
||||||
|
|
||||||
const router = useRouter();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section>
|
<section>
|
||||||
@ -57,14 +57,8 @@ export const ArticleIndex: FC = () => {
|
|||||||
<TableCell>
|
<TableCell>
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label="edit"
|
aria-label="edit"
|
||||||
onClick={() =>
|
onClick={() => navigate(`/articles/${article.id}`)}
|
||||||
router.navigate({
|
size="large"
|
||||||
url: router.url({
|
|
||||||
name: "modify-article",
|
|
||||||
params: article,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<EditIcon />
|
<EditIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
@ -75,6 +69,7 @@ export const ArticleIndex: FC = () => {
|
|||||||
variables: article,
|
variables: article,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
size="large"
|
||||||
>
|
>
|
||||||
<Delete />
|
<Delete />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
@ -7,6 +7,11 @@ export const ARTICLE = gql`
|
|||||||
title
|
title
|
||||||
content
|
content
|
||||||
publishedAt
|
publishedAt
|
||||||
|
histories {
|
||||||
|
updatedAt
|
||||||
|
payload
|
||||||
|
automatically
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
147
src/articles/hooks/useDrafts.tsx
Normal file
147
src/articles/hooks/useDrafts.tsx
Normal file
@ -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<DraftArticle>;
|
||||||
|
putDraft: (input: CreateDraftArticleInput) => Promise<DraftArticle>;
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
};
|
@ -1,4 +1,4 @@
|
|||||||
import { makeStyles } from "@material-ui/core";
|
import makeStyles from '@mui/styles/makeStyles';
|
||||||
import { FC, Fragment, useEffect, useRef } from "react";
|
import { FC, Fragment, useEffect, useRef } from "react";
|
||||||
import { useAuth } from "./auth.provider";
|
import { useAuth } from "./auth.provider";
|
||||||
const useStyles = makeStyles((theme) => ({
|
const useStyles = makeStyles((theme) => ({
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import { createStyles, FormControl, FormHelperText, FormLabel, makeStyles } from '@material-ui/core';
|
import { FormControl, FormHelperText, FormLabel } from '@mui/material';
|
||||||
import { Skeleton } from "@material-ui/lab";
|
import createStyles from '@mui/styles/createStyles';
|
||||||
|
import makeStyles from '@mui/styles/makeStyles';
|
||||||
|
import { Skeleton } from '@mui/material';
|
||||||
import { FieldHookConfig, useField } from "formik";
|
import { FieldHookConfig, useField } from "formik";
|
||||||
import React, { FC, useEffect, useMemo, useRef, useState } from "react";
|
import React, { FC, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import Vditor from "vditor";
|
import Vditor from "vditor";
|
||||||
@ -63,7 +65,7 @@ export const Editor: FC<Props> = ({ label, className, ...props }) => {
|
|||||||
{meta.error && <FormHelperText error={true}>{meta.error}</FormHelperText>}
|
{meta.error && <FormHelperText error={true}>{meta.error}</FormHelperText>}
|
||||||
{instance ? null : (
|
{instance ? null : (
|
||||||
<Skeleton
|
<Skeleton
|
||||||
variant="rect"
|
variant="rectangular"
|
||||||
animation="wave"
|
animation="wave"
|
||||||
height="100%"
|
height="100%"
|
||||||
width="100%"
|
width="100%"
|
||||||
|
@ -1,20 +0,0 @@
|
|||||||
import { ActiveHookProps, Link, LinkProps, useActive } from '@curi/react-dom';
|
|
||||||
import React, { FC, ReactNode } from 'react';
|
|
||||||
|
|
||||||
export type ActiveLinkProps = ActiveHookProps &
|
|
||||||
LinkProps & {
|
|
||||||
className?: string;
|
|
||||||
children: ReactNode;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ActiveLink:FC<ActiveLinkProps> = ({ name, params, partial, className = "", ...rest }) => {
|
|
||||||
const active = useActive({ name, params, partial });
|
|
||||||
return (
|
|
||||||
<Link
|
|
||||||
name={name}
|
|
||||||
params={params}
|
|
||||||
{...rest}
|
|
||||||
className={active ? `${className} active` : className}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,31 +0,0 @@
|
|||||||
import { useApolloClient } from "@apollo/client";
|
|
||||||
import { createRouterComponent } from "@curi/react-dom";
|
|
||||||
import { createRouter, announce } from "@curi/router";
|
|
||||||
import { browser } from "@hickory/browser";
|
|
||||||
import { FC, useEffect, useState } from "react";
|
|
||||||
import routes from "../../routes";
|
|
||||||
import { LinearProgress } from "@material-ui/core";
|
|
||||||
|
|
||||||
const Component: FC = ({ children }) => {
|
|
||||||
const client = useApolloClient();
|
|
||||||
const [body, setBody] = useState<any>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const router = createRouter(browser, routes, {
|
|
||||||
sideEffects: [
|
|
||||||
announce(({ response }) => {
|
|
||||||
return `Navigated to ${response.location.pathname}`;
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
external: { client },
|
|
||||||
});
|
|
||||||
const Router = createRouterComponent(router);
|
|
||||||
router.once(() => {
|
|
||||||
setBody(<Router>{children}</Router>);
|
|
||||||
});
|
|
||||||
}, [setBody, client, children]);
|
|
||||||
|
|
||||||
return body ?? <LinearProgress />;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Component;
|
|
@ -97,18 +97,6 @@
|
|||||||
"isDeprecated": false,
|
"isDeprecated": false,
|
||||||
"deprecationReason": null
|
"deprecationReason": null
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "key",
|
|
||||||
"description": null,
|
|
||||||
"args": [],
|
|
||||||
"type": {
|
|
||||||
"kind": "SCALAR",
|
|
||||||
"name": "String",
|
|
||||||
"ofType": null
|
|
||||||
},
|
|
||||||
"isDeprecated": false,
|
|
||||||
"deprecationReason": null
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "html",
|
"name": "html",
|
||||||
"description": null,
|
"description": null,
|
||||||
@ -136,6 +124,30 @@
|
|||||||
},
|
},
|
||||||
"isDeprecated": false,
|
"isDeprecated": false,
|
||||||
"deprecationReason": null
|
"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,
|
"inputFields": null,
|
||||||
@ -163,6 +175,91 @@
|
|||||||
"enumValues": null,
|
"enumValues": null,
|
||||||
"possibleTypes": 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",
|
"kind": "INPUT_OBJECT",
|
||||||
"name": "CreateArticleInput",
|
"name": "CreateArticleInput",
|
||||||
@ -242,71 +339,6 @@
|
|||||||
"enumValues": null,
|
"enumValues": null,
|
||||||
"possibleTypes": 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",
|
"kind": "INPUT_OBJECT",
|
||||||
"name": "CreateTagInput",
|
"name": "CreateTagInput",
|
||||||
@ -344,81 +376,6 @@
|
|||||||
"enumValues": null,
|
"enumValues": null,
|
||||||
"possibleTypes": 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",
|
"kind": "OBJECT",
|
||||||
"name": "Hello",
|
"name": "Hello",
|
||||||
@ -550,113 +507,6 @@
|
|||||||
"isDeprecated": false,
|
"isDeprecated": false,
|
||||||
"deprecationReason": null
|
"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",
|
"name": "createTag",
|
||||||
"description": null,
|
"description": null,
|
||||||
@ -775,7 +625,7 @@
|
|||||||
{
|
{
|
||||||
"kind": "SCALAR",
|
"kind": "SCALAR",
|
||||||
"name": "Object",
|
"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,
|
"fields": null,
|
||||||
"inputFields": null,
|
"inputFields": null,
|
||||||
"interfaces": null,
|
"interfaces": null,
|
||||||
|
@ -12,7 +12,7 @@ export type Scalars = {
|
|||||||
Float: number;
|
Float: number;
|
||||||
/** A date-time string at UTC, such as 2019-12-03T09:54:33Z, compliant with the date-time format. */
|
/** A date-time string at UTC, such as 2019-12-03T09:54:33Z, compliant with the date-time format. */
|
||||||
DateTime: any;
|
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;
|
Object: any;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -23,9 +23,17 @@ export type Article = {
|
|||||||
content: Scalars['String'];
|
content: Scalars['String'];
|
||||||
publishedAt?: Maybe<Scalars['DateTime']>;
|
publishedAt?: Maybe<Scalars['DateTime']>;
|
||||||
tags: Array<Scalars['String']>;
|
tags: Array<Scalars['String']>;
|
||||||
key?: Maybe<Scalars['String']>;
|
|
||||||
html: Scalars['String'];
|
html: Scalars['String'];
|
||||||
description?: Maybe<Scalars['String']>;
|
description?: Maybe<Scalars['String']>;
|
||||||
|
histories: Array<ArticleHistory>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ArticleHistory = {
|
||||||
|
__typename?: 'ArticleHistory';
|
||||||
|
payload: Scalars['Object'];
|
||||||
|
updatedAt: Scalars['DateTime'];
|
||||||
|
automatic: Scalars['Boolean'];
|
||||||
|
published: Scalars['Boolean'];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CreateArticleInput = {
|
export type CreateArticleInput = {
|
||||||
@ -35,25 +43,11 @@ export type CreateArticleInput = {
|
|||||||
tags: Array<Scalars['String']>;
|
tags: Array<Scalars['String']>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CreateDraftArticleInput = {
|
|
||||||
payload: Scalars['Object'];
|
|
||||||
key: Scalars['String'];
|
|
||||||
automatically?: Maybe<Scalars['Boolean']>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type CreateTagInput = {
|
export type CreateTagInput = {
|
||||||
name: Scalars['String'];
|
name: Scalars['String'];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export type DraftArticle = {
|
|
||||||
__typename?: 'DraftArticle';
|
|
||||||
id: Scalars['ID'];
|
|
||||||
payload: Scalars['Object'];
|
|
||||||
key: Scalars['String'];
|
|
||||||
automatically: Scalars['Boolean'];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Hello = {
|
export type Hello = {
|
||||||
__typename?: 'Hello';
|
__typename?: 'Hello';
|
||||||
message: Scalars['String'];
|
message: Scalars['String'];
|
||||||
@ -64,9 +58,6 @@ export type Mutation = {
|
|||||||
createArticle: Article;
|
createArticle: Article;
|
||||||
updateArticle: Article;
|
updateArticle: Article;
|
||||||
removeArticle: Scalars['Int'];
|
removeArticle: Scalars['Int'];
|
||||||
putDraftForArticle: DraftArticle;
|
|
||||||
listDraftForArticle: Array<DraftArticle>;
|
|
||||||
lastDraftForArticle: DraftArticle;
|
|
||||||
createTag: Tag;
|
createTag: Tag;
|
||||||
updateTag: Tag;
|
updateTag: Tag;
|
||||||
removeTag: 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 = {
|
export type MutationCreateTagArgs = {
|
||||||
createTagInput: CreateTagInput;
|
createTagInput: CreateTagInput;
|
||||||
};
|
};
|
||||||
|
@ -9,22 +9,36 @@ import { MuiPickersUtilsProvider } from "@material-ui/pickers";
|
|||||||
import DateFnsUtils from "@date-io/date-fns";
|
import DateFnsUtils from "@date-io/date-fns";
|
||||||
import zhLocale from "date-fns/locale/zh-CN";
|
import zhLocale from "date-fns/locale/zh-CN";
|
||||||
import { SnackbarProvider } from "notistack";
|
import { SnackbarProvider } from "notistack";
|
||||||
import Router from "./commons/route/router";
|
|
||||||
import { AuthProvider } from "./commons/auth/auth.provider";
|
import { AuthProvider } from "./commons/auth/auth.provider";
|
||||||
|
import {
|
||||||
|
ThemeProvider,
|
||||||
|
Theme,
|
||||||
|
StyledEngineProvider,
|
||||||
|
createTheme,
|
||||||
|
} from "@mui/material/styles";
|
||||||
|
|
||||||
|
declare module "@mui/styles/defaultTheme" {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||||
|
interface DefaultTheme extends Theme {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const theme = createTheme();
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<SnackbarProvider maxSnack={5}>
|
<StyledEngineProvider injectFirst>
|
||||||
<AuthProvider>
|
<ThemeProvider theme={theme}>
|
||||||
<AppApolloClientProvider>
|
<SnackbarProvider maxSnack={5}>
|
||||||
<MuiPickersUtilsProvider utils={DateFnsUtils} locale={zhLocale}>
|
<AuthProvider>
|
||||||
<Router>
|
<AppApolloClientProvider>
|
||||||
<App />
|
<MuiPickersUtilsProvider utils={DateFnsUtils} locale={zhLocale}>
|
||||||
</Router>
|
<App />
|
||||||
</MuiPickersUtilsProvider>
|
</MuiPickersUtilsProvider>
|
||||||
</AppApolloClientProvider>
|
</AppApolloClientProvider>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
</SnackbarProvider>
|
</SnackbarProvider>
|
||||||
|
</ThemeProvider>
|
||||||
|
</StyledEngineProvider>
|
||||||
</React.StrictMode>,
|
</React.StrictMode>,
|
||||||
document.getElementById("root")
|
document.getElementById("root")
|
||||||
);
|
);
|
||||||
|
@ -1,95 +1,96 @@
|
|||||||
import React, { FC } from "react";
|
import React, { FC, Suspense } from "react";
|
||||||
import clsx from "clsx";
|
import { useTheme, Theme, CSSObject, styled } from "@mui/material/styles";
|
||||||
import {
|
import Drawer from "@mui/material/Drawer";
|
||||||
createStyles,
|
import AppBar, { AppBarProps } from "@mui/material/AppBar";
|
||||||
makeStyles,
|
import Toolbar from "@mui/material/Toolbar";
|
||||||
useTheme,
|
import List from "@mui/material/List";
|
||||||
Theme,
|
import CssBaseline from "@mui/material/CssBaseline";
|
||||||
} from "@material-ui/core/styles";
|
import Typography from "@mui/material/Typography";
|
||||||
import Drawer from "@material-ui/core/Drawer";
|
import Divider from "@mui/material/Divider";
|
||||||
import AppBar from "@material-ui/core/AppBar";
|
import IconButton from "@mui/material/IconButton";
|
||||||
import Toolbar from "@material-ui/core/Toolbar";
|
import MenuIcon from "@mui/icons-material/Menu";
|
||||||
import List from "@material-ui/core/List";
|
import ChevronLeftIcon from "@mui/icons-material/ChevronLeft";
|
||||||
import CssBaseline from "@material-ui/core/CssBaseline";
|
import ChevronRightIcon from "@mui/icons-material/ChevronRight";
|
||||||
import Typography from "@material-ui/core/Typography";
|
import ListItem from "@mui/material/ListItem";
|
||||||
import Divider from "@material-ui/core/Divider";
|
import ListItemIcon from "@mui/material/ListItemIcon";
|
||||||
import IconButton from "@material-ui/core/IconButton";
|
import ListItemText from "@mui/material/ListItemText";
|
||||||
import MenuIcon from "@material-ui/icons/Menu";
|
import { AddCircle, Description, LocalOffer } from "@mui/icons-material";
|
||||||
import ChevronLeftIcon from "@material-ui/icons/ChevronLeft";
|
import { Box } from "@mui/system";
|
||||||
import ChevronRightIcon from "@material-ui/icons/ChevronRight";
|
import { Link } from "react-router-dom";
|
||||||
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';
|
|
||||||
const drawerWidth = 240;
|
const drawerWidth = 240;
|
||||||
|
|
||||||
const useStyles = makeStyles((theme: Theme) =>
|
const openedMixin = (theme: Theme): CSSObject => ({
|
||||||
createStyles({
|
width: drawerWidth,
|
||||||
root: {
|
transition: theme.transitions.create("width", {
|
||||||
display: "flex",
|
easing: theme.transitions.easing.sharp,
|
||||||
},
|
duration: theme.transitions.duration.enteringScreen,
|
||||||
appBar: {
|
}),
|
||||||
zIndex: theme.zIndex.drawer + 1,
|
overflowX: "hidden",
|
||||||
transition: theme.transitions.create(["width", "margin"], {
|
});
|
||||||
easing: theme.transitions.easing.sharp,
|
|
||||||
duration: theme.transitions.duration.leavingScreen,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
appBarShift: {
|
|
||||||
marginLeft: drawerWidth,
|
|
||||||
width: `calc(100% - ${drawerWidth}px)`,
|
|
||||||
transition: theme.transitions.create(["width", "margin"], {
|
|
||||||
easing: theme.transitions.easing.sharp,
|
|
||||||
duration: theme.transitions.duration.enteringScreen,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
menuButton: {
|
|
||||||
marginRight: 36,
|
|
||||||
},
|
|
||||||
hide: {
|
|
||||||
display: "none",
|
|
||||||
},
|
|
||||||
drawer: {
|
|
||||||
width: drawerWidth,
|
|
||||||
flexShrink: 0,
|
|
||||||
whiteSpace: "nowrap",
|
|
||||||
},
|
|
||||||
drawerOpen: {
|
|
||||||
width: drawerWidth,
|
|
||||||
transition: theme.transitions.create("width", {
|
|
||||||
easing: theme.transitions.easing.sharp,
|
|
||||||
duration: theme.transitions.duration.enteringScreen,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
drawerClose: {
|
|
||||||
transition: theme.transitions.create("width", {
|
|
||||||
easing: theme.transitions.easing.sharp,
|
|
||||||
duration: theme.transitions.duration.leavingScreen,
|
|
||||||
}),
|
|
||||||
overflowX: "hidden",
|
|
||||||
width: theme.spacing(7) + 1,
|
|
||||||
[theme.breakpoints.up("sm")]: {
|
|
||||||
width: theme.spacing(9) + 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
toolbar: {
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "flex-end",
|
|
||||||
padding: theme.spacing(0, 1),
|
|
||||||
// necessary for content to be below app bar
|
|
||||||
...theme.mixins.toolbar,
|
|
||||||
},
|
|
||||||
content: {
|
|
||||||
flexGrow: 1,
|
|
||||||
padding: theme.spacing(3),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
export const DefaultLayout: FC = ({children}) => {
|
const closedMixin = (theme: Theme): CSSObject => ({
|
||||||
const classes = useStyles();
|
transition: theme.transitions.create("width", {
|
||||||
|
easing: theme.transitions.easing.sharp,
|
||||||
|
duration: theme.transitions.duration.leavingScreen,
|
||||||
|
}),
|
||||||
|
overflowX: "hidden",
|
||||||
|
width: `calc(${theme.spacing(7)} + 1px)`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const DrawerHeader = styled("div")(({ theme }) => ({
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "flex-end",
|
||||||
|
padding: theme.spacing(0, 1),
|
||||||
|
// necessary for content to be below app bar
|
||||||
|
...theme.mixins.toolbar,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const Main = styled("main")(({ theme }) => ({
|
||||||
|
padding: theme.spacing(2, 2),
|
||||||
|
[theme.breakpoints.up("md")]: {
|
||||||
|
padding: theme.spacing(4, 3),
|
||||||
|
},
|
||||||
|
width: "100%",
|
||||||
|
marginTop: theme.mixins.toolbar.minHeight,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledAppBar = styled(AppBar, {
|
||||||
|
shouldForwardProp: (prop) => prop !== "open",
|
||||||
|
})<AppBarProps & { open?: boolean }>(({ theme, open }) => ({
|
||||||
|
zIndex: theme.zIndex.drawer + 1,
|
||||||
|
transition: theme.transitions.create(["width", "margin"], {
|
||||||
|
easing: theme.transitions.easing.sharp,
|
||||||
|
duration: theme.transitions.duration.leavingScreen,
|
||||||
|
}),
|
||||||
|
...(open && {
|
||||||
|
marginLeft: drawerWidth,
|
||||||
|
width: `calc(100% - ${drawerWidth}px)`,
|
||||||
|
transition: theme.transitions.create(["width", "margin"], {
|
||||||
|
easing: theme.transitions.easing.sharp,
|
||||||
|
duration: theme.transitions.duration.enteringScreen,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledDrawer = styled(Drawer, {
|
||||||
|
shouldForwardProp: (prop) => prop !== "open",
|
||||||
|
})(({ theme, open }) => ({
|
||||||
|
width: drawerWidth,
|
||||||
|
flexShrink: 0,
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
boxSizing: "border-box",
|
||||||
|
...(open && {
|
||||||
|
...openedMixin(theme),
|
||||||
|
"& .MuiDrawer-paper": openedMixin(theme),
|
||||||
|
}),
|
||||||
|
...(!open && {
|
||||||
|
...closedMixin(theme),
|
||||||
|
"& .MuiDrawer-paper": closedMixin(theme),
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const DefaultLayout: FC = ({ children }) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const [open, setOpen] = React.useState(false);
|
const [open, setOpen] = React.useState(false);
|
||||||
|
|
||||||
@ -102,86 +103,70 @@ export const DefaultLayout: FC = ({children}) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classes.root}>
|
<Box sx={{ display: "flex" }}>
|
||||||
<CssBaseline />
|
<CssBaseline />
|
||||||
<AppBar
|
<StyledAppBar position="fixed" open={open}>
|
||||||
position="fixed"
|
<Toolbar>
|
||||||
className={clsx(classes.appBar, {
|
<IconButton
|
||||||
[classes.appBarShift]: open,
|
color="inherit"
|
||||||
})}
|
aria-label="open drawer"
|
||||||
>
|
onClick={handleDrawerOpen}
|
||||||
<Toolbar>
|
edge="start"
|
||||||
<IconButton
|
sx={{
|
||||||
color="inherit"
|
marginRight: "36px",
|
||||||
aria-label="open drawer"
|
...(open && { display: "none" }),
|
||||||
onClick={handleDrawerOpen}
|
}}
|
||||||
edge="start"
|
size="large"
|
||||||
className={clsx(classes.menuButton, {
|
>
|
||||||
[classes.hide]: open,
|
<MenuIcon />
|
||||||
})}
|
</IconButton>
|
||||||
>
|
<Typography variant="h6" noWrap>
|
||||||
<MenuIcon />
|
Mini variant drawer
|
||||||
</IconButton>
|
</Typography>
|
||||||
<Typography variant="h6" noWrap>
|
</Toolbar>
|
||||||
Mini variant drawer
|
</StyledAppBar>
|
||||||
</Typography>
|
<StyledDrawer variant="permanent" open={open}>
|
||||||
</Toolbar>
|
<DrawerHeader>
|
||||||
</AppBar>
|
<IconButton onClick={handleDrawerClose} size="large">
|
||||||
<Drawer
|
{theme.direction === "rtl" ? (
|
||||||
variant="permanent"
|
<ChevronRightIcon />
|
||||||
className={clsx(classes.drawer, {
|
) : (
|
||||||
[classes.drawerOpen]: open,
|
<ChevronLeftIcon />
|
||||||
[classes.drawerClose]: !open,
|
)}
|
||||||
})}
|
</IconButton>
|
||||||
classes={{
|
</DrawerHeader>
|
||||||
paper: clsx({
|
<Divider />
|
||||||
[classes.drawerOpen]: open,
|
<List>
|
||||||
[classes.drawerClose]: !open,
|
<Link to="/articles/create">
|
||||||
}),
|
<ListItem button>
|
||||||
}}
|
<ListItemIcon>
|
||||||
>
|
<AddCircle />
|
||||||
<div className={classes.toolbar}>
|
</ListItemIcon>
|
||||||
<IconButton onClick={handleDrawerClose}>
|
<ListItemText primary="New Article" />
|
||||||
{theme.direction === "rtl" ? (
|
</ListItem>
|
||||||
<ChevronRightIcon />
|
</Link>
|
||||||
) : (
|
<Link to="/articles">
|
||||||
<ChevronLeftIcon />
|
<ListItem button>
|
||||||
)}
|
<ListItemIcon>
|
||||||
</IconButton>
|
<Description />
|
||||||
</div>
|
</ListItemIcon>
|
||||||
<Divider />
|
<ListItemText primary="Articles" />
|
||||||
<List>
|
</ListItem>
|
||||||
<Link name="create-article">
|
</Link>
|
||||||
<ListItem button>
|
<Link to="/tags">
|
||||||
<ListItemIcon>
|
<ListItem button>
|
||||||
<AddCircle />
|
<ListItemIcon>
|
||||||
</ListItemIcon>
|
<LocalOffer />
|
||||||
<ListItemText primary="New Article" />
|
</ListItemIcon>
|
||||||
</ListItem>
|
<ListItemText primary="Tags" />
|
||||||
</Link>
|
</ListItem>
|
||||||
<Link name="articles">
|
</Link>
|
||||||
<ListItem button>
|
</List>
|
||||||
<ListItemIcon>
|
<Divider />
|
||||||
<Description />
|
</StyledDrawer>
|
||||||
</ListItemIcon>
|
<Main>
|
||||||
<ListItemText primary="Articles" />
|
<Suspense fallback={"Loading"}>{children}</Suspense>
|
||||||
</ListItem>
|
</Main>
|
||||||
</Link>
|
</Box>
|
||||||
<Link name="tags">
|
|
||||||
<ListItem button>
|
|
||||||
<ListItemIcon>
|
|
||||||
<LocalOffer />
|
|
||||||
</ListItemIcon>
|
|
||||||
<ListItemText primary="Tags" />
|
|
||||||
</ListItem>
|
|
||||||
</Link>
|
|
||||||
</List>
|
|
||||||
<Divider />
|
|
||||||
</Drawer>
|
|
||||||
<main className={classes.content}>
|
|
||||||
<div className={classes.toolbar} />
|
|
||||||
{ children }
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
173
src/routes.tsx
173
src/routes.tsx
@ -1,74 +1,113 @@
|
|||||||
import { ApolloClient } from "@apollo/client";
|
import { useApolloClient } from "@apollo/client";
|
||||||
import { prepareRoutes } from "@curi/router";
|
import * as R from "ramda";
|
||||||
import { omit } from 'ramda';
|
import { FC, lazy, useMemo } from "react";
|
||||||
|
import {
|
||||||
|
Navigate,
|
||||||
|
useParams,
|
||||||
|
useRoutes,
|
||||||
|
useSearchParams,
|
||||||
|
} from "react-router-dom";
|
||||||
import { ARTICLE } from "./articles";
|
import { ARTICLE } from "./articles";
|
||||||
import { Article } from './generated/graphql';
|
import { Article } from "./generated/graphql";
|
||||||
|
import { DefaultLayout } from "./layouts";
|
||||||
|
|
||||||
export default prepareRoutes([
|
const CreateArticle = () => {
|
||||||
{
|
const ArticleEditor = lazy(() =>
|
||||||
name: "dashboard",
|
import(
|
||||||
path: "",
|
/* webpackChunkName: "article-editor" */ "./articles/article-editor"
|
||||||
respond() {
|
).then((m) => ({
|
||||||
return { body: () => <div>DashBoard</div> };
|
default: m.ArticleEditor,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
const [search] = useSearchParams();
|
||||||
|
if (!search.has("key")) {
|
||||||
|
search.set("key", new Date().toISOString());
|
||||||
|
return <Navigate to={`/articles/create?${search}`} replace />;
|
||||||
|
}
|
||||||
|
return <ArticleEditor draftKey={search.get("key")!} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ModifyArticle: FC = () => {
|
||||||
|
const { id } = useParams();
|
||||||
|
const client = useApolloClient();
|
||||||
|
const Lazy = useMemo(
|
||||||
|
() =>
|
||||||
|
lazy(async () => {
|
||||||
|
const [{ ArticleEditor }, article] = await Promise.all([
|
||||||
|
import(
|
||||||
|
/* webpackChunkName: "article-editor" */ "./articles/article-editor"
|
||||||
|
),
|
||||||
|
client
|
||||||
|
.query<{ article: Article }, { id: string }>({
|
||||||
|
query: ARTICLE,
|
||||||
|
variables: { id: id! },
|
||||||
|
})
|
||||||
|
.then(({ data }) => {
|
||||||
|
return R.omit(["__typename"], data.article);
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
return {
|
||||||
|
default: () => (
|
||||||
|
<ArticleEditor article={article} draftKey={article.key!} />
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
[id, client]
|
||||||
|
);
|
||||||
|
return <Lazy />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ArticleIndex = lazy(() =>
|
||||||
|
import(/* webpackChunkName: "articles" */ "./articles").then((m) => ({
|
||||||
|
default: m.ArticleIndex,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
const TagIndex = lazy(() =>
|
||||||
|
import(/* webpackChunkName: "tags" */ "./tags").then((m) => ({
|
||||||
|
default: m.TagsIndex,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
export const AppRoutes: FC = () => {
|
||||||
|
const element = useRoutes([
|
||||||
|
{
|
||||||
|
index: true,
|
||||||
|
element: <div>DashBoard</div>,
|
||||||
},
|
},
|
||||||
},
|
{
|
||||||
{
|
path: "/articles/create",
|
||||||
name: "create-article",
|
element: (
|
||||||
path: "articles/create",
|
<DefaultLayout>
|
||||||
resolve() {
|
<CreateArticle />
|
||||||
const body = import(
|
</DefaultLayout>
|
||||||
/* webpackChunkName: "article-editor" */ "./articles"
|
),
|
||||||
).then((m) => m.ArticleEditor);
|
|
||||||
return body;
|
|
||||||
},
|
},
|
||||||
respond({ resolved }) {
|
{
|
||||||
return { body: resolved };
|
path: "/articles/:id",
|
||||||
|
element: (
|
||||||
|
<DefaultLayout>
|
||||||
|
<ModifyArticle />
|
||||||
|
</DefaultLayout>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
},
|
{
|
||||||
{
|
path: "articles",
|
||||||
name: "modify-article",
|
element: (
|
||||||
path: "articles/:id",
|
<DefaultLayout>
|
||||||
async resolve(matched, { client }: { client: ApolloClient<any> }) {
|
<ArticleIndex />
|
||||||
const [ArticleEditor, result] = await Promise.all([
|
</DefaultLayout>
|
||||||
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 };
|
path: "tags",
|
||||||
|
element: (
|
||||||
|
<DefaultLayout>
|
||||||
|
<TagIndex />
|
||||||
|
</DefaultLayout>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
},
|
]);
|
||||||
{
|
|
||||||
name: "articles",
|
return element;
|
||||||
path: "articles",
|
};
|
||||||
resolve() {
|
|
||||||
return import(/* webpackChunkName: "articles" */ "./articles").then(
|
|
||||||
(m) => m.ArticleIndex
|
|
||||||
);
|
|
||||||
},
|
|
||||||
respond({ resolved }) {
|
|
||||||
return { body: resolved };
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "tags",
|
|
||||||
path: "tags",
|
|
||||||
resolve() {
|
|
||||||
return import(/* webpackChunkName: "tags" */ "./tags").then(
|
|
||||||
(m) => m.TagsIndex
|
|
||||||
);
|
|
||||||
},
|
|
||||||
respond({ resolved }) {
|
|
||||||
return { body: resolved };
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
@ -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,3 +1,2 @@
|
|||||||
export * from "./article-editor";
|
|
||||||
export * from "./tags-index";
|
export * from "./tags-index";
|
||||||
export * from "./tags.constants";
|
export * from "./tags.constants";
|
||||||
|
@ -8,13 +8,13 @@ import {
|
|||||||
TableContainer,
|
TableContainer,
|
||||||
TableHead,
|
TableHead,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "@material-ui/core";
|
} from "@mui/material";
|
||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
import { Tag } from "../generated/graphql";
|
import { Tag } from "../generated/graphql";
|
||||||
import EditIcon from "@material-ui/icons/Edit";
|
import EditIcon from "@mui/icons-material/Edit";
|
||||||
import { useRouter } from "@curi/react-dom";
|
import { Delete } from "@mui/icons-material";
|
||||||
import { Delete } from "@material-ui/icons";
|
|
||||||
import { REMOVE_TAG, TAGS } from "./tags.constants";
|
import { REMOVE_TAG, TAGS } from "./tags.constants";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
export const TagsIndex: FC = () => {
|
export const TagsIndex: FC = () => {
|
||||||
const { data } = useQuery<{
|
const { data } = useQuery<{
|
||||||
@ -23,7 +23,7 @@ export const TagsIndex: FC = () => {
|
|||||||
|
|
||||||
const [removeArticle] = useMutation<any, { id: string }>(REMOVE_TAG);
|
const [removeArticle] = useMutation<any, { id: string }>(REMOVE_TAG);
|
||||||
|
|
||||||
const router = useRouter();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section>
|
<section>
|
||||||
@ -48,14 +48,8 @@ export const TagsIndex: FC = () => {
|
|||||||
<TableCell>
|
<TableCell>
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label="edit"
|
aria-label="edit"
|
||||||
onClick={() =>
|
onClick={() => navigate(`/tags/${tag.id}/modify`)}
|
||||||
router.navigate({
|
size="large"
|
||||||
url: router.url({
|
|
||||||
name: "modify-tag",
|
|
||||||
params: tag,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<EditIcon />
|
<EditIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
@ -66,6 +60,7 @@ export const TagsIndex: FC = () => {
|
|||||||
variables: tag,
|
variables: tag,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
size="large"
|
||||||
>
|
>
|
||||||
<Delete />
|
<Delete />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
Reference in New Issue
Block a user