refactor: improve performence

This commit is contained in:
fantasticit 2022-05-02 15:30:22 +08:00
parent 5c0d9f54e4
commit d2bec0f448
24 changed files with 676 additions and 327 deletions

View File

@ -1,13 +1,15 @@
import React, { useEffect, useRef } from 'react';
import { Banner as SemiBanner } from '@douyinfe/semi-ui';
import { IconClose } from '@douyinfe/semi-icons';
import { BannerProps } from '@douyinfe/semi-ui/banner';
import { useToggle } from 'hooks/use-toggle';
interface IProps extends BannerProps {
duration?: number;
closeable?: boolean;
}
export const Banner: React.FC<IProps> = ({ type, description, duration = 0 }) => {
export const Banner: React.FC<IProps> = ({ type, description, duration = 0, closeable = true }) => {
const timer = useRef<ReturnType<typeof setTimeout>>();
const [visible, toggleVisible] = useToggle(true);
@ -26,5 +28,5 @@ export const Banner: React.FC<IProps> = ({ type, description, duration = 0 }) =>
if (!visible) return null;
return <SemiBanner type={type} description={description} />;
return <SemiBanner type={type} description={description} closeIcon={closeable ? <IconClose /> : null} />;
};

View File

@ -59,6 +59,7 @@ export const DataRender: React.FC<IProps> = ({
normalContent,
}) => {
if (error) {
console.log(error, errorContent);
return runRender(errorContent, error);
}

View File

@ -1,12 +1,14 @@
import Router from 'next/router';
import React, { useMemo, useEffect, useState, useRef } from 'react';
import cls from 'classnames';
import { useEditor, EditorContent } from '@tiptap/react';
import { BackTop, Toast } from '@douyinfe/semi-ui';
import { BackTop, Toast, Spin, Typography } from '@douyinfe/semi-ui';
import { ILoginUser, IAuthority } from '@think/domains';
import { SecureDocumentIllustration } from 'illustrations/secure-document';
import { useToggle } from 'hooks/use-toggle';
import { useNetwork } from 'hooks/use-network';
import {
useEditor,
EditorContent,
MenuBar,
BaseKit,
DocumentWithTitle,
@ -36,6 +38,8 @@ interface IProps {
style: React.CSSProperties;
}
const { Text } = Typography;
export const Editor: React.FC<IProps> = ({ user: currentUser, documentId, authority, className, style }) => {
const $hasShowUserSettingModal = useRef(false);
const { users, addUser, updateUser } = useCollaborationDocument(documentId);
@ -182,7 +186,31 @@ export const Editor: React.FC<IProps> = ({ user: currentUser, documentId, author
return (
<DataRender
loading={loading}
loadingContent={
<div style={{ margin: '10vh auto' }}>
<Spin tip="正在为您加载编辑器中...">
{/* FIXME: semi-design 的问题,不加 div文字会换行! */}
<div></div>
</Spin>
</div>
}
error={error}
errorContent={(error) => (
<div
style={{
margin: '10vh',
display: 'flex',
justifyContent: 'center',
flexDirection: 'column',
alignItems: 'center',
}}
>
<SecureDocumentIllustration />
<Text style={{ marginTop: 12 }} type="danger">
{(error && error.message) || '未知错误'}
</Text>
</div>
)}
normalContent={() => {
return (
<div className={styles.editorWrap}>
@ -192,6 +220,9 @@ export const Editor: React.FC<IProps> = ({ user: currentUser, documentId, author
description="我们已与您断开连接,您可以继续编辑文档。一旦重新连接,我们会自动重新提交数据。"
/>
)}
{authority && !authority.editable && (
<Banner type="warning" description="您没有编辑权限,暂不能编辑该文档。" closeable={false} />
)}
<header className={className}>
<div>
<MenuBar editor={editor} />

View File

@ -5,6 +5,7 @@ import { IconChevronLeft, IconArticle } from '@douyinfe/semi-icons';
import { useUser } from 'data/user';
import { useDocumentDetail } from 'data/document';
import { useWindowSize } from 'hooks/use-window-size';
import { SecureDocumentIllustration } from 'illustrations/secure-document';
import { Seo } from 'components/seo';
import { Theme } from 'components/theme';
import { DataRender } from 'components/data-render';
@ -43,26 +44,6 @@ export const DocumentEditor: React.FC<IProps> = ({ documentId }) => {
});
}, [document, documentId]);
const DocumentTitle = (
<>
<Tooltip content="返回" position="bottom">
<Button onClick={goback} icon={<IconChevronLeft />} style={{ marginRight: 16 }} />
</Tooltip>
<DataRender
loading={docAuthLoading}
error={docAuthError}
loadingContent={
<Skeleton active placeholder={<Skeleton.Title style={{ width: 80, marginBottom: 8 }} />} loading={true} />
}
normalContent={() => (
<Text ellipsis={{ showTooltip: true }} style={{ width: ~~(windowWith / 4) }}>
{title}
</Text>
)}
/>
</>
);
useEffect(() => {
event.on(CHANGE_DOCUMENT_TITLE, setTitle);
@ -77,7 +58,25 @@ export const DocumentEditor: React.FC<IProps> = ({ documentId }) => {
<Nav
className={styles.headerOuterWrap}
mode="horizontal"
header={DocumentTitle}
header={
<>
<Tooltip content="返回" position="bottom">
<Button onClick={goback} icon={<IconChevronLeft />} style={{ marginRight: 16 }} />
</Tooltip>
<DataRender
loading={docAuthLoading}
error={docAuthError}
loadingContent={
<Skeleton active placeholder={<Skeleton.Title style={{ width: 80 }} />} loading={true} />
}
normalContent={() => (
<Text ellipsis={{ showTooltip: true }} style={{ width: ~~(windowWith / 4) }}>
{title}
</Text>
)}
/>
</>
}
footer={
<Space>
{document && authority.readable && (
@ -100,11 +99,19 @@ export const DocumentEditor: React.FC<IProps> = ({ documentId }) => {
<DataRender
loading={docAuthLoading}
loadingContent={
<div style={{ margin: 24 }}>
<Spin></Spin>
<div style={{ margin: '10vh auto' }}>
<Spin tip="正在为您读取文档中...">
{/* FIXME: semi-design 的问题,不加 div文字会换行! */}
<div></div>
</Spin>
</div>
}
error={docAuthError}
errorContent={
<div style={{ margin: '10vh', textAlign: 'center' }}>
<SecureDocumentIllustration />
</div>
}
normalContent={() => {
return (
<>

View File

@ -1,17 +1,18 @@
import React, { useMemo, useEffect, useState } from 'react';
import { useEditor, EditorContent } from '@tiptap/react';
import { Layout } from '@douyinfe/semi-ui';
import { Layout, Spin, Typography } from '@douyinfe/semi-ui';
import { IDocument, ILoginUser } from '@think/domains';
import { useToggle } from 'hooks/use-toggle';
import {
useEditor,
EditorContent,
BaseKit,
DocumentWithTitle,
getCollaborationExtension,
getCollaborationCursorExtension,
getProvider,
destoryProvider,
DocumentSkeleton,
} from 'tiptap';
import { SecureDocumentIllustration } from 'illustrations/secure-document';
import { DataRender } from 'components/data-render';
import { ImageViewer } from 'components/image-viewer';
import { triggerJoinUser } from 'event';
@ -19,6 +20,7 @@ import { CreateUser } from './user';
import styles from './index.module.scss';
const { Content } = Layout;
const { Text } = Typography;
interface IProps {
user: ILoginUser;
@ -73,8 +75,31 @@ export const Editor: React.FC<IProps> = ({ user, documentId, document, children
return (
<DataRender
loading={loading}
loadingContent={<DocumentSkeleton />}
loadingContent={
<div style={{ margin: '10vh auto' }}>
<Spin tip="正在为您加载文档内容中...">
{/* FIXME: semi-design 的问题,不加 div文字会换行! */}
<div />
</Spin>
</div>
}
error={error}
errorContent={(error) => (
<div
style={{
margin: '10vh',
display: 'flex',
justifyContent: 'center',
flexDirection: 'column',
alignItems: 'center',
}}
>
<SecureDocumentIllustration />
<Text style={{ marginTop: 12 }} type="danger">
{(error && error.message) || '未知错误'}
</Text>
</div>
)}
normalContent={() => {
return (
<Content className={styles.editorWrap}>

View File

@ -1,7 +1,7 @@
import Router from 'next/router';
import React, { useCallback, useMemo, useRef, useState } from 'react';
import cls from 'classnames';
import { Layout, Nav, Space, Button, Typography, Skeleton, Tooltip, Popover, BackTop } from '@douyinfe/semi-ui';
import { Layout, Nav, Space, Button, Typography, Skeleton, Tooltip, Popover, BackTop, Spin } from '@douyinfe/semi-ui';
import { IconEdit, IconArticle } from '@douyinfe/semi-icons';
import { Seo } from 'components/seo';
import { DataRender } from 'components/data-render';
@ -15,7 +15,6 @@ import { useDocumentStyle } from 'hooks/use-document-style';
import { useWindowSize } from 'hooks/use-window-size';
import { useUser } from 'data/user';
import { useDocumentDetail } from 'data/document';
import { DocumentSkeleton } from 'tiptap';
import { Editor } from './editor';
import styles from './index.module.scss';
@ -91,7 +90,14 @@ export const DocumentReader: React.FC<IProps> = ({ documentId }) => {
<DataRender
loading={docAuthLoading}
error={docAuthError}
loadingContent={<DocumentSkeleton />}
loadingContent={
<div style={{ margin: '10vh auto' }}>
<Spin tip="正在为您读取文档中...">
{/* FIXME: semi-design 的问题,不加 div文字会换行! */}
<div></div>
</Spin>
</div>
}
normalContent={() => {
return (
<>

View File

@ -1,7 +1,6 @@
import React from 'react';
import { useEditor, EditorContent } from '@tiptap/react';
import React, { useMemo } from 'react';
import { IDocument } from '@think/domains';
import { BaseKit, DocumentWithTitle } from 'tiptap';
import { useEditor, EditorContent, BaseKit, DocumentWithTitle } from 'tiptap';
import { safeJSONParse } from 'helpers/json';
import { CreateUser } from '../user';
@ -11,16 +10,20 @@ interface IProps {
}
export const DocumentContent: React.FC<IProps> = ({ document, createUserContainerSelector }) => {
const c = safeJSONParse(document.content);
const json = c.default || c;
const json = useMemo(() => {
const c = safeJSONParse(document.content);
const json = c.default || c;
return json;
}, [document]);
const editor = useEditor({
editable: false,
extensions: [...BaseKit, DocumentWithTitle],
content: json,
});
if (!json) return null;
const editor = useEditor(
{
editable: false,
extensions: [...BaseKit, DocumentWithTitle],
content: json,
},
[json]
);
return (
<>

View File

@ -1,4 +1,4 @@
import React, { useMemo, useEffect } from 'react';
import React, { useMemo, useRef, useCallback } from 'react';
import cls from 'classnames';
import {
Layout,
@ -7,12 +7,12 @@ import {
Button,
Typography,
Skeleton,
Input,
Popover,
Modal,
Breadcrumb,
BackTop,
Form,
} from '@douyinfe/semi-ui';
import { FormApi } from '@douyinfe/semi-ui/lib/es/form';
import { IconArticle } from '@douyinfe/semi-icons';
import Link from 'next/link';
import { Seo } from 'components/seo';
@ -37,38 +37,18 @@ interface IProps {
}
export const DocumentPublicReader: React.FC<IProps> = ({ documentId, hideLogo = true }) => {
const $form = useRef<FormApi>();
const { data, loading, error, query } = usePublicDocument(documentId);
const { width, fontSize } = useDocumentStyle();
const editorWrapClassNames = useMemo(() => {
return width === 'standardWidth' ? styles.isStandardWidth : styles.isFullWidth;
}, [width]);
useEffect(() => {
if (!error) return;
if (error.statusCode !== 400) return;
Modal.confirm({
title: '请输入密码',
content: (
<>
<Seo title={'输入密码后查看'} />
<Input
id="js-share-document-password"
style={{ marginTop: 24 }}
autofocus
mode="password"
placeholder="请输入密码"
/>
</>
),
closable: false,
hasCancel: false,
maskClosable: false,
onOk() {
const $input = document.querySelector('#js-share-document-password');
query($input.value);
},
const handleOk = useCallback(() => {
$form.current.validate().then((values) => {
query(values.password);
});
}, [error, query]);
}, [query]);
if (!documentId) return null;
@ -116,6 +96,29 @@ export const DocumentPublicReader: React.FC<IProps> = ({ documentId, hideLogo =
<DataRender
loading={loading}
error={error}
errorContent={(error) => {
if (error.statusCode === 400) {
return (
<div>
<Seo title={'输入密码后查看'} />
<Form
style={{ width: 320, maxWidth: 'calc(100vw - 160px)', margin: '10vh auto' }}
initValues={{ password: '' }}
getFormApi={(formApi) => ($form.current = formApi)}
labelPosition="left"
onSubmit={handleOk}
layout="horizontal"
>
<Form.Input autofocus label="密码" field="password" placeholder="请输入密码" />
<Button type="primary" theme="solid" htmlType="submit">
</Button>
</Form>
</div>
);
}
return <Text>{error.message || error || '未知错误'}</Text>;
}}
loadingContent={
<div className={cls(styles.editorWrap, editorWrapClassNames)} style={{ fontSize }}>
<DocumentSkeleton />

View File

@ -1,5 +1,5 @@
import { createPortal } from 'react-dom';
import { Space, Typography, Avatar } from '@douyinfe/semi-ui';
import { Space, Avatar } from '@douyinfe/semi-ui';
import { IconUser } from '@douyinfe/semi-icons';
import { IDocument } from '@think/domains';
import { LocaleTime } from 'components/locale-time';

View File

@ -1,15 +1,12 @@
import React, { useMemo, useCallback, useState, useEffect } from 'react';
import Router from 'next/router';
import cls from 'classnames';
import { useEditor, EditorContent } from '@tiptap/react';
import {
Button,
Nav,
Space,
Skeleton,
Typography,
Tooltip,
Spin,
Switch,
Popover,
Popconfirm,
@ -19,8 +16,15 @@ import {
import { IconChevronLeft, IconArticle } from '@douyinfe/semi-icons';
import { ILoginUser, ITemplate } from '@think/domains';
import { Theme } from 'components/theme';
import { BaseKit, DocumentWithTitle, getCollaborationExtension, getProvider, MenuBar } from 'tiptap';
import { DataRender } from 'components/data-render';
import {
useEditor,
EditorContent,
BaseKit,
DocumentWithTitle,
getCollaborationExtension,
getProvider,
MenuBar,
} from 'tiptap';
import { User } from 'components/user';
import { DocumentStyle } from 'components/document/style';
import { LogoName } from 'components/logo';
@ -33,13 +37,11 @@ const { Text } = Typography;
interface IProps {
user: ILoginUser;
data: ITemplate;
loading: boolean;
error: Error | null;
updateTemplate: (arg) => Promise<ITemplate>;
deleteTemplate: () => Promise<void>;
}
export const Editor: React.FC<IProps> = ({ user, data, loading, error, updateTemplate, deleteTemplate }) => {
export const Editor: React.FC<IProps> = ({ user, data, updateTemplate, deleteTemplate }) => {
const { width: windowWidth } = useWindowSize();
const [title, setTitle] = useState(data.title);
const provider = useMemo(() => {
@ -50,19 +52,22 @@ export const Editor: React.FC<IProps> = ({ user, data, loading, error, updateTem
user,
docType: 'template',
});
}, [data, user]);
const editor = useEditor({
editable: true,
extensions: [...BaseKit, DocumentWithTitle, getCollaborationExtension(provider)],
onTransaction: ({ transaction }) => {
try {
const title = transaction.doc.content.firstChild.content.firstChild.textContent;
setTitle(title);
} catch (e) {
//
}
}, []);
const editor = useEditor(
{
editable: true,
extensions: [...BaseKit, DocumentWithTitle, getCollaborationExtension(provider)],
onTransaction: ({ transaction }) => {
try {
const title = transaction.doc.content.firstChild.content.firstChild.textContent;
setTitle(title);
} catch (e) {
//
}
},
},
});
[provider]
);
const [isPublic, setPublic] = useState(false);
const { width, fontSize } = useDocumentStyle();
const editorWrapClassNames = useMemo(() => {
@ -100,8 +105,6 @@ export const Editor: React.FC<IProps> = ({ user, data, loading, error, updateTem
};
}, []);
if (!user) return null;
return (
<div className={styles.wrap}>
<header>
@ -109,27 +112,14 @@ export const Editor: React.FC<IProps> = ({ user, data, loading, error, updateTem
style={{ overflow: 'auto' }}
mode="horizontal"
header={
<DataRender
loading={loading}
error={error}
loadingContent={
<Skeleton
active
placeholder={<Skeleton.Title style={{ width: 80, marginBottom: 8 }} />}
loading={true}
/>
}
normalContent={() => (
<>
<Tooltip content="返回" position="bottom">
<Button onClick={goback} icon={<IconChevronLeft />} style={{ marginRight: 16 }} />
</Tooltip>
<Text strong ellipsis={{ showTooltip: true }} style={{ width: ~~(windowWidth / 4) }}>
{title}
</Text>
</>
)}
/>
<>
<Tooltip content="返回" position="bottom">
<Button onClick={goback} icon={<IconChevronLeft />} style={{ marginRight: 16 }} />
</Tooltip>
<Text strong ellipsis={{ showTooltip: true }} style={{ width: ~~(windowWidth / 4) }}>
{title}
</Text>
</>
}
footer={
<Space>
@ -149,32 +139,19 @@ export const Editor: React.FC<IProps> = ({ user, data, loading, error, updateTem
></Nav>
</header>
<main className={styles.contentWrap}>
<DataRender
loading={false}
loadingContent={
<div style={{ margin: 24 }}>
<Spin></Spin>
<div className={styles.editorWrap}>
<header className={editorWrapClassNames}>
<div>
<MenuBar editor={editor} />
</div>
}
error={error}
normalContent={() => {
return (
<div className={styles.editorWrap}>
<header className={editorWrapClassNames}>
<div>
<MenuBar editor={editor} />
</div>
</header>
<main id="js-template-editor-container">
<div className={cls(styles.contentWrap, editorWrapClassNames)} style={{ fontSize }}>
<EditorContent editor={editor} />
</div>
<BackTop target={() => document.querySelector('#js-template-editor-container')} />
</main>
</div>
);
}}
/>
</header>
<main id="js-template-editor-container">
<div className={cls(styles.contentWrap, editorWrapClassNames)} style={{ fontSize }}>
<EditorContent editor={editor} />
</div>
<BackTop target={() => document.querySelector('#js-template-editor-container')} />
</main>
</div>
</main>
</div>
);

View File

@ -27,14 +27,9 @@ export const TemplateEditor: React.FC<IProps> = ({ templateId }) => {
return (
<>
<Seo title={data.title} />
<Editor
user={user}
data={data}
loading={loading}
error={error}
updateTemplate={updateTemplate}
deleteTemplate={deleteTemplate}
/>
{user && data && (
<Editor user={user} data={data} updateTemplate={updateTemplate} deleteTemplate={deleteTemplate} />
)}
</>
);
}}

View File

@ -1,9 +1,8 @@
import React, { useMemo } from 'react';
import cls from 'classnames';
import { useEditor, EditorContent } from '@tiptap/react';
import { Layout, Spin, Typography } from '@douyinfe/semi-ui';
import { IUser, ITemplate } from '@think/domains';
import { BaseKit, DocumentWithTitle } from 'tiptap';
import { useEditor, EditorContent, BaseKit, DocumentWithTitle } from 'tiptap';
import { DataRender } from 'components/data-render';
import { ImageViewer } from 'components/image-viewer';
import { useDocumentStyle } from 'hooks/use-document-style';
@ -21,21 +20,28 @@ interface IProps {
}
export const Editor: React.FC<IProps> = ({ user, data, loading, error }) => {
const c = safeJSONParse(data.content);
let json = c.default || c;
const json = useMemo(() => {
const c = safeJSONParse(data.content);
let json = c.default || c;
if (json && json.content) {
json = {
type: 'doc',
content: json.content.slice(1),
};
}
if (json && json.content) {
json = {
type: 'doc',
content: json.content.slice(1),
};
}
const editor = useEditor({
editable: false,
extensions: [...BaseKit, DocumentWithTitle],
content: json,
});
return json;
}, [data]);
const editor = useEditor(
{
editable: false,
extensions: [...BaseKit, DocumentWithTitle],
content: json,
},
[json]
);
const { width, fontSize } = useDocumentStyle();
const editorWrapClassNames = useMemo(() => {

View File

@ -1,5 +1,5 @@
import type { IComment } from '@think/domains';
import { useState } from 'react';
import React, { useState, useCallback } from 'react';
import useSWR from 'swr';
import { HttpClient } from 'services/http-client';
@ -20,29 +20,38 @@ export const useComments = (documentId) => {
}>(`/comment/document/${documentId}?page=${page}`, (url) => HttpClient.get(url));
const loading = !data && !error;
const addComment = async (data: CreateCommentDto) => {
const ret = await HttpClient.post(`/comment/add`, {
documentId,
...data,
});
mutate();
return ret;
};
const addComment = useCallback(
async (data: CreateCommentDto) => {
const ret = await HttpClient.post(`/comment/add`, {
documentId,
...data,
});
mutate();
return ret;
},
[mutate]
);
const updateComment = async (data: UpdateCommentDto) => {
const ret = await HttpClient.post(`/comment/update`, {
documentId,
...data,
});
mutate();
return ret;
};
const updateComment = useCallback(
async (data: UpdateCommentDto) => {
const ret = await HttpClient.post(`/comment/update`, {
documentId,
...data,
});
mutate();
return ret;
},
[mutate]
);
const deleteComment = async (comment: IComment) => {
const ret = await HttpClient.post(`/comment/delete/${comment.id}`);
mutate();
return ret;
};
const deleteComment = useCallback(
async (comment: IComment) => {
const ret = await HttpClient.post(`/comment/delete/${comment.id}`);
mutate();
return ret;
},
[mutate]
);
return {
data,

View File

@ -53,17 +53,23 @@ export const useDocumentDetail = (documentId, options = null) => {
options
);
const loading = !data && !error;
const update = async (data: IUpdateDocument) => {
const res = await HttpClient.post('/document/update/' + documentId, data);
mutate();
return res;
};
const update = useCallback(
async (data: IUpdateDocument) => {
const res = await HttpClient.post('/document/update/' + documentId, data);
mutate();
return res;
},
[mutate]
);
const toggleStatus = async (data: Partial<Pick<IDocument, 'sharePassword'>>) => {
const ret = await HttpClient.post('/document/share/' + documentId, data);
mutate();
return ret;
};
const toggleStatus = useCallback(
async (data: Partial<Pick<IDocument, 'sharePassword'>>) => {
const ret = await HttpClient.post('/document/share/' + documentId, data);
mutate();
return ret;
},
[mutate]
);
return { data, loading, error, update, toggleStatus };
};
@ -106,13 +112,13 @@ export const useDocumentStar = (documentId) => {
})
);
const toggleStar = async () => {
const toggleStar = useCallback(async () => {
await HttpClient.post('/collector/toggle/', {
type: 'document',
targetId: documentId,
});
mutate();
};
}, [mutate]);
return { data, error, toggleStar };
};
@ -181,34 +187,43 @@ export const useCollaborationDocument = (documentId) => {
);
const loading = !data && !error;
const addUser = async (userName) => {
const ret = await HttpClient.post(`/document/user/${documentId}/add`, {
documentId,
userName,
readable: true,
editable: false,
});
mutate();
return ret;
};
const addUser = useCallback(
async (userName) => {
const ret = await HttpClient.post(`/document/user/${documentId}/add`, {
documentId,
userName,
readable: true,
editable: false,
});
mutate();
return ret;
},
[mutate]
);
const updateUser = async (docAuth: DocAuth) => {
const ret = await HttpClient.post(`/document/user/${documentId}/update`, {
documentId,
...docAuth,
});
mutate();
return ret;
};
const updateUser = useCallback(
async (docAuth: DocAuth) => {
const ret = await HttpClient.post(`/document/user/${documentId}/update`, {
documentId,
...docAuth,
});
mutate();
return ret;
},
[mutate]
);
const deleteUser = async (docAuth: DocAuth) => {
const ret = await HttpClient.post(`/document/user/${documentId}/delete`, {
documentId,
...docAuth,
});
mutate();
return ret;
};
const deleteUser = useCallback(
async (docAuth: DocAuth) => {
const ret = await HttpClient.post(`/document/user/${documentId}/delete`, {
documentId,
...docAuth,
});
mutate();
return ret;
},
[mutate]
);
return { users: data, loading, error, addUser, updateUser, deleteUser };
};

View File

@ -1,5 +1,5 @@
import type { IMessage } from '@think/domains';
import { useState } from 'react';
import React, { useState, useCallback } from 'react';
import useSWR from 'swr';
import { HttpClient } from 'services/http-client';
@ -63,11 +63,14 @@ export const useUnreadMessages = () => {
});
const loading = !data && !error;
const readMessage = async (messageId) => {
const ret = await HttpClient.post(`/message/read/${messageId}`);
mutate();
return ret;
};
const readMessage = useCallback(
async (messageId) => {
const ret = await HttpClient.post(`/message/read/${messageId}`);
mutate();
return ret;
},
[mutate]
);
return {
data,

View File

@ -1,5 +1,5 @@
import type { ITemplate } from '@think/domains';
import { useState } from 'react';
import { useCallback, useState } from 'react';
import useSWR from 'swr';
import { HttpClient } from 'services/http-client';
@ -31,11 +31,14 @@ export const useOwnTemplates = () => {
}>(`/template/own?page=${page}`, (url) => HttpClient.get(url));
const loading = !data && !error;
const addTemplate = async (data): Promise<ITemplate> => {
const ret = await HttpClient.post(`/template/add`, data);
mutate();
return ret as unknown as ITemplate;
};
const addTemplate = useCallback(
async (data): Promise<ITemplate> => {
const ret = await HttpClient.post(`/template/add`, data);
mutate();
return ret as unknown as ITemplate;
},
[mutate]
);
return {
data,
@ -47,23 +50,24 @@ export const useOwnTemplates = () => {
};
export const useTemplate = (templateId) => {
const { data, error, mutate } = useSWR<ITemplate>(`/template/detail/${templateId}`, (url) => HttpClient.get(url), {
revalidateOnMount: true,
});
const { data, error, mutate } = useSWR<ITemplate>(`/template/detail/${templateId}`, (url) => HttpClient.get(url));
const loading = !data && !error;
const updateTemplate = async (data): Promise<ITemplate> => {
const ret = await HttpClient.post(`/template/update`, {
id: templateId,
...data,
});
mutate();
return ret as unknown as ITemplate;
};
const updateTemplate = useCallback(
async (data): Promise<ITemplate> => {
const ret = await HttpClient.post(`/template/update`, {
id: templateId,
...data,
});
mutate();
return ret as unknown as ITemplate;
},
[mutate]
);
const deleteTemplate = async () => {
const deleteTemplate = useCallback(async () => {
await HttpClient.post(`/template/delete/${templateId}`);
};
}, []);
return {
data,

View File

@ -1,6 +1,6 @@
import { CollectType, IDocument, IUser, IWiki, IWikiUser } from '@think/domains';
import useSWR from 'swr';
import { useState } from 'react';
import { useCallback, useState } from 'react';
import { HttpClient } from 'services/http-client';
export type ICreateWiki = Pick<IWiki, 'name' | 'description'>;
@ -47,22 +47,28 @@ export const useOwnWikis = () => {
HttpClient.get(url)
);
const createWiki = async (data: ICreateWiki) => {
const res = await HttpClient.post<IWiki>('/wiki/create', data);
mutate();
return res;
};
const createWiki = useCallback(
async (data: ICreateWiki) => {
const res = await HttpClient.post<IWiki>('/wiki/create', data);
mutate();
return res;
},
[mutate]
);
/**
*
* @param id
* @returns
*/
const deletWiki = async (id) => {
const res = await HttpClient.delete<IWiki>('/wiki/delete/' + id);
mutate();
return res;
};
const deletWiki = useCallback(
async (id) => {
const res = await HttpClient.delete<IWiki>('/wiki/delete/' + id);
mutate();
return res;
},
[mutate]
);
const loading = !data && !error;
const list = (data && data.data) || [];
@ -92,11 +98,14 @@ export const useWikiTocs = (wikiId) => {
);
const loading = !data && !error;
const update = async (relations: Array<{ id: string; parentDocumentId: string }>) => {
const res = await HttpClient.post(`/wiki/tocs/${wikiId}/update`, relations);
mutate();
return res;
};
const update = useCallback(
async (relations: Array<{ id: string; parentDocumentId: string }>) => {
const res = await HttpClient.post(`/wiki/tocs/${wikiId}/update`, relations);
mutate();
return res;
},
[mutate]
);
return { data, loading, error, refresh: mutate, update };
};
@ -128,22 +137,28 @@ export const useWikiDetail = (wikiId) => {
* @param data
* @returns
*/
const update = async (data: IUpdateWiki) => {
const res = await HttpClient.patch('/wiki/update/' + wikiId, data);
mutate();
return res;
};
const update = useCallback(
async (data: IUpdateWiki) => {
const res = await HttpClient.patch('/wiki/update/' + wikiId, data);
mutate();
return res;
},
[mutate]
);
/**
*
* @param data
* @returns
*/
const toggleStatus = async (data) => {
const res = await HttpClient.post('/wiki/share/' + wikiId, data);
mutate();
return res;
};
const toggleStatus = useCallback(
async (data) => {
const res = await HttpClient.post('/wiki/share/' + wikiId, data);
mutate();
return res;
},
[mutate]
);
return { data, loading, error, update, toggleStatus };
};
@ -157,23 +172,32 @@ export const useWikiUsers = (wikiId) => {
const { data, error, mutate } = useSWR<IWikiUser[]>('/wiki/user/' + wikiId, (url) => HttpClient.get(url));
const loading = !data && !error;
const addUser = async (data: IWikiUserOpeateData) => {
const ret = await HttpClient.post(`/wiki/user/${wikiId}/add`, data);
mutate();
return ret;
};
const addUser = useCallback(
async (data: IWikiUserOpeateData) => {
const ret = await HttpClient.post(`/wiki/user/${wikiId}/add`, data);
mutate();
return ret;
},
[mutate]
);
const updateUser = async (data: IWikiUserOpeateData) => {
const ret = await HttpClient.post(`/wiki/user/${wikiId}/update`, data);
mutate();
return ret;
};
const updateUser = useCallback(
async (data: IWikiUserOpeateData) => {
const ret = await HttpClient.post(`/wiki/user/${wikiId}/update`, data);
mutate();
return ret;
},
[mutate]
);
const deleteUser = async (data: IWikiUserOpeateData) => {
const ret = await HttpClient.post(`/wiki/user/${wikiId}/delete`, data);
mutate();
return ret;
};
const deleteUser = useCallback(
async (data: IWikiUserOpeateData) => {
const ret = await HttpClient.post(`/wiki/user/${wikiId}/delete`, data);
mutate();
return ret;
},
[mutate]
);
return {
data,
@ -199,13 +223,13 @@ export const useWikiStar = (wikiId) => {
})
);
const toggleStar = async () => {
const toggleStar = useCallback(async () => {
await HttpClient.post('/collector/toggle/', {
type: CollectType.wiki,
targetId: wikiId,
});
mutate();
};
}, [mutate]);
return { data, error, toggleStar };
};

View File

@ -0,0 +1,188 @@
import React from 'react';
export const SecureDocumentIllustration = () => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
data-name="Layer 1"
width="80%"
height="300"
viewBox="0 0 866.52362 637.05628"
>
<path
d="M971.73819,768.52814v-72.34S999.92985,747.47411,971.73819,768.52814Z"
transform="translate(-166.73819 -131.47186)"
fill="#f1f1f1"
/>
<path
d="M973.47966,768.51541,920.19,719.59417S977.03523,733.5097,973.47966,768.51541Z"
transform="translate(-166.73819 -131.47186)"
fill="#f1f1f1"
/>
<path
d="M743.26636,577.44241a9.09535,9.09535,0,0,1,9.85146-9.872l9.60661-18.431,12.62434,3.10614-13.932,25.83764a9.14461,9.14461,0,0,1-18.15045-.64078Z"
transform="translate(-166.73819 -131.47186)"
fill="#ffb7b7"
/>
<polygon points="633.871 625.527 623.218 625.526 618.15 584.437 633.873 584.438 633.871 625.527" fill="#ffb7b7" />
<path
d="M803.32565,767.32462l-34.34883-.00127v-.43446a13.37025,13.37025,0,0,1,13.36952-13.36931h.00085l20.9791.00085Z"
transform="translate(-166.73819 -131.47186)"
fill="#2f2e41"
/>
<polygon points="719.549 617.569 709.271 620.371 693.57 582.063 708.739 577.927 719.549 617.569" fill="#ffb7b7" />
<path
d="M891.62477,758.288,858.486,767.32462l-.11432-.41915a13.37025,13.37025,0,0,1,9.38068-16.416l.00082-.00022L887.99322,744.97Z"
transform="translate(-166.73819 -131.47186)"
fill="#2f2e41"
/>
<polygon
points="614.423 448.033 609.109 528.727 620.524 619.261 639.221 613.554 640.009 532.663 654.966 486.018 699.25 610.602 719.521 603.123 702.792 522.232 693.345 446.262 614.423 448.033"
fill="#2f2e41"
/>
<path
d="M832.82255,437.80621,801.33678,442.449,789.994,453.66711l-3.5219,40.30669,2.18745,35.702-9.11869,62.959s22.93018-12.72919,40.70053,3.20519,42.63547,2.816,43.147-7.784Z"
transform="translate(-166.73819 -131.47186)"
fill="#cbcbcb"
/>
<path
d="M816.83429,488.35732l-.00043-.04526,15.64133-50.984.20211-.01318c1.11327-.07248,27.33679-1.618,33.20236,11.33006l.02836.06246-1.78163,52.983,2.45371,82.97939-48.50061,10.50471-.35252.07678Z"
transform="translate(-166.73819 -131.47186)"
fill="#2f2e41"
/>
<path
d="M799.50072,484.833l2.86926-43.2209c-20.40666,1.2694-20.09926,15.73786-20.07577,16.3687l-.223,64.64973-4.08709,69.1641,14.86038-1.11441Z"
transform="translate(-166.73819 -131.47186)"
fill="#2f2e41"
/>
<path
d="M753.48519,563.47709l18.11738-42.04748L784.248,498.208l6.60212,41.88934-22.9224,34.38352Z"
transform="translate(-166.73819 -131.47186)"
fill="#2f2e41"
/>
<path
d="M828.36,548.00623a9.09534,9.09534,0,0,1,12.32532-6.52638l14.61634-14.77674,11.14617,6.69216L845.51367,553.973A9.14461,9.14461,0,0,1,828.36,548.00623Z"
transform="translate(-166.73819 -131.47186)"
fill="#ffb7b7"
/>
<path
d="M840.65123,536.08544,870.45248,503.393,850.72593,475.2442l2.03043-13.762L864.38293,447.12l.2269.29336c1.23932,1.60372,30.36218,39.43935,31.19784,53.76257.83853,14.37622-41.02087,50.74162-42.80336,52.28232l-.24766.21428Z"
transform="translate(-166.73819 -131.47186)"
fill="#2f2e41"
/>
<circle cx="647.9164" cy="281.61187" r="21.18132" fill="#ffb7b7" />
<path
d="M834.57593,396.15035l1.02686-2.06675-5.1669-2.56715s-5.69991-9.27437-16.01412-6.66807-14.95472,4.16612-14.95472,4.16612l-5.15383,2.59323,2.58668,2.57367-4.6404,1.55986,3.10011,1.54028-3.60707,2.07328,1.94177,10.62831s3.22513-8.06117,9.42537-4.98062,17.5414-1.59245,17.5414-1.59245l9.853,19.06862s2.03267-6.6845,5.65681-4.9021C836.17091,417.57661,845.42963,402.83144,834.57593,396.15035Z"
transform="translate(-166.73819 -131.47186)"
fill="#2f2e41"
/>
<path
d="M594.90728,516.06962,272.80489,604.901,166.73819,220.3032l322.10239-88.83134Z"
transform="translate(-166.73819 -131.47186)"
fill="#fff"
/>
<path
d="M594.90728,516.06962,272.80489,604.901,166.73819,220.3032l322.10239-88.83134ZM276.92016,597.65144l310.73759-85.69709-102.93244-373.233-310.7376,85.6971Z"
transform="translate(-166.73819 -131.47186)"
fill="#f1f1f1"
/>
<path
d="M418.744,303.76532l-80.741,22.26726a4.46018,4.46018,0,0,1-5.47917-3.11031l-22.26725-80.74105a4.46017,4.46017,0,0,1,3.11031-5.47917l80.74105-22.26725a4.46016,4.46016,0,0,1,5.47916,3.11031l22.26726,80.74105A4.46016,4.46016,0,0,1,418.744,303.76532ZM313.8406,238.42a2.676,2.676,0,0,0-1.86619,3.2875l22.26726,80.74105a2.676,2.676,0,0,0,3.2875,1.86619l80.741-22.26726a2.676,2.676,0,0,0,1.86619-3.2875l-22.26726-80.741a2.676,2.676,0,0,0-3.2875-1.86619Z"
transform="translate(-166.73819 -131.47186)"
fill="#e5e5e5"
/>
<path
d="M398.2766,320.03913l-80.741,22.26726a4.01409,4.01409,0,0,1-4.93125-2.79928L290.337,258.76606a4.01409,4.01409,0,0,1,2.79928-4.93125l80.741-22.26726a4.01409,4.01409,0,0,1,4.93125,2.79928l22.26726,80.74105A4.01408,4.01408,0,0,1,398.2766,320.03913Z"
transform="translate(-166.73819 -131.47186)"
fill="#6c63ff"
/>
<rect
x="263.48929"
y="361.96862"
width="233.72825"
height="9.03209"
transform="translate(-250.48374 -17.16149) rotate(-15.41811)"
fill="#f1f1f1"
/>
<rect
x="269.75017"
y="384.67055"
width="233.72825"
height="9.03209"
transform="translate(-256.29398 -14.67996) rotate(-15.41811)"
fill="#f1f1f1"
/>
<rect
x="276.01104"
y="407.37248"
width="233.72825"
height="9.03209"
transform="translate(-262.10422 -12.19843) rotate(-15.41811)"
fill="#f1f1f1"
/>
<rect
x="287.03019"
y="447.32789"
width="233.72825"
height="9.03209"
transform="translate(-272.33023 -7.83093) rotate(-15.41811)"
fill="#f1f1f1"
/>
<rect
x="293.29106"
y="470.02982"
width="233.72825"
height="9.03209"
transform="translate(-278.14047 -5.34939) rotate(-15.41811)"
fill="#f1f1f1"
/>
<rect
x="299.55194"
y="492.73176"
width="233.72825"
height="9.03209"
transform="translate(-283.9507 -2.86786) rotate(-15.41811)"
fill="#f1f1f1"
/>
<path
d="M698.0144,603.61617H363.88724V204.66055H698.0144Z"
transform="translate(-166.73819 -131.47186)"
fill="#fff"
/>
<path
d="M698.0144,603.61617H363.88724V204.66055H698.0144Zm-328.23263-5.89454H692.11986V210.55508H369.78177Z"
transform="translate(-166.73819 -131.47186)"
fill="#e5e5e5"
/>
<rect x="279.40817" y="244.69464" width="191.03421" height="9.03209" fill="#6c63ff" />
<rect x="279.40817" y="268.17807" width="191.03421" height="9.03209" fill="#6c63ff" />
<rect x="279.40817" y="291.66149" width="191.03421" height="9.03209" fill="#6c63ff" />
<circle cx="263.87741" cy="250.53394" r="5.89453" fill="#6c63ff" />
<rect x="279.40817" y="143.50512" width="191.03421" height="9.03209" fill="#e5e5e5" />
<rect x="279.40817" y="166.98855" width="191.03421" height="9.03209" fill="#e5e5e5" />
<rect x="279.40817" y="190.47198" width="191.03421" height="9.03209" fill="#e5e5e5" />
<circle cx="263.87741" cy="148.36201" r="5.89453" fill="#e5e5e5" />
<rect x="279.40817" y="346.86657" width="191.03421" height="9.03209" fill="#e5e5e5" />
<rect x="279.40817" y="370.35" width="191.03421" height="9.03209" fill="#e5e5e5" />
<rect x="279.40817" y="393.83343" width="191.03421" height="9.03209" fill="#e5e5e5" />
<circle cx="263.87741" cy="351.72346" r="5.89453" fill="#e5e5e5" />
<circle cx="515.67691" cy="438.20509" r="68.29339" fill="#6c63ff" />
<path
d="M701.33633,565.32032V552.25311a18.92123,18.92123,0,1,0-37.84245,0v13.06721a9.83809,9.83809,0,0,0-9.39555,9.82325v30.87926h56.63355V575.14357A9.83809,9.83809,0,0,0,701.33633,565.32032ZM682.4151,539.99384a12.27276,12.27276,0,0,1,12.25846,12.25927v13.04444H670.15665V552.25311A12.27275,12.27275,0,0,1,682.4151,539.99384Z"
transform="translate(-166.73819 -131.47186)"
fill="#fff"
/>
<path
d="M687.50571,580.56948a5.09061,5.09061,0,1,0-7.95433,4.207v11.065H685.278v-11.065A5.08421,5.08421,0,0,0,687.50571,580.56948Z"
transform="translate(-166.73819 -131.47186)"
fill="#6c63ff"
/>
<path
d="M1032.26181,768.12193h-381a1,1,0,0,1,0-2h381a1,1,0,0,1,0,2Z"
transform="translate(-166.73819 -131.47186)"
fill="#cbcbcb"
/>
</svg>
);
};

View File

@ -1,34 +1,7 @@
import { HocuspocusProvider } from '@hocuspocus/provider';
import { Collaboration } from './extensions/collaboration';
import { CollaborationCursor } from './extensions/collaboration-cursor';
import History from '@tiptap/extension-history';
import { getRandomColor } from 'helpers/color';
import { Document } from './extensions/document';
export { BaseKit, CommentKit } from './start-kit';
export { getSchema } from '@tiptap/core';
export * from './react';
export * from './start-kit';
export * from './menubar';
export * from './provider';
export * from './indexdb';
export * from './skeleton';
export const DocumentWithTitle = Document.extend({
content: 'title block+',
});
export { Document, History };
export const getCollaborationExtension = (provider: HocuspocusProvider) => {
return Collaboration.configure({
document: provider.document,
});
};
export const getCollaborationCursorExtension = (provider: HocuspocusProvider, user) => {
return CollaborationCursor.configure({
provider,
user: {
...user,
color: getRandomColor(),
},
});
};

View File

@ -43,7 +43,11 @@ import { Iframe } from './menus/iframe';
import { Table } from './menus/table';
import { Mind } from './menus/mind';
import useTilg from 'tilg';
const _MenuBar: React.FC<{ editor: any }> = ({ editor }) => {
useTilg();
if (!editor) return null;
return (

View File

@ -0,0 +1,4 @@
import { EditorContent, NodeViewWrapper, NodeViewContent } from '@tiptap/react';
import { useEditor } from './useEditor';
export { EditorContent, NodeViewWrapper, NodeViewContent, useEditor };

View File

@ -0,0 +1,35 @@
import { useState, useEffect, DependencyList } from 'react';
import { EditorOptions } from '@tiptap/core';
import { Editor } from '@tiptap/react';
function useForceUpdate() {
const [, setValue] = useState(0);
return () => setValue((value) => value + 1);
}
export const useEditor = (options: Partial<EditorOptions> = {}, deps: DependencyList = []) => {
const [editor, setEditor] = useState<Editor | null>(null);
const forceUpdate = useForceUpdate();
useEffect(() => {
const instance = new Editor(options);
setEditor(instance);
// instance.on('transaction', () => {
// requestAnimationFrame(() => {
// requestAnimationFrame(() => {
// console.log('update');
// forceUpdate();
// });
// });
// });
return () => {
instance.destroy();
};
}, deps);
return editor;
};

View File

@ -1,3 +1,4 @@
import { HocuspocusProvider } from '@hocuspocus/provider';
import { Attachment } from './extensions/attachment';
import { BackgroundColor } from './extensions/background-color';
import { Blockquote } from './extensions/blockquote';
@ -54,6 +55,15 @@ import { TrailingNode } from './extensions/trailing-node';
import { Underline } from './extensions/underline';
import { Paste } from './extensions/paste';
import { getRandomColor } from 'helpers/color';
// 文档
import { Document } from './extensions/document';
// 操作历史
import History from '@tiptap/extension-history';
// 协作
import { Collaboration } from './extensions/collaboration';
import { CollaborationCursor } from './extensions/collaboration-cursor';
export const BaseKit = [
Attachment,
BackgroundColor,
@ -164,3 +174,25 @@ export const CommentKit = [
TrailingNode,
Underline,
];
export { Document, History };
export const DocumentWithTitle = Document.extend({
content: 'title block+',
});
export const getCollaborationExtension = (provider: HocuspocusProvider) => {
return Collaboration.configure({
document: provider.document,
});
};
export const getCollaborationCursorExtension = (provider: HocuspocusProvider, user) => {
return CollaborationCursor.configure({
provider,
user: {
...user,
color: getRandomColor(),
},
});
};

View File

@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect, useRef } from 'react';
import { BubbleMenuPlugin, BubbleMenuPluginProps } from './bubble-menu-plugin';
type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>;
@ -8,9 +8,11 @@ export type BubbleMenuProps = Omit<Optional<BubbleMenuPluginProps, 'pluginKey'>,
};
export const BubbleMenu: React.FC<BubbleMenuProps> = (props) => {
const [element, setElement] = useState<HTMLDivElement | null>(null);
const $element = useRef<HTMLDivElement | null>(null);
useEffect(() => {
const element = $element.current;
if (!element) {
return;
}
@ -41,10 +43,10 @@ export const BubbleMenu: React.FC<BubbleMenuProps> = (props) => {
editor.registerPlugin(plugin);
return () => editor.unregisterPlugin(pluginKey);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [props.editor, element]);
}, [props.editor]);
return (
<div ref={setElement} className={props.className} style={{ visibility: 'hidden' }}>
<div ref={$element} className={props.className} style={{ visibility: 'hidden' }}>
{props.children}
</div>
);