mirror of https://github.com/fantasticit/think.git
client: update document import
This commit is contained in:
parent
ccc8adfe6b
commit
fa91f87675
|
@ -1,11 +1,10 @@
|
||||||
import { IconUpload } from '@douyinfe/semi-icons';
|
import { Button, Toast, Typography, Upload } from '@douyinfe/semi-ui';
|
||||||
import { Button, List, Toast, Typography } from '@douyinfe/semi-ui';
|
|
||||||
import type { IWiki } from '@think/domains';
|
import type { IWiki } from '@think/domains';
|
||||||
import { useCreateDocument } from 'data/document';
|
import { useCreateDocument } from 'data/document';
|
||||||
import { useToggle } from 'hooks/use-toggle';
|
import { useToggle } from 'hooks/use-toggle';
|
||||||
import { useCallback, useRef, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
import { ImportEditor } from './editor';
|
import { createMarkdownParser, MarkdownParse } from './parser';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
wikiId: IWiki['id'];
|
wikiId: IWiki['id'];
|
||||||
|
@ -15,137 +14,97 @@ const { Text } = Typography;
|
||||||
|
|
||||||
export const Import: React.FC<IProps> = ({ wikiId }) => {
|
export const Import: React.FC<IProps> = ({ wikiId }) => {
|
||||||
const { create } = useCreateDocument();
|
const { create } = useCreateDocument();
|
||||||
const $upload = useRef<HTMLInputElement>();
|
const $upload = useRef<Upload>();
|
||||||
const [uploadFiles, setUploadFiles] = useState([]);
|
|
||||||
const [texts, setTexts] = useState<Record<string, string | ArrayBuffer>>({});
|
|
||||||
const [payloads, setPayloads] = useState<
|
|
||||||
Record<
|
|
||||||
string,
|
|
||||||
{
|
|
||||||
title: string;
|
|
||||||
content: string;
|
|
||||||
state: Uint8Array;
|
|
||||||
}
|
|
||||||
>
|
|
||||||
>({});
|
|
||||||
const [parsedFiles, setParsedFiles] = useState([]);
|
|
||||||
const [loading, toggleLoading] = useToggle(false);
|
const [loading, toggleLoading] = useToggle(false);
|
||||||
|
const [markdownParser, setMarkdownParser] = useState<MarkdownParse>();
|
||||||
|
const [fileList, setFileList] = useState([]);
|
||||||
|
|
||||||
const selectFile = useCallback(() => {
|
const handleFile = useCallback(({ fileList: files }) => {
|
||||||
$upload.current.click();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleFile = useCallback((e) => {
|
|
||||||
const files = Array.from(e.target.files) as Array<File>;
|
|
||||||
|
|
||||||
if (!files.length) return;
|
if (!files.length) return;
|
||||||
|
|
||||||
files.forEach((file) => {
|
files.forEach((file) => {
|
||||||
|
const fileName = file.fileInstance.name;
|
||||||
|
|
||||||
const fileReader = new FileReader();
|
const fileReader = new FileReader();
|
||||||
fileReader.onload = function () {
|
fileReader.onload = function () {
|
||||||
setTexts((texts) => {
|
setFileList((fileList) => {
|
||||||
texts[file.name] = fileReader.result;
|
if (fileList.find((file) => file.name === fileName)) return fileList;
|
||||||
return texts;
|
return fileList.concat({ ...file, text: fileReader.result });
|
||||||
});
|
|
||||||
|
|
||||||
setUploadFiles((files) => {
|
|
||||||
return files.concat(file.name);
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
fileReader.readAsText(file);
|
fileReader.readAsText(file.fileInstance);
|
||||||
|
});
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const removeFile = useCallback((currentFile) => {
|
||||||
|
setFileList((fileList) => {
|
||||||
|
return fileList.filter((file) => file.name !== currentFile.name);
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const onParsedFile = useCallback((filename) => {
|
|
||||||
return (payload) => {
|
|
||||||
setPayloads((payloads) => {
|
|
||||||
payloads[filename] = payload;
|
|
||||||
setParsedFiles((files) => files.concat(filename));
|
|
||||||
return payloads;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const onParsedFileError = useCallback((filename) => {
|
|
||||||
return () => {
|
|
||||||
setUploadFiles((files) => {
|
|
||||||
return files.filter((name) => name !== filename);
|
|
||||||
});
|
|
||||||
setTexts((texts) => {
|
|
||||||
delete texts[filename];
|
|
||||||
return texts;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const onDeleteFile = useCallback((toDeleteFilename) => {
|
|
||||||
return () => {
|
|
||||||
setPayloads((payloads) => {
|
|
||||||
const newPayloads = Object.keys(payloads).reduce((accu, filename) => {
|
|
||||||
if (filename !== toDeleteFilename) {
|
|
||||||
accu[filename] = payloads[filename];
|
|
||||||
}
|
|
||||||
return accu;
|
|
||||||
}, {});
|
|
||||||
setParsedFiles(Object.keys(newPayloads));
|
|
||||||
return newPayloads;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const importFile = useCallback(() => {
|
const importFile = useCallback(() => {
|
||||||
|
if (!markdownParser) return;
|
||||||
|
|
||||||
|
const total = fileList.length;
|
||||||
|
let success = 0;
|
||||||
|
let failed = 0;
|
||||||
|
|
||||||
toggleLoading(true);
|
toggleLoading(true);
|
||||||
|
|
||||||
Promise.all(
|
for (const file of fileList) {
|
||||||
Object.keys(payloads).map((filename) => {
|
const payload = markdownParser.parse(file.name, file.text);
|
||||||
return create({ ...payloads[filename], wikiId });
|
create({ ...payload, wikiId })
|
||||||
})
|
.then(() => {
|
||||||
)
|
success += 1;
|
||||||
.then(() => {
|
})
|
||||||
Toast.success('文档已导入');
|
.catch(() => {
|
||||||
})
|
failed += 1;
|
||||||
.finally(() => {
|
})
|
||||||
toggleLoading(false);
|
.finally(() => {
|
||||||
setTexts({});
|
if (success + failed === total) {
|
||||||
setUploadFiles([]);
|
$upload.current.clear();
|
||||||
setPayloads({});
|
toggleLoading(false);
|
||||||
setParsedFiles([]);
|
setFileList([]);
|
||||||
$upload.current.value = '';
|
|
||||||
});
|
if (failed > 0) {
|
||||||
}, [payloads, toggleLoading, create, wikiId]);
|
Toast.error('部分文件导入失败,请重新尝试导入!');
|
||||||
|
} else {
|
||||||
|
Toast.success('导入成功');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [markdownParser, fileList, toggleLoading, create, wikiId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const markdownParser = createMarkdownParser();
|
||||||
|
setMarkdownParser(markdownParser);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
markdownParser.destroy();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ marginTop: 16 }}>
|
<div style={{ marginTop: 16 }}>
|
||||||
<Button icon={<IconUpload />} theme="light" onClick={selectFile}>
|
<Upload
|
||||||
点击上传
|
action=""
|
||||||
</Button>
|
accept="text/markdown"
|
||||||
|
draggable
|
||||||
<input ref={$upload} type="file" hidden multiple accept="text/markdown" onChange={handleFile} />
|
multiple
|
||||||
|
ref={$upload}
|
||||||
{uploadFiles.map((filename) => {
|
beforeUpload={handleFile}
|
||||||
return (
|
dragMainText={<Text>点击上传文件或拖拽文件到这里</Text>}
|
||||||
<ImportEditor
|
dragSubText={<Text type="tertiary">仅支持 Markdown 文件导入</Text>}
|
||||||
key={filename}
|
onRemove={removeFile}
|
||||||
filename={filename}
|
|
||||||
content={texts[filename]}
|
|
||||||
onChange={onParsedFile(filename)}
|
|
||||||
onError={onParsedFileError(filename)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
|
|
||||||
<List
|
|
||||||
dataSource={parsedFiles}
|
|
||||||
renderItem={(filename) => (
|
|
||||||
<List.Item main={<div>{filename}</div>} extra={<Button onClick={onDeleteFile(filename)}>删除</Button>} />
|
|
||||||
)}
|
|
||||||
emptyContent={<Text type="tertiary">仅支持 Markdown 文件导入</Text>}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
onClick={importFile}
|
onClick={importFile}
|
||||||
disabled={!parsedFiles.length}
|
disabled={!fileList.length}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
theme="solid"
|
theme="solid"
|
||||||
style={{ marginTop: 16 }}
|
style={{ marginTop: 16 }}
|
||||||
|
|
|
@ -1,32 +1,30 @@
|
||||||
import { Toast } from '@douyinfe/semi-ui';
|
import { Toast } from '@douyinfe/semi-ui';
|
||||||
import { safeJSONStringify } from 'helpers/json';
|
import { safeJSONStringify } from 'helpers/json';
|
||||||
import { useEffect, useMemo, useRef } from 'react';
|
import { createEditor } from 'tiptap/core';
|
||||||
import { useEditor } from 'tiptap/core';
|
|
||||||
import { AllExtensions } from 'tiptap/core/all-kit';
|
import { AllExtensions } from 'tiptap/core/all-kit';
|
||||||
import { Collaboration } from 'tiptap/core/extensions/collaboration';
|
import { Collaboration } from 'tiptap/core/extensions/collaboration';
|
||||||
import { prosemirrorJSONToYDoc } from 'tiptap/core/thritypart/y-prosemirror/y-prosemirror';
|
import { prosemirrorJSONToYDoc } from 'tiptap/core/thritypart/y-prosemirror/y-prosemirror';
|
||||||
import { markdownToProsemirror } from 'tiptap/markdown/markdown-to-prosemirror';
|
import { markdownToProsemirror } from 'tiptap/markdown/markdown-to-prosemirror';
|
||||||
import * as Y from 'yjs';
|
import * as Y from 'yjs';
|
||||||
|
|
||||||
export const ImportEditor = ({ filename, content, onChange, onError }) => {
|
export interface MarkdownParse {
|
||||||
const parsed = useRef(false);
|
parse: (filename: string, markdown: string) => { title: string; content: string; state: Buffer };
|
||||||
const ydoc = useMemo(() => new Y.Doc(), []);
|
destroy: () => void;
|
||||||
const editor = useEditor(
|
}
|
||||||
{
|
|
||||||
editable: false,
|
|
||||||
extensions: AllExtensions.concat(Collaboration.configure({ document: ydoc })),
|
|
||||||
content: '',
|
|
||||||
},
|
|
||||||
[ydoc]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
export const createMarkdownParser = () => {
|
||||||
if (!content || !editor || !ydoc || parsed.current) return;
|
const ydoc = new Y.Doc();
|
||||||
|
const editor = createEditor({
|
||||||
|
editable: false,
|
||||||
|
extensions: AllExtensions.concat(Collaboration.configure({ document: ydoc })),
|
||||||
|
content: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const parse = (filename: string, markdown: string) => {
|
||||||
try {
|
try {
|
||||||
const prosemirrorNode = markdownToProsemirror({
|
const prosemirrorNode = markdownToProsemirror({
|
||||||
schema: editor.schema,
|
schema: editor.schema,
|
||||||
content,
|
content: markdown,
|
||||||
needTitle: true,
|
needTitle: true,
|
||||||
defaultTitle: filename.replace(/\.md$/gi, ''),
|
defaultTitle: filename.replace(/\.md$/gi, ''),
|
||||||
});
|
});
|
||||||
|
@ -36,24 +34,22 @@ export const ImportEditor = ({ filename, content, onChange, onError }) => {
|
||||||
Y.applyUpdate(ydoc, Y.encodeStateAsUpdate(prosemirrorJSONToYDoc(editor.schema, prosemirrorNode)));
|
Y.applyUpdate(ydoc, Y.encodeStateAsUpdate(prosemirrorJSONToYDoc(editor.schema, prosemirrorNode)));
|
||||||
const state = Y.encodeStateAsUpdate(ydoc);
|
const state = Y.encodeStateAsUpdate(ydoc);
|
||||||
|
|
||||||
onChange({
|
return {
|
||||||
title,
|
title,
|
||||||
content: safeJSONStringify({ default: prosemirrorNode }),
|
content: safeJSONStringify({ default: prosemirrorNode }),
|
||||||
state: Buffer.from(state),
|
state: Buffer.from(state),
|
||||||
});
|
};
|
||||||
|
|
||||||
parsed.current = true;
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
onError();
|
|
||||||
console.error(e.message, e.stack);
|
console.error(e.message, e.stack);
|
||||||
Toast.error('文件内容解析失败,请打开控制台,截图错误信息,请到 Github 提 issue 寻求解决!');
|
Toast.error('文件内容解析失败,请打开控制台,截图错误信息,请到 Github 提 issue 寻求解决!');
|
||||||
|
throw e;
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return () => {
|
const destroy = () => {
|
||||||
ydoc.destroy();
|
ydoc.destroy();
|
||||||
editor.destroy();
|
editor.destroy();
|
||||||
};
|
};
|
||||||
}, [editor, ydoc, filename, content, onChange, onError]);
|
|
||||||
|
|
||||||
return null;
|
return { parse, destroy } as MarkdownParse;
|
||||||
};
|
};
|
|
@ -13,6 +13,10 @@ export class Editor extends BuiltInEditor {
|
||||||
public eventEmitter: EventEmitter = new EventEmitter();
|
public eventEmitter: EventEmitter = new EventEmitter();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const createEditor = (options: Partial<EditorOptions> = {}) => {
|
||||||
|
return new Editor(options);
|
||||||
|
};
|
||||||
|
|
||||||
export const useEditor = (options: Partial<EditorOptions> = {}, deps: DependencyList = []) => {
|
export const useEditor = (options: Partial<EditorOptions> = {}, deps: DependencyList = []) => {
|
||||||
const [editor, setEditor] = useState<Editor | null>(null);
|
const [editor, setEditor] = useState<Editor | null>(null);
|
||||||
const forceUpdate = useForceUpdate();
|
const forceUpdate = useForceUpdate();
|
||||||
|
|
Loading…
Reference in New Issue