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