mirror of https://github.com/fantasticit/think.git
refactor: improve performence
This commit is contained in:
parent
5c0d9f54e4
commit
d2bec0f448
|
@ -1,13 +1,15 @@
|
||||||
import React, { useEffect, useRef } from 'react';
|
import React, { useEffect, useRef } from 'react';
|
||||||
import { Banner as SemiBanner } from '@douyinfe/semi-ui';
|
import { Banner as SemiBanner } from '@douyinfe/semi-ui';
|
||||||
|
import { IconClose } from '@douyinfe/semi-icons';
|
||||||
import { BannerProps } from '@douyinfe/semi-ui/banner';
|
import { BannerProps } from '@douyinfe/semi-ui/banner';
|
||||||
import { useToggle } from 'hooks/use-toggle';
|
import { useToggle } from 'hooks/use-toggle';
|
||||||
|
|
||||||
interface IProps extends BannerProps {
|
interface IProps extends BannerProps {
|
||||||
duration?: number;
|
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 timer = useRef<ReturnType<typeof setTimeout>>();
|
||||||
const [visible, toggleVisible] = useToggle(true);
|
const [visible, toggleVisible] = useToggle(true);
|
||||||
|
|
||||||
|
@ -26,5 +28,5 @@ export const Banner: React.FC<IProps> = ({ type, description, duration = 0 }) =>
|
||||||
|
|
||||||
if (!visible) return null;
|
if (!visible) return null;
|
||||||
|
|
||||||
return <SemiBanner type={type} description={description} />;
|
return <SemiBanner type={type} description={description} closeIcon={closeable ? <IconClose /> : null} />;
|
||||||
};
|
};
|
||||||
|
|
|
@ -59,6 +59,7 @@ export const DataRender: React.FC<IProps> = ({
|
||||||
normalContent,
|
normalContent,
|
||||||
}) => {
|
}) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
|
console.log(error, errorContent);
|
||||||
return runRender(errorContent, error);
|
return runRender(errorContent, error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
import Router from 'next/router';
|
import Router from 'next/router';
|
||||||
import React, { useMemo, useEffect, useState, useRef } from 'react';
|
import React, { useMemo, useEffect, useState, useRef } from 'react';
|
||||||
import cls from 'classnames';
|
import cls from 'classnames';
|
||||||
import { useEditor, EditorContent } from '@tiptap/react';
|
import { BackTop, Toast, Spin, Typography } from '@douyinfe/semi-ui';
|
||||||
import { BackTop, Toast } from '@douyinfe/semi-ui';
|
|
||||||
import { ILoginUser, IAuthority } from '@think/domains';
|
import { ILoginUser, IAuthority } from '@think/domains';
|
||||||
|
import { SecureDocumentIllustration } from 'illustrations/secure-document';
|
||||||
import { useToggle } from 'hooks/use-toggle';
|
import { useToggle } from 'hooks/use-toggle';
|
||||||
import { useNetwork } from 'hooks/use-network';
|
import { useNetwork } from 'hooks/use-network';
|
||||||
import {
|
import {
|
||||||
|
useEditor,
|
||||||
|
EditorContent,
|
||||||
MenuBar,
|
MenuBar,
|
||||||
BaseKit,
|
BaseKit,
|
||||||
DocumentWithTitle,
|
DocumentWithTitle,
|
||||||
|
@ -36,6 +38,8 @@ interface IProps {
|
||||||
style: React.CSSProperties;
|
style: React.CSSProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { Text } = Typography;
|
||||||
|
|
||||||
export const Editor: React.FC<IProps> = ({ user: currentUser, documentId, authority, className, style }) => {
|
export const Editor: React.FC<IProps> = ({ user: currentUser, documentId, authority, className, style }) => {
|
||||||
const $hasShowUserSettingModal = useRef(false);
|
const $hasShowUserSettingModal = useRef(false);
|
||||||
const { users, addUser, updateUser } = useCollaborationDocument(documentId);
|
const { users, addUser, updateUser } = useCollaborationDocument(documentId);
|
||||||
|
@ -182,7 +186,31 @@ export const Editor: React.FC<IProps> = ({ user: currentUser, documentId, author
|
||||||
return (
|
return (
|
||||||
<DataRender
|
<DataRender
|
||||||
loading={loading}
|
loading={loading}
|
||||||
|
loadingContent={
|
||||||
|
<div style={{ margin: '10vh auto' }}>
|
||||||
|
<Spin tip="正在为您加载编辑器中...">
|
||||||
|
{/* FIXME: semi-design 的问题,不加 div,文字会换行! */}
|
||||||
|
<div></div>
|
||||||
|
</Spin>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
error={error}
|
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={() => {
|
normalContent={() => {
|
||||||
return (
|
return (
|
||||||
<div className={styles.editorWrap}>
|
<div className={styles.editorWrap}>
|
||||||
|
@ -192,6 +220,9 @@ export const Editor: React.FC<IProps> = ({ user: currentUser, documentId, author
|
||||||
description="我们已与您断开连接,您可以继续编辑文档。一旦重新连接,我们会自动重新提交数据。"
|
description="我们已与您断开连接,您可以继续编辑文档。一旦重新连接,我们会自动重新提交数据。"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{authority && !authority.editable && (
|
||||||
|
<Banner type="warning" description="您没有编辑权限,暂不能编辑该文档。" closeable={false} />
|
||||||
|
)}
|
||||||
<header className={className}>
|
<header className={className}>
|
||||||
<div>
|
<div>
|
||||||
<MenuBar editor={editor} />
|
<MenuBar editor={editor} />
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { IconChevronLeft, IconArticle } from '@douyinfe/semi-icons';
|
||||||
import { useUser } from 'data/user';
|
import { useUser } from 'data/user';
|
||||||
import { useDocumentDetail } from 'data/document';
|
import { useDocumentDetail } from 'data/document';
|
||||||
import { useWindowSize } from 'hooks/use-window-size';
|
import { useWindowSize } from 'hooks/use-window-size';
|
||||||
|
import { SecureDocumentIllustration } from 'illustrations/secure-document';
|
||||||
import { Seo } from 'components/seo';
|
import { Seo } from 'components/seo';
|
||||||
import { Theme } from 'components/theme';
|
import { Theme } from 'components/theme';
|
||||||
import { DataRender } from 'components/data-render';
|
import { DataRender } from 'components/data-render';
|
||||||
|
@ -43,26 +44,6 @@ export const DocumentEditor: React.FC<IProps> = ({ documentId }) => {
|
||||||
});
|
});
|
||||||
}, [document, 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(() => {
|
useEffect(() => {
|
||||||
event.on(CHANGE_DOCUMENT_TITLE, setTitle);
|
event.on(CHANGE_DOCUMENT_TITLE, setTitle);
|
||||||
|
|
||||||
|
@ -77,7 +58,25 @@ export const DocumentEditor: React.FC<IProps> = ({ documentId }) => {
|
||||||
<Nav
|
<Nav
|
||||||
className={styles.headerOuterWrap}
|
className={styles.headerOuterWrap}
|
||||||
mode="horizontal"
|
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={
|
footer={
|
||||||
<Space>
|
<Space>
|
||||||
{document && authority.readable && (
|
{document && authority.readable && (
|
||||||
|
@ -100,11 +99,19 @@ export const DocumentEditor: React.FC<IProps> = ({ documentId }) => {
|
||||||
<DataRender
|
<DataRender
|
||||||
loading={docAuthLoading}
|
loading={docAuthLoading}
|
||||||
loadingContent={
|
loadingContent={
|
||||||
<div style={{ margin: 24 }}>
|
<div style={{ margin: '10vh auto' }}>
|
||||||
<Spin></Spin>
|
<Spin tip="正在为您读取文档中...">
|
||||||
|
{/* FIXME: semi-design 的问题,不加 div,文字会换行! */}
|
||||||
|
<div></div>
|
||||||
|
</Spin>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
error={docAuthError}
|
error={docAuthError}
|
||||||
|
errorContent={
|
||||||
|
<div style={{ margin: '10vh', textAlign: 'center' }}>
|
||||||
|
<SecureDocumentIllustration />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
normalContent={() => {
|
normalContent={() => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -1,17 +1,18 @@
|
||||||
import React, { useMemo, useEffect, useState } from 'react';
|
import React, { useMemo, useEffect, useState } from 'react';
|
||||||
import { useEditor, EditorContent } from '@tiptap/react';
|
import { Layout, Spin, Typography } from '@douyinfe/semi-ui';
|
||||||
import { Layout } from '@douyinfe/semi-ui';
|
|
||||||
import { IDocument, ILoginUser } from '@think/domains';
|
import { IDocument, ILoginUser } from '@think/domains';
|
||||||
import { useToggle } from 'hooks/use-toggle';
|
import { useToggle } from 'hooks/use-toggle';
|
||||||
import {
|
import {
|
||||||
|
useEditor,
|
||||||
|
EditorContent,
|
||||||
BaseKit,
|
BaseKit,
|
||||||
DocumentWithTitle,
|
DocumentWithTitle,
|
||||||
getCollaborationExtension,
|
getCollaborationExtension,
|
||||||
getCollaborationCursorExtension,
|
getCollaborationCursorExtension,
|
||||||
getProvider,
|
getProvider,
|
||||||
destoryProvider,
|
destoryProvider,
|
||||||
DocumentSkeleton,
|
|
||||||
} from 'tiptap';
|
} from 'tiptap';
|
||||||
|
import { SecureDocumentIllustration } from 'illustrations/secure-document';
|
||||||
import { DataRender } from 'components/data-render';
|
import { DataRender } from 'components/data-render';
|
||||||
import { ImageViewer } from 'components/image-viewer';
|
import { ImageViewer } from 'components/image-viewer';
|
||||||
import { triggerJoinUser } from 'event';
|
import { triggerJoinUser } from 'event';
|
||||||
|
@ -19,6 +20,7 @@ import { CreateUser } from './user';
|
||||||
import styles from './index.module.scss';
|
import styles from './index.module.scss';
|
||||||
|
|
||||||
const { Content } = Layout;
|
const { Content } = Layout;
|
||||||
|
const { Text } = Typography;
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
user: ILoginUser;
|
user: ILoginUser;
|
||||||
|
@ -73,8 +75,31 @@ export const Editor: React.FC<IProps> = ({ user, documentId, document, children
|
||||||
return (
|
return (
|
||||||
<DataRender
|
<DataRender
|
||||||
loading={loading}
|
loading={loading}
|
||||||
loadingContent={<DocumentSkeleton />}
|
loadingContent={
|
||||||
|
<div style={{ margin: '10vh auto' }}>
|
||||||
|
<Spin tip="正在为您加载文档内容中...">
|
||||||
|
{/* FIXME: semi-design 的问题,不加 div,文字会换行! */}
|
||||||
|
<div />
|
||||||
|
</Spin>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
error={error}
|
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={() => {
|
normalContent={() => {
|
||||||
return (
|
return (
|
||||||
<Content className={styles.editorWrap}>
|
<Content className={styles.editorWrap}>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import Router from 'next/router';
|
import Router from 'next/router';
|
||||||
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
||||||
import cls from 'classnames';
|
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 { IconEdit, IconArticle } from '@douyinfe/semi-icons';
|
||||||
import { Seo } from 'components/seo';
|
import { Seo } from 'components/seo';
|
||||||
import { DataRender } from 'components/data-render';
|
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 { useWindowSize } from 'hooks/use-window-size';
|
||||||
import { useUser } from 'data/user';
|
import { useUser } from 'data/user';
|
||||||
import { useDocumentDetail } from 'data/document';
|
import { useDocumentDetail } from 'data/document';
|
||||||
import { DocumentSkeleton } from 'tiptap';
|
|
||||||
import { Editor } from './editor';
|
import { Editor } from './editor';
|
||||||
import styles from './index.module.scss';
|
import styles from './index.module.scss';
|
||||||
|
|
||||||
|
@ -91,7 +90,14 @@ export const DocumentReader: React.FC<IProps> = ({ documentId }) => {
|
||||||
<DataRender
|
<DataRender
|
||||||
loading={docAuthLoading}
|
loading={docAuthLoading}
|
||||||
error={docAuthError}
|
error={docAuthError}
|
||||||
loadingContent={<DocumentSkeleton />}
|
loadingContent={
|
||||||
|
<div style={{ margin: '10vh auto' }}>
|
||||||
|
<Spin tip="正在为您读取文档中...">
|
||||||
|
{/* FIXME: semi-design 的问题,不加 div,文字会换行! */}
|
||||||
|
<div></div>
|
||||||
|
</Spin>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
normalContent={() => {
|
normalContent={() => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import React from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { useEditor, EditorContent } from '@tiptap/react';
|
|
||||||
import { IDocument } from '@think/domains';
|
import { IDocument } from '@think/domains';
|
||||||
import { BaseKit, DocumentWithTitle } from 'tiptap';
|
import { useEditor, EditorContent, BaseKit, DocumentWithTitle } from 'tiptap';
|
||||||
import { safeJSONParse } from 'helpers/json';
|
import { safeJSONParse } from 'helpers/json';
|
||||||
import { CreateUser } from '../user';
|
import { CreateUser } from '../user';
|
||||||
|
|
||||||
|
@ -11,16 +10,20 @@ interface IProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DocumentContent: React.FC<IProps> = ({ document, createUserContainerSelector }) => {
|
export const DocumentContent: React.FC<IProps> = ({ document, createUserContainerSelector }) => {
|
||||||
const c = safeJSONParse(document.content);
|
const json = useMemo(() => {
|
||||||
const json = c.default || c;
|
const c = safeJSONParse(document.content);
|
||||||
|
const json = c.default || c;
|
||||||
|
return json;
|
||||||
|
}, [document]);
|
||||||
|
|
||||||
const editor = useEditor({
|
const editor = useEditor(
|
||||||
editable: false,
|
{
|
||||||
extensions: [...BaseKit, DocumentWithTitle],
|
editable: false,
|
||||||
content: json,
|
extensions: [...BaseKit, DocumentWithTitle],
|
||||||
});
|
content: json,
|
||||||
|
},
|
||||||
if (!json) return null;
|
[json]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useMemo, useEffect } from 'react';
|
import React, { useMemo, useRef, useCallback } from 'react';
|
||||||
import cls from 'classnames';
|
import cls from 'classnames';
|
||||||
import {
|
import {
|
||||||
Layout,
|
Layout,
|
||||||
|
@ -7,12 +7,12 @@ import {
|
||||||
Button,
|
Button,
|
||||||
Typography,
|
Typography,
|
||||||
Skeleton,
|
Skeleton,
|
||||||
Input,
|
|
||||||
Popover,
|
Popover,
|
||||||
Modal,
|
|
||||||
Breadcrumb,
|
Breadcrumb,
|
||||||
BackTop,
|
BackTop,
|
||||||
|
Form,
|
||||||
} from '@douyinfe/semi-ui';
|
} from '@douyinfe/semi-ui';
|
||||||
|
import { FormApi } from '@douyinfe/semi-ui/lib/es/form';
|
||||||
import { IconArticle } from '@douyinfe/semi-icons';
|
import { IconArticle } from '@douyinfe/semi-icons';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { Seo } from 'components/seo';
|
import { Seo } from 'components/seo';
|
||||||
|
@ -37,38 +37,18 @@ interface IProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DocumentPublicReader: React.FC<IProps> = ({ documentId, hideLogo = true }) => {
|
export const DocumentPublicReader: React.FC<IProps> = ({ documentId, hideLogo = true }) => {
|
||||||
|
const $form = useRef<FormApi>();
|
||||||
const { data, loading, error, query } = usePublicDocument(documentId);
|
const { data, loading, error, query } = usePublicDocument(documentId);
|
||||||
const { width, fontSize } = useDocumentStyle();
|
const { width, fontSize } = useDocumentStyle();
|
||||||
const editorWrapClassNames = useMemo(() => {
|
const editorWrapClassNames = useMemo(() => {
|
||||||
return width === 'standardWidth' ? styles.isStandardWidth : styles.isFullWidth;
|
return width === 'standardWidth' ? styles.isStandardWidth : styles.isFullWidth;
|
||||||
}, [width]);
|
}, [width]);
|
||||||
|
|
||||||
useEffect(() => {
|
const handleOk = useCallback(() => {
|
||||||
if (!error) return;
|
$form.current.validate().then((values) => {
|
||||||
if (error.statusCode !== 400) return;
|
query(values.password);
|
||||||
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);
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}, [error, query]);
|
}, [query]);
|
||||||
|
|
||||||
if (!documentId) return null;
|
if (!documentId) return null;
|
||||||
|
|
||||||
|
@ -116,6 +96,29 @@ export const DocumentPublicReader: React.FC<IProps> = ({ documentId, hideLogo =
|
||||||
<DataRender
|
<DataRender
|
||||||
loading={loading}
|
loading={loading}
|
||||||
error={error}
|
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={
|
loadingContent={
|
||||||
<div className={cls(styles.editorWrap, editorWrapClassNames)} style={{ fontSize }}>
|
<div className={cls(styles.editorWrap, editorWrapClassNames)} style={{ fontSize }}>
|
||||||
<DocumentSkeleton />
|
<DocumentSkeleton />
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { createPortal } from 'react-dom';
|
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 { IconUser } from '@douyinfe/semi-icons';
|
||||||
import { IDocument } from '@think/domains';
|
import { IDocument } from '@think/domains';
|
||||||
import { LocaleTime } from 'components/locale-time';
|
import { LocaleTime } from 'components/locale-time';
|
||||||
|
|
|
@ -1,15 +1,12 @@
|
||||||
import React, { useMemo, useCallback, useState, useEffect } from 'react';
|
import React, { useMemo, useCallback, useState, useEffect } from 'react';
|
||||||
import Router from 'next/router';
|
import Router from 'next/router';
|
||||||
import cls from 'classnames';
|
import cls from 'classnames';
|
||||||
import { useEditor, EditorContent } from '@tiptap/react';
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Nav,
|
Nav,
|
||||||
Space,
|
Space,
|
||||||
Skeleton,
|
|
||||||
Typography,
|
Typography,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
Spin,
|
|
||||||
Switch,
|
Switch,
|
||||||
Popover,
|
Popover,
|
||||||
Popconfirm,
|
Popconfirm,
|
||||||
|
@ -19,8 +16,15 @@ import {
|
||||||
import { IconChevronLeft, IconArticle } from '@douyinfe/semi-icons';
|
import { IconChevronLeft, IconArticle } from '@douyinfe/semi-icons';
|
||||||
import { ILoginUser, ITemplate } from '@think/domains';
|
import { ILoginUser, ITemplate } from '@think/domains';
|
||||||
import { Theme } from 'components/theme';
|
import { Theme } from 'components/theme';
|
||||||
import { BaseKit, DocumentWithTitle, getCollaborationExtension, getProvider, MenuBar } from 'tiptap';
|
import {
|
||||||
import { DataRender } from 'components/data-render';
|
useEditor,
|
||||||
|
EditorContent,
|
||||||
|
BaseKit,
|
||||||
|
DocumentWithTitle,
|
||||||
|
getCollaborationExtension,
|
||||||
|
getProvider,
|
||||||
|
MenuBar,
|
||||||
|
} from 'tiptap';
|
||||||
import { User } from 'components/user';
|
import { User } from 'components/user';
|
||||||
import { DocumentStyle } from 'components/document/style';
|
import { DocumentStyle } from 'components/document/style';
|
||||||
import { LogoName } from 'components/logo';
|
import { LogoName } from 'components/logo';
|
||||||
|
@ -33,13 +37,11 @@ const { Text } = Typography;
|
||||||
interface IProps {
|
interface IProps {
|
||||||
user: ILoginUser;
|
user: ILoginUser;
|
||||||
data: ITemplate;
|
data: ITemplate;
|
||||||
loading: boolean;
|
|
||||||
error: Error | null;
|
|
||||||
updateTemplate: (arg) => Promise<ITemplate>;
|
updateTemplate: (arg) => Promise<ITemplate>;
|
||||||
deleteTemplate: () => Promise<void>;
|
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 { width: windowWidth } = useWindowSize();
|
||||||
const [title, setTitle] = useState(data.title);
|
const [title, setTitle] = useState(data.title);
|
||||||
const provider = useMemo(() => {
|
const provider = useMemo(() => {
|
||||||
|
@ -50,19 +52,22 @@ export const Editor: React.FC<IProps> = ({ user, data, loading, error, updateTem
|
||||||
user,
|
user,
|
||||||
docType: 'template',
|
docType: 'template',
|
||||||
});
|
});
|
||||||
}, [data, user]);
|
}, []);
|
||||||
const editor = useEditor({
|
const editor = useEditor(
|
||||||
editable: true,
|
{
|
||||||
extensions: [...BaseKit, DocumentWithTitle, getCollaborationExtension(provider)],
|
editable: true,
|
||||||
onTransaction: ({ transaction }) => {
|
extensions: [...BaseKit, DocumentWithTitle, getCollaborationExtension(provider)],
|
||||||
try {
|
onTransaction: ({ transaction }) => {
|
||||||
const title = transaction.doc.content.firstChild.content.firstChild.textContent;
|
try {
|
||||||
setTitle(title);
|
const title = transaction.doc.content.firstChild.content.firstChild.textContent;
|
||||||
} catch (e) {
|
setTitle(title);
|
||||||
//
|
} catch (e) {
|
||||||
}
|
//
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
[provider]
|
||||||
|
);
|
||||||
const [isPublic, setPublic] = useState(false);
|
const [isPublic, setPublic] = useState(false);
|
||||||
const { width, fontSize } = useDocumentStyle();
|
const { width, fontSize } = useDocumentStyle();
|
||||||
const editorWrapClassNames = useMemo(() => {
|
const editorWrapClassNames = useMemo(() => {
|
||||||
|
@ -100,8 +105,6 @@ export const Editor: React.FC<IProps> = ({ user, data, loading, error, updateTem
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (!user) return null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.wrap}>
|
<div className={styles.wrap}>
|
||||||
<header>
|
<header>
|
||||||
|
@ -109,27 +112,14 @@ export const Editor: React.FC<IProps> = ({ user, data, loading, error, updateTem
|
||||||
style={{ overflow: 'auto' }}
|
style={{ overflow: 'auto' }}
|
||||||
mode="horizontal"
|
mode="horizontal"
|
||||||
header={
|
header={
|
||||||
<DataRender
|
<>
|
||||||
loading={loading}
|
<Tooltip content="返回" position="bottom">
|
||||||
error={error}
|
<Button onClick={goback} icon={<IconChevronLeft />} style={{ marginRight: 16 }} />
|
||||||
loadingContent={
|
</Tooltip>
|
||||||
<Skeleton
|
<Text strong ellipsis={{ showTooltip: true }} style={{ width: ~~(windowWidth / 4) }}>
|
||||||
active
|
{title}
|
||||||
placeholder={<Skeleton.Title style={{ width: 80, marginBottom: 8 }} />}
|
</Text>
|
||||||
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>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
}
|
}
|
||||||
footer={
|
footer={
|
||||||
<Space>
|
<Space>
|
||||||
|
@ -149,32 +139,19 @@ export const Editor: React.FC<IProps> = ({ user, data, loading, error, updateTem
|
||||||
></Nav>
|
></Nav>
|
||||||
</header>
|
</header>
|
||||||
<main className={styles.contentWrap}>
|
<main className={styles.contentWrap}>
|
||||||
<DataRender
|
<div className={styles.editorWrap}>
|
||||||
loading={false}
|
<header className={editorWrapClassNames}>
|
||||||
loadingContent={
|
<div>
|
||||||
<div style={{ margin: 24 }}>
|
<MenuBar editor={editor} />
|
||||||
<Spin></Spin>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
</header>
|
||||||
error={error}
|
<main id="js-template-editor-container">
|
||||||
normalContent={() => {
|
<div className={cls(styles.contentWrap, editorWrapClassNames)} style={{ fontSize }}>
|
||||||
return (
|
<EditorContent editor={editor} />
|
||||||
<div className={styles.editorWrap}>
|
</div>
|
||||||
<header className={editorWrapClassNames}>
|
<BackTop target={() => document.querySelector('#js-template-editor-container')} />
|
||||||
<div>
|
</main>
|
||||||
<MenuBar editor={editor} />
|
</div>
|
||||||
</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>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -27,14 +27,9 @@ export const TemplateEditor: React.FC<IProps> = ({ templateId }) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Seo title={data.title} />
|
<Seo title={data.title} />
|
||||||
<Editor
|
{user && data && (
|
||||||
user={user}
|
<Editor user={user} data={data} updateTemplate={updateTemplate} deleteTemplate={deleteTemplate} />
|
||||||
data={data}
|
)}
|
||||||
loading={loading}
|
|
||||||
error={error}
|
|
||||||
updateTemplate={updateTemplate}
|
|
||||||
deleteTemplate={deleteTemplate}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import cls from 'classnames';
|
import cls from 'classnames';
|
||||||
import { useEditor, EditorContent } from '@tiptap/react';
|
|
||||||
import { Layout, Spin, Typography } from '@douyinfe/semi-ui';
|
import { Layout, Spin, Typography } from '@douyinfe/semi-ui';
|
||||||
import { IUser, ITemplate } from '@think/domains';
|
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 { DataRender } from 'components/data-render';
|
||||||
import { ImageViewer } from 'components/image-viewer';
|
import { ImageViewer } from 'components/image-viewer';
|
||||||
import { useDocumentStyle } from 'hooks/use-document-style';
|
import { useDocumentStyle } from 'hooks/use-document-style';
|
||||||
|
@ -21,21 +20,28 @@ interface IProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Editor: React.FC<IProps> = ({ user, data, loading, error }) => {
|
export const Editor: React.FC<IProps> = ({ user, data, loading, error }) => {
|
||||||
const c = safeJSONParse(data.content);
|
const json = useMemo(() => {
|
||||||
let json = c.default || c;
|
const c = safeJSONParse(data.content);
|
||||||
|
let json = c.default || c;
|
||||||
|
|
||||||
if (json && json.content) {
|
if (json && json.content) {
|
||||||
json = {
|
json = {
|
||||||
type: 'doc',
|
type: 'doc',
|
||||||
content: json.content.slice(1),
|
content: json.content.slice(1),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const editor = useEditor({
|
return json;
|
||||||
editable: false,
|
}, [data]);
|
||||||
extensions: [...BaseKit, DocumentWithTitle],
|
|
||||||
content: json,
|
const editor = useEditor(
|
||||||
});
|
{
|
||||||
|
editable: false,
|
||||||
|
extensions: [...BaseKit, DocumentWithTitle],
|
||||||
|
content: json,
|
||||||
|
},
|
||||||
|
[json]
|
||||||
|
);
|
||||||
|
|
||||||
const { width, fontSize } = useDocumentStyle();
|
const { width, fontSize } = useDocumentStyle();
|
||||||
const editorWrapClassNames = useMemo(() => {
|
const editorWrapClassNames = useMemo(() => {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import type { IComment } from '@think/domains';
|
import type { IComment } from '@think/domains';
|
||||||
import { useState } from 'react';
|
import React, { useState, useCallback } from 'react';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { HttpClient } from 'services/http-client';
|
import { HttpClient } from 'services/http-client';
|
||||||
|
|
||||||
|
@ -20,29 +20,38 @@ export const useComments = (documentId) => {
|
||||||
}>(`/comment/document/${documentId}?page=${page}`, (url) => HttpClient.get(url));
|
}>(`/comment/document/${documentId}?page=${page}`, (url) => HttpClient.get(url));
|
||||||
const loading = !data && !error;
|
const loading = !data && !error;
|
||||||
|
|
||||||
const addComment = async (data: CreateCommentDto) => {
|
const addComment = useCallback(
|
||||||
const ret = await HttpClient.post(`/comment/add`, {
|
async (data: CreateCommentDto) => {
|
||||||
documentId,
|
const ret = await HttpClient.post(`/comment/add`, {
|
||||||
...data,
|
documentId,
|
||||||
});
|
...data,
|
||||||
mutate();
|
});
|
||||||
return ret;
|
mutate();
|
||||||
};
|
return ret;
|
||||||
|
},
|
||||||
|
[mutate]
|
||||||
|
);
|
||||||
|
|
||||||
const updateComment = async (data: UpdateCommentDto) => {
|
const updateComment = useCallback(
|
||||||
const ret = await HttpClient.post(`/comment/update`, {
|
async (data: UpdateCommentDto) => {
|
||||||
documentId,
|
const ret = await HttpClient.post(`/comment/update`, {
|
||||||
...data,
|
documentId,
|
||||||
});
|
...data,
|
||||||
mutate();
|
});
|
||||||
return ret;
|
mutate();
|
||||||
};
|
return ret;
|
||||||
|
},
|
||||||
|
[mutate]
|
||||||
|
);
|
||||||
|
|
||||||
const deleteComment = async (comment: IComment) => {
|
const deleteComment = useCallback(
|
||||||
const ret = await HttpClient.post(`/comment/delete/${comment.id}`);
|
async (comment: IComment) => {
|
||||||
mutate();
|
const ret = await HttpClient.post(`/comment/delete/${comment.id}`);
|
||||||
return ret;
|
mutate();
|
||||||
};
|
return ret;
|
||||||
|
},
|
||||||
|
[mutate]
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data,
|
data,
|
||||||
|
|
|
@ -53,17 +53,23 @@ export const useDocumentDetail = (documentId, options = null) => {
|
||||||
options
|
options
|
||||||
);
|
);
|
||||||
const loading = !data && !error;
|
const loading = !data && !error;
|
||||||
const update = async (data: IUpdateDocument) => {
|
const update = useCallback(
|
||||||
const res = await HttpClient.post('/document/update/' + documentId, data);
|
async (data: IUpdateDocument) => {
|
||||||
mutate();
|
const res = await HttpClient.post('/document/update/' + documentId, data);
|
||||||
return res;
|
mutate();
|
||||||
};
|
return res;
|
||||||
|
},
|
||||||
|
[mutate]
|
||||||
|
);
|
||||||
|
|
||||||
const toggleStatus = async (data: Partial<Pick<IDocument, 'sharePassword'>>) => {
|
const toggleStatus = useCallback(
|
||||||
const ret = await HttpClient.post('/document/share/' + documentId, data);
|
async (data: Partial<Pick<IDocument, 'sharePassword'>>) => {
|
||||||
mutate();
|
const ret = await HttpClient.post('/document/share/' + documentId, data);
|
||||||
return ret;
|
mutate();
|
||||||
};
|
return ret;
|
||||||
|
},
|
||||||
|
[mutate]
|
||||||
|
);
|
||||||
|
|
||||||
return { data, loading, error, update, toggleStatus };
|
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/', {
|
await HttpClient.post('/collector/toggle/', {
|
||||||
type: 'document',
|
type: 'document',
|
||||||
targetId: documentId,
|
targetId: documentId,
|
||||||
});
|
});
|
||||||
mutate();
|
mutate();
|
||||||
};
|
}, [mutate]);
|
||||||
|
|
||||||
return { data, error, toggleStar };
|
return { data, error, toggleStar };
|
||||||
};
|
};
|
||||||
|
@ -181,34 +187,43 @@ export const useCollaborationDocument = (documentId) => {
|
||||||
);
|
);
|
||||||
const loading = !data && !error;
|
const loading = !data && !error;
|
||||||
|
|
||||||
const addUser = async (userName) => {
|
const addUser = useCallback(
|
||||||
const ret = await HttpClient.post(`/document/user/${documentId}/add`, {
|
async (userName) => {
|
||||||
documentId,
|
const ret = await HttpClient.post(`/document/user/${documentId}/add`, {
|
||||||
userName,
|
documentId,
|
||||||
readable: true,
|
userName,
|
||||||
editable: false,
|
readable: true,
|
||||||
});
|
editable: false,
|
||||||
mutate();
|
});
|
||||||
return ret;
|
mutate();
|
||||||
};
|
return ret;
|
||||||
|
},
|
||||||
|
[mutate]
|
||||||
|
);
|
||||||
|
|
||||||
const updateUser = async (docAuth: DocAuth) => {
|
const updateUser = useCallback(
|
||||||
const ret = await HttpClient.post(`/document/user/${documentId}/update`, {
|
async (docAuth: DocAuth) => {
|
||||||
documentId,
|
const ret = await HttpClient.post(`/document/user/${documentId}/update`, {
|
||||||
...docAuth,
|
documentId,
|
||||||
});
|
...docAuth,
|
||||||
mutate();
|
});
|
||||||
return ret;
|
mutate();
|
||||||
};
|
return ret;
|
||||||
|
},
|
||||||
|
[mutate]
|
||||||
|
);
|
||||||
|
|
||||||
const deleteUser = async (docAuth: DocAuth) => {
|
const deleteUser = useCallback(
|
||||||
const ret = await HttpClient.post(`/document/user/${documentId}/delete`, {
|
async (docAuth: DocAuth) => {
|
||||||
documentId,
|
const ret = await HttpClient.post(`/document/user/${documentId}/delete`, {
|
||||||
...docAuth,
|
documentId,
|
||||||
});
|
...docAuth,
|
||||||
mutate();
|
});
|
||||||
return ret;
|
mutate();
|
||||||
};
|
return ret;
|
||||||
|
},
|
||||||
|
[mutate]
|
||||||
|
);
|
||||||
|
|
||||||
return { users: data, loading, error, addUser, updateUser, deleteUser };
|
return { users: data, loading, error, addUser, updateUser, deleteUser };
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import type { IMessage } from '@think/domains';
|
import type { IMessage } from '@think/domains';
|
||||||
import { useState } from 'react';
|
import React, { useState, useCallback } from 'react';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { HttpClient } from 'services/http-client';
|
import { HttpClient } from 'services/http-client';
|
||||||
|
|
||||||
|
@ -63,11 +63,14 @@ export const useUnreadMessages = () => {
|
||||||
});
|
});
|
||||||
const loading = !data && !error;
|
const loading = !data && !error;
|
||||||
|
|
||||||
const readMessage = async (messageId) => {
|
const readMessage = useCallback(
|
||||||
const ret = await HttpClient.post(`/message/read/${messageId}`);
|
async (messageId) => {
|
||||||
mutate();
|
const ret = await HttpClient.post(`/message/read/${messageId}`);
|
||||||
return ret;
|
mutate();
|
||||||
};
|
return ret;
|
||||||
|
},
|
||||||
|
[mutate]
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data,
|
data,
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import type { ITemplate } from '@think/domains';
|
import type { ITemplate } from '@think/domains';
|
||||||
import { useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { HttpClient } from 'services/http-client';
|
import { HttpClient } from 'services/http-client';
|
||||||
|
|
||||||
|
@ -31,11 +31,14 @@ export const useOwnTemplates = () => {
|
||||||
}>(`/template/own?page=${page}`, (url) => HttpClient.get(url));
|
}>(`/template/own?page=${page}`, (url) => HttpClient.get(url));
|
||||||
const loading = !data && !error;
|
const loading = !data && !error;
|
||||||
|
|
||||||
const addTemplate = async (data): Promise<ITemplate> => {
|
const addTemplate = useCallback(
|
||||||
const ret = await HttpClient.post(`/template/add`, data);
|
async (data): Promise<ITemplate> => {
|
||||||
mutate();
|
const ret = await HttpClient.post(`/template/add`, data);
|
||||||
return ret as unknown as ITemplate;
|
mutate();
|
||||||
};
|
return ret as unknown as ITemplate;
|
||||||
|
},
|
||||||
|
[mutate]
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data,
|
data,
|
||||||
|
@ -47,23 +50,24 @@ export const useOwnTemplates = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useTemplate = (templateId) => {
|
export const useTemplate = (templateId) => {
|
||||||
const { data, error, mutate } = useSWR<ITemplate>(`/template/detail/${templateId}`, (url) => HttpClient.get(url), {
|
const { data, error, mutate } = useSWR<ITemplate>(`/template/detail/${templateId}`, (url) => HttpClient.get(url));
|
||||||
revalidateOnMount: true,
|
|
||||||
});
|
|
||||||
const loading = !data && !error;
|
const loading = !data && !error;
|
||||||
|
|
||||||
const updateTemplate = async (data): Promise<ITemplate> => {
|
const updateTemplate = useCallback(
|
||||||
const ret = await HttpClient.post(`/template/update`, {
|
async (data): Promise<ITemplate> => {
|
||||||
id: templateId,
|
const ret = await HttpClient.post(`/template/update`, {
|
||||||
...data,
|
id: templateId,
|
||||||
});
|
...data,
|
||||||
mutate();
|
});
|
||||||
return ret as unknown as ITemplate;
|
mutate();
|
||||||
};
|
return ret as unknown as ITemplate;
|
||||||
|
},
|
||||||
|
[mutate]
|
||||||
|
);
|
||||||
|
|
||||||
const deleteTemplate = async () => {
|
const deleteTemplate = useCallback(async () => {
|
||||||
await HttpClient.post(`/template/delete/${templateId}`);
|
await HttpClient.post(`/template/delete/${templateId}`);
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data,
|
data,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { CollectType, IDocument, IUser, IWiki, IWikiUser } from '@think/domains';
|
import { CollectType, IDocument, IUser, IWiki, IWikiUser } from '@think/domains';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
import { HttpClient } from 'services/http-client';
|
import { HttpClient } from 'services/http-client';
|
||||||
|
|
||||||
export type ICreateWiki = Pick<IWiki, 'name' | 'description'>;
|
export type ICreateWiki = Pick<IWiki, 'name' | 'description'>;
|
||||||
|
@ -47,22 +47,28 @@ export const useOwnWikis = () => {
|
||||||
HttpClient.get(url)
|
HttpClient.get(url)
|
||||||
);
|
);
|
||||||
|
|
||||||
const createWiki = async (data: ICreateWiki) => {
|
const createWiki = useCallback(
|
||||||
const res = await HttpClient.post<IWiki>('/wiki/create', data);
|
async (data: ICreateWiki) => {
|
||||||
mutate();
|
const res = await HttpClient.post<IWiki>('/wiki/create', data);
|
||||||
return res;
|
mutate();
|
||||||
};
|
return res;
|
||||||
|
},
|
||||||
|
[mutate]
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 删除文档
|
* 删除文档
|
||||||
* @param id
|
* @param id
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
const deletWiki = async (id) => {
|
const deletWiki = useCallback(
|
||||||
const res = await HttpClient.delete<IWiki>('/wiki/delete/' + id);
|
async (id) => {
|
||||||
mutate();
|
const res = await HttpClient.delete<IWiki>('/wiki/delete/' + id);
|
||||||
return res;
|
mutate();
|
||||||
};
|
return res;
|
||||||
|
},
|
||||||
|
[mutate]
|
||||||
|
);
|
||||||
|
|
||||||
const loading = !data && !error;
|
const loading = !data && !error;
|
||||||
const list = (data && data.data) || [];
|
const list = (data && data.data) || [];
|
||||||
|
@ -92,11 +98,14 @@ export const useWikiTocs = (wikiId) => {
|
||||||
);
|
);
|
||||||
const loading = !data && !error;
|
const loading = !data && !error;
|
||||||
|
|
||||||
const update = async (relations: Array<{ id: string; parentDocumentId: string }>) => {
|
const update = useCallback(
|
||||||
const res = await HttpClient.post(`/wiki/tocs/${wikiId}/update`, relations);
|
async (relations: Array<{ id: string; parentDocumentId: string }>) => {
|
||||||
mutate();
|
const res = await HttpClient.post(`/wiki/tocs/${wikiId}/update`, relations);
|
||||||
return res;
|
mutate();
|
||||||
};
|
return res;
|
||||||
|
},
|
||||||
|
[mutate]
|
||||||
|
);
|
||||||
|
|
||||||
return { data, loading, error, refresh: mutate, update };
|
return { data, loading, error, refresh: mutate, update };
|
||||||
};
|
};
|
||||||
|
@ -128,22 +137,28 @@ export const useWikiDetail = (wikiId) => {
|
||||||
* @param data
|
* @param data
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
const update = async (data: IUpdateWiki) => {
|
const update = useCallback(
|
||||||
const res = await HttpClient.patch('/wiki/update/' + wikiId, data);
|
async (data: IUpdateWiki) => {
|
||||||
mutate();
|
const res = await HttpClient.patch('/wiki/update/' + wikiId, data);
|
||||||
return res;
|
mutate();
|
||||||
};
|
return res;
|
||||||
|
},
|
||||||
|
[mutate]
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 公开或私有知识库
|
* 公开或私有知识库
|
||||||
* @param data
|
* @param data
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
const toggleStatus = async (data) => {
|
const toggleStatus = useCallback(
|
||||||
const res = await HttpClient.post('/wiki/share/' + wikiId, data);
|
async (data) => {
|
||||||
mutate();
|
const res = await HttpClient.post('/wiki/share/' + wikiId, data);
|
||||||
return res;
|
mutate();
|
||||||
};
|
return res;
|
||||||
|
},
|
||||||
|
[mutate]
|
||||||
|
);
|
||||||
|
|
||||||
return { data, loading, error, update, toggleStatus };
|
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 { data, error, mutate } = useSWR<IWikiUser[]>('/wiki/user/' + wikiId, (url) => HttpClient.get(url));
|
||||||
const loading = !data && !error;
|
const loading = !data && !error;
|
||||||
|
|
||||||
const addUser = async (data: IWikiUserOpeateData) => {
|
const addUser = useCallback(
|
||||||
const ret = await HttpClient.post(`/wiki/user/${wikiId}/add`, data);
|
async (data: IWikiUserOpeateData) => {
|
||||||
mutate();
|
const ret = await HttpClient.post(`/wiki/user/${wikiId}/add`, data);
|
||||||
return ret;
|
mutate();
|
||||||
};
|
return ret;
|
||||||
|
},
|
||||||
|
[mutate]
|
||||||
|
);
|
||||||
|
|
||||||
const updateUser = async (data: IWikiUserOpeateData) => {
|
const updateUser = useCallback(
|
||||||
const ret = await HttpClient.post(`/wiki/user/${wikiId}/update`, data);
|
async (data: IWikiUserOpeateData) => {
|
||||||
mutate();
|
const ret = await HttpClient.post(`/wiki/user/${wikiId}/update`, data);
|
||||||
return ret;
|
mutate();
|
||||||
};
|
return ret;
|
||||||
|
},
|
||||||
|
[mutate]
|
||||||
|
);
|
||||||
|
|
||||||
const deleteUser = async (data: IWikiUserOpeateData) => {
|
const deleteUser = useCallback(
|
||||||
const ret = await HttpClient.post(`/wiki/user/${wikiId}/delete`, data);
|
async (data: IWikiUserOpeateData) => {
|
||||||
mutate();
|
const ret = await HttpClient.post(`/wiki/user/${wikiId}/delete`, data);
|
||||||
return ret;
|
mutate();
|
||||||
};
|
return ret;
|
||||||
|
},
|
||||||
|
[mutate]
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data,
|
data,
|
||||||
|
@ -199,13 +223,13 @@ export const useWikiStar = (wikiId) => {
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const toggleStar = async () => {
|
const toggleStar = useCallback(async () => {
|
||||||
await HttpClient.post('/collector/toggle/', {
|
await HttpClient.post('/collector/toggle/', {
|
||||||
type: CollectType.wiki,
|
type: CollectType.wiki,
|
||||||
targetId: wikiId,
|
targetId: wikiId,
|
||||||
});
|
});
|
||||||
mutate();
|
mutate();
|
||||||
};
|
}, [mutate]);
|
||||||
|
|
||||||
return { data, error, toggleStar };
|
return { data, error, toggleStar };
|
||||||
};
|
};
|
||||||
|
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -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 { getSchema } from '@tiptap/core';
|
||||||
|
export * from './react';
|
||||||
|
export * from './start-kit';
|
||||||
export * from './menubar';
|
export * from './menubar';
|
||||||
export * from './provider';
|
export * from './provider';
|
||||||
export * from './indexdb';
|
export * from './indexdb';
|
||||||
export * from './skeleton';
|
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(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
|
@ -43,7 +43,11 @@ import { Iframe } from './menus/iframe';
|
||||||
import { Table } from './menus/table';
|
import { Table } from './menus/table';
|
||||||
import { Mind } from './menus/mind';
|
import { Mind } from './menus/mind';
|
||||||
|
|
||||||
|
import useTilg from 'tilg';
|
||||||
|
|
||||||
const _MenuBar: React.FC<{ editor: any }> = ({ editor }) => {
|
const _MenuBar: React.FC<{ editor: any }> = ({ editor }) => {
|
||||||
|
useTilg();
|
||||||
|
|
||||||
if (!editor) return null;
|
if (!editor) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
import { EditorContent, NodeViewWrapper, NodeViewContent } from '@tiptap/react';
|
||||||
|
import { useEditor } from './useEditor';
|
||||||
|
|
||||||
|
export { EditorContent, NodeViewWrapper, NodeViewContent, useEditor };
|
|
@ -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;
|
||||||
|
};
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { HocuspocusProvider } from '@hocuspocus/provider';
|
||||||
import { Attachment } from './extensions/attachment';
|
import { Attachment } from './extensions/attachment';
|
||||||
import { BackgroundColor } from './extensions/background-color';
|
import { BackgroundColor } from './extensions/background-color';
|
||||||
import { Blockquote } from './extensions/blockquote';
|
import { Blockquote } from './extensions/blockquote';
|
||||||
|
@ -54,6 +55,15 @@ import { TrailingNode } from './extensions/trailing-node';
|
||||||
import { Underline } from './extensions/underline';
|
import { Underline } from './extensions/underline';
|
||||||
import { Paste } from './extensions/paste';
|
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 = [
|
export const BaseKit = [
|
||||||
Attachment,
|
Attachment,
|
||||||
BackgroundColor,
|
BackgroundColor,
|
||||||
|
@ -164,3 +174,25 @@ export const CommentKit = [
|
||||||
TrailingNode,
|
TrailingNode,
|
||||||
Underline,
|
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(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useRef } from 'react';
|
||||||
import { BubbleMenuPlugin, BubbleMenuPluginProps } from './bubble-menu-plugin';
|
import { BubbleMenuPlugin, BubbleMenuPluginProps } from './bubble-menu-plugin';
|
||||||
|
|
||||||
type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>;
|
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) => {
|
export const BubbleMenu: React.FC<BubbleMenuProps> = (props) => {
|
||||||
const [element, setElement] = useState<HTMLDivElement | null>(null);
|
const $element = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const element = $element.current;
|
||||||
|
|
||||||
if (!element) {
|
if (!element) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -41,10 +43,10 @@ export const BubbleMenu: React.FC<BubbleMenuProps> = (props) => {
|
||||||
editor.registerPlugin(plugin);
|
editor.registerPlugin(plugin);
|
||||||
return () => editor.unregisterPlugin(pluginKey);
|
return () => editor.unregisterPlugin(pluginKey);
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [props.editor, element]);
|
}, [props.editor]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={setElement} className={props.className} style={{ visibility: 'hidden' }}>
|
<div ref={$element} className={props.className} style={{ visibility: 'hidden' }}>
|
||||||
{props.children}
|
{props.children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
Loading…
Reference in New Issue