From 328d07b0455410c6b7052c12bc2f67b5b04f8129 Mon Sep 17 00:00:00 2001 From: "cinwell.li" Date: Sun, 9 May 2021 09:41:33 +0800 Subject: [PATCH] feat: import and export, close #5 (#43) * feat: import and export, close #5 * fix: export untitled --- components/button-progress.tsx | 29 +++++++ components/editor/note-edit.tsx | 3 +- ...{daily-notes-field.tsx => daily-notes.tsx} | 4 +- components/settings/export-button.tsx | 16 ++++ components/settings/import-button.tsx | 86 +++++++++++++++++++ components/settings/import-or-export.tsx | 51 +++++++++++ .../{language-field.tsx => language.tsx} | 4 +- components/settings/setting-footer.tsx | 2 +- components/settings/settings-container.tsx | 28 ++++++ components/settings/settings-form.tsx | 26 ------ .../settings/{theme-field.tsx => theme.tsx} | 4 +- components/settings/type.ts | 3 + libs/server/file.ts | 15 ++++ libs/server/meta.ts | 4 +- libs/server/middlewares/error.ts | 12 +-- libs/server/note.ts | 30 +++++++ libs/server/tree.ts | 13 ++- libs/shared/const.ts | 2 + libs/shared/markdown.ts | 14 +++ libs/shared/meta.ts | 2 +- libs/shared/settings.ts | 3 +- libs/shared/tree.ts | 10 ++- libs/web/api/fetcher.ts | 6 ++ libs/web/hooks/use-toast.ts | 26 ++++++ libs/web/state/trash.ts | 3 +- libs/web/state/tree.ts | 3 +- locales/de-DE.json | 10 ++- locales/en.json | 9 +- locales/ru-RU.json | 9 +- locales/zh-CN.json | 9 +- package.json | 5 +- pages/_app.tsx | 5 +- pages/api/export.ts | 44 ++++++++++ pages/api/import.ts | 76 ++++++++++++++++ pages/api/notes/[id]/index.ts | 3 +- pages/api/notes/index.ts | 30 +------ pages/api/trash.ts | 3 +- pages/api/upload.ts | 15 +--- pages/login.tsx | 16 ++-- pages/settings.tsx | 4 +- yarn.lock | 26 +++++- 41 files changed, 554 insertions(+), 109 deletions(-) create mode 100644 components/button-progress.tsx rename components/settings/{daily-notes-field.tsx => daily-notes.tsx} (93%) create mode 100644 components/settings/export-button.tsx create mode 100644 components/settings/import-button.tsx create mode 100644 components/settings/import-or-export.tsx rename components/settings/{language-field.tsx => language.tsx} (90%) create mode 100644 components/settings/settings-container.tsx delete mode 100644 components/settings/settings-form.tsx rename components/settings/{theme-field.tsx => theme.tsx} (90%) create mode 100644 components/settings/type.ts create mode 100644 libs/server/file.ts create mode 100644 libs/server/note.ts create mode 100644 libs/shared/markdown.ts create mode 100644 libs/web/hooks/use-toast.ts create mode 100644 pages/api/export.ts create mode 100644 pages/api/import.ts diff --git a/components/button-progress.tsx b/components/button-progress.tsx new file mode 100644 index 0000000..d605d4b --- /dev/null +++ b/components/button-progress.tsx @@ -0,0 +1,29 @@ +import { Button, ButtonProps, CircularProgress } from '@material-ui/core' +import classNames from 'classnames' +import { FC } from 'react' + +export const ButtonProgress: FC< + ButtonProps & { + loading?: boolean + progress?: number + } +> = ({ children, loading, progress, ...props }) => { + return ( + + ) +} diff --git a/components/editor/note-edit.tsx b/components/editor/note-edit.tsx index 71d425b..2c98e70 100644 --- a/components/editor/note-edit.tsx +++ b/components/editor/note-edit.tsx @@ -7,6 +7,7 @@ import { useDebouncedCallback } from 'use-debounce' import EditTitle from './edit-title' import Editor from './editor' import { NoteModel } from 'libs/shared/note' +import { ROOT_ID } from 'libs/shared/tree' const NoteEdit = () => { const { updateNote, createNote, note } = NoteState.useContainer() @@ -17,7 +18,7 @@ const NoteEdit = () => { const isNew = has(router.query, 'new') if (isNew) { - data.pid = (router.query.pid as string) || 'root' + data.pid = (router.query.pid as string) || ROOT_ID const item = await createNote({ ...note, ...data }) const noteUrl = `/${item?.id}` diff --git a/components/settings/daily-notes-field.tsx b/components/settings/daily-notes.tsx similarity index 93% rename from components/settings/daily-notes-field.tsx rename to components/settings/daily-notes.tsx index e50e4de..c487acf 100644 --- a/components/settings/daily-notes-field.tsx +++ b/components/settings/daily-notes.tsx @@ -5,10 +5,10 @@ import NoteTreeState from 'libs/web/state/tree' import { filter } from 'lodash' import UIState from 'libs/web/state/ui' import { TreeItemModel } from 'libs/shared/tree' -import { defaultFieldConfig } from './settings-form' +import { defaultFieldConfig } from './settings-container' import useI18n from 'libs/web/hooks/use-i18n' -export const DailyNotesField: FC = () => { +export const DailyNotes: FC = () => { const { t } = useI18n() const { tree } = NoteTreeState.useContainer() const { diff --git a/components/settings/export-button.tsx b/components/settings/export-button.tsx new file mode 100644 index 0000000..78403d8 --- /dev/null +++ b/components/settings/export-button.tsx @@ -0,0 +1,16 @@ +import { FC } from 'react' +import useI18n from 'libs/web/hooks/use-i18n' +import { ButtonProps } from './type' +import { ROOT_ID } from 'libs/shared/tree' +import Link from 'next/link' +import { ButtonProgress } from 'components/button-progress' + +export const ExportButton: FC = ({ parentId = ROOT_ID }) => { + const { t } = useI18n() + + return ( + + {t('Export')} + + ) +} diff --git a/components/settings/import-button.tsx b/components/settings/import-button.tsx new file mode 100644 index 0000000..50e7ee5 --- /dev/null +++ b/components/settings/import-button.tsx @@ -0,0 +1,86 @@ +import { ChangeEvent, FC, useCallback, useEffect } from 'react' +import useI18n from 'libs/web/hooks/use-i18n' +import { ButtonProps } from './type' +import useFetcher from 'libs/web/api/fetcher' +import { useToast } from 'libs/web/hooks/use-toast' +import { ButtonProgress } from 'components/button-progress' +import { IMPORT_FILE_LIMIT_SIZE } from 'libs/shared/const' +import { useRouter } from 'next/router' +import { ROOT_ID } from 'libs/shared/tree' + +export const ImportButton: FC = ({ parentId = ROOT_ID }) => { + const { t } = useI18n() + const { request, loading, error } = useFetcher() + const toast = useToast() + const router = useRouter() + + const onSelectFile = useCallback( + async (event: ChangeEvent) => { + const data = new FormData() + + if (!event.target.files?.length) { + return toast(t('Please select zip file'), 'error') + } + + const file = event.target.files[0] + + if (file.size > IMPORT_FILE_LIMIT_SIZE) { + return toast( + t('File size must be less than {{n}}mb', { + n: IMPORT_FILE_LIMIT_SIZE / 1024 / 1024, + }), + 'error' + ) + } + + data.append('file', file) + + const result = await request< + FormData, + { total: number; imported: number } + >( + { + method: 'POST', + url: '/api/import?pid=' + parentId, + }, + data + ) + + event.target.value = '' + if (!result) return + if (!result?.imported) { + return toast(t('Not found markdown file'), 'warning') + } + toast( + t('Successfully imported {{n}} markdown files', { + n: result?.imported, + }), + 'success' + ) + /** + * @todo fetch tree without reload page + */ + router.reload() + }, + [parentId, request, router, t, toast] + ) + + useEffect(() => { + if (error) { + toast(error, 'error') + } + }, [error, toast]) + + return ( + + ) +} diff --git a/components/settings/import-or-export.tsx b/components/settings/import-or-export.tsx new file mode 100644 index 0000000..de8949a --- /dev/null +++ b/components/settings/import-or-export.tsx @@ -0,0 +1,51 @@ +import { FC, useCallback, useMemo, useState } from 'react' +import { TextField } from '@material-ui/core' +import { defaultFieldConfig } from './settings-container' +import useI18n from 'libs/web/hooks/use-i18n' +import { ROOT_ID, TreeItemModel } from 'libs/shared/tree' +import NoteTreeState from 'libs/web/state/tree' +import { filter } from 'lodash' +import { Autocomplete } from '@material-ui/lab' +import { ExportButton } from './export-button' +import { ImportButton } from './import-button' + +export const ImportOrExport: FC = () => { + const { t } = useI18n() + const { tree } = NoteTreeState.useContainer() + const [selected, setSelected] = useState(tree.items[ROOT_ID]) + const items = useMemo( + () => filter(tree.items, (item) => !item.data?.deleted), + [tree] + ) + + const handleChange = useCallback((_event, item: TreeItemModel | null) => { + if (item) { + setSelected(item) + } + }, []) + + return ( +
+ option.data?.title || t('Root Page')} + value={selected} + onChange={handleChange} + renderInput={(params) => ( + + )} + > +
+ + +
+
+ ) +} diff --git a/components/settings/language-field.tsx b/components/settings/language.tsx similarity index 90% rename from components/settings/language-field.tsx rename to components/settings/language.tsx index 4e1e193..47e6a7e 100644 --- a/components/settings/language-field.tsx +++ b/components/settings/language.tsx @@ -1,13 +1,13 @@ import { FC, useCallback, ChangeEvent } from 'react' import { MenuItem, TextField } from '@material-ui/core' import UIState from 'libs/web/state/ui' -import { defaultFieldConfig } from './settings-form' +import { defaultFieldConfig } from './settings-container' import router from 'next/router' import useI18n from 'libs/web/hooks/use-i18n' import { configLocale, Locale } from 'locales' import { map } from 'lodash' -export const LanguageField: FC = () => { +export const Language: FC = () => { const { t } = useI18n() const { settings: { settings, updateSettings }, diff --git a/components/settings/setting-footer.tsx b/components/settings/setting-footer.tsx index b400b9c..c48f25f 100644 --- a/components/settings/setting-footer.tsx +++ b/components/settings/setting-footer.tsx @@ -2,7 +2,7 @@ import pkg from 'package.json' export const SettingFooter = () => { return ( -