mirror of https://github.com/fantasticit/think.git
client: improve perf
This commit is contained in:
parent
64113f48f1
commit
a9892a3fa8
|
@ -89,7 +89,8 @@ export const ColorPicker: React.FC<{
|
||||||
const [visible, toggleVisible] = useToggle(false);
|
const [visible, toggleVisible] = useToggle(false);
|
||||||
|
|
||||||
const content = useMemo(
|
const content = useMemo(
|
||||||
() => (
|
() =>
|
||||||
|
!visible ? null : (
|
||||||
<div style={{ padding: isMobile ? '24px 0 24px' : '12px 16px', width: isMobile ? 'auto' : 272 }}>
|
<div style={{ padding: isMobile ? '24px 0 24px' : '12px 16px', width: isMobile ? 'auto' : 272 }}>
|
||||||
<div className={styles.emptyWrap} onClick={() => onSetColor(null)}>
|
<div className={styles.emptyWrap} onClick={() => onSetColor(null)}>
|
||||||
<span></span>
|
<span></span>
|
||||||
|
@ -107,7 +108,7 @@ export const ColorPicker: React.FC<{
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
[onSetColor, isMobile]
|
[onSetColor, isMobile, visible]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (disabled) return <span style={{ display: 'inline-block' }}>{children}</span>;
|
if (disabled) return <span style={{ display: 'inline-block' }}>{children}</span>;
|
||||||
|
@ -132,7 +133,14 @@ export const ColorPicker: React.FC<{
|
||||||
</span>
|
</span>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<Dropdown zIndex={10000} trigger="click" position={'bottomLeft'} render={content}>
|
<Dropdown
|
||||||
|
visible={visible}
|
||||||
|
onVisibleChange={toggleVisible}
|
||||||
|
zIndex={10000}
|
||||||
|
trigger="click"
|
||||||
|
position={'bottomLeft'}
|
||||||
|
render={content}
|
||||||
|
>
|
||||||
<span style={{ display: 'inline-block' }}>{children}</span>
|
<span style={{ display: 'inline-block' }}>{children}</span>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -4,15 +4,17 @@ import React, { useMemo } from 'react';
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
|
|
||||||
export const defaultLoading = <Spin />;
|
export const defaultLoading = (
|
||||||
|
<div style={{ margin: 'auto' }}>
|
||||||
|
<Spin />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
export const defaultRenderError = (error) => {
|
export const defaultRenderError = (error) => {
|
||||||
return <Text>{(error && error.message) || '未知错误'}</Text>;
|
return <Text>{(error && error.message) || '未知错误'}</Text>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const defaultEmpty = (
|
const emptyStyle: React.CSSProperties = {
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
|
@ -21,8 +23,10 @@ export const defaultEmpty = (
|
||||||
top: '50%',
|
top: '50%',
|
||||||
left: '50%',
|
left: '50%',
|
||||||
transform: 'translate(-50%, -50%)',
|
transform: 'translate(-50%, -50%)',
|
||||||
}}
|
};
|
||||||
>
|
|
||||||
|
export const defaultEmpty = (
|
||||||
|
<div style={emptyStyle}>
|
||||||
<div>
|
<div>
|
||||||
<Empty />
|
<Empty />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import deepEqual from 'deep-equal';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { defaultEmpty, defaultLoading, defaultRenderError, Render } from './constant';
|
import { defaultEmpty, defaultLoading, defaultRenderError, Render } from './constant';
|
||||||
|
@ -15,7 +16,7 @@ interface IProps {
|
||||||
normalContent: RenderProps;
|
normalContent: RenderProps;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DataRender: React.FC<IProps> = ({
|
export const _DataRender: React.FC<IProps> = ({
|
||||||
loading,
|
loading,
|
||||||
error,
|
error,
|
||||||
empty,
|
empty,
|
||||||
|
@ -36,3 +37,7 @@ export const DataRender: React.FC<IProps> = ({
|
||||||
<LoadingWrap loading={loading} loadingContent={loadingContent} normalContent={loading ? null : normalContent} />
|
<LoadingWrap loading={loading} loadingContent={loadingContent} normalContent={loading ? null : normalContent} />
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const DataRender = React.memo(_DataRender, (prevProps, nextProps) => {
|
||||||
|
return deepEqual(prevProps, nextProps);
|
||||||
|
});
|
||||||
|
|
|
@ -16,14 +16,26 @@ interface IProps {
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
|
|
||||||
|
const mobileContainerStyle: React.CSSProperties = { maxWidth: '96vw', maxHeight: '60vh', overflow: 'auto' };
|
||||||
|
|
||||||
|
const pcContainerStyle: React.CSSProperties = {
|
||||||
|
width: 412,
|
||||||
|
maxWidth: '96vw',
|
||||||
|
padding: '0 24px',
|
||||||
|
maxHeight: '60vh',
|
||||||
|
overflow: 'auto',
|
||||||
|
};
|
||||||
|
|
||||||
export const DocumentCollaboration: React.FC<IProps> = ({ wikiId, documentId, disabled = false }) => {
|
export const DocumentCollaboration: React.FC<IProps> = ({ wikiId, documentId, disabled = false }) => {
|
||||||
const { isMobile } = IsOnMobile.useHook();
|
const { isMobile } = IsOnMobile.useHook();
|
||||||
const toastedUsersRef = useRef([]);
|
const toastedUsersRef = useRef([]);
|
||||||
const { user: currentUser } = useUser();
|
const { user: currentUser } = useUser();
|
||||||
const [visible, toggleVisible] = useToggle(false);
|
const [visible, toggleVisible] = useToggle(false);
|
||||||
const [collaborationUsers, setCollaborationUsers] = useState([]);
|
const [collaborationUsers, setCollaborationUsers] = useState([]);
|
||||||
|
|
||||||
const content = useMemo(
|
const content = useMemo(
|
||||||
() => (
|
() =>
|
||||||
|
!visible ? null : (
|
||||||
<div style={{ padding: '24px 0' }}>
|
<div style={{ padding: '24px 0' }}>
|
||||||
<Members
|
<Members
|
||||||
id={documentId}
|
id={documentId}
|
||||||
|
@ -35,8 +47,9 @@ export const DocumentCollaboration: React.FC<IProps> = ({ wikiId, documentId, di
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
[documentId]
|
[visible, documentId]
|
||||||
);
|
);
|
||||||
|
|
||||||
const btn = useMemo(
|
const btn = useMemo(
|
||||||
() => (
|
() => (
|
||||||
<Button theme="borderless" type="tertiary" disabled={disabled} icon={<IconUserAdd />} onClick={toggleVisible} />
|
<Button theme="borderless" type="tertiary" disabled={disabled} icon={<IconUserAdd />} onClick={toggleVisible} />
|
||||||
|
@ -113,6 +126,7 @@ export const DocumentCollaboration: React.FC<IProps> = ({ wikiId, documentId, di
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</AvatarGroup>
|
</AvatarGroup>
|
||||||
|
|
||||||
{isMobile ? (
|
{isMobile ? (
|
||||||
<>
|
<>
|
||||||
<Modal
|
<Modal
|
||||||
|
@ -121,7 +135,7 @@ export const DocumentCollaboration: React.FC<IProps> = ({ wikiId, documentId, di
|
||||||
visible={visible}
|
visible={visible}
|
||||||
footer={null}
|
footer={null}
|
||||||
onCancel={toggleVisible}
|
onCancel={toggleVisible}
|
||||||
style={{ maxWidth: '96vw', maxHeight: '60vh', overflow: 'auto' }}
|
style={mobileContainerStyle}
|
||||||
>
|
>
|
||||||
{content}
|
{content}
|
||||||
</Modal>
|
</Modal>
|
||||||
|
@ -134,19 +148,7 @@ export const DocumentCollaboration: React.FC<IProps> = ({ wikiId, documentId, di
|
||||||
onVisibleChange={toggleVisible}
|
onVisibleChange={toggleVisible}
|
||||||
trigger="click"
|
trigger="click"
|
||||||
position="bottomRight"
|
position="bottomRight"
|
||||||
content={
|
content={<div style={pcContainerStyle}>{content}</div>}
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
width: 412,
|
|
||||||
maxWidth: '96vw',
|
|
||||||
padding: '0 24px',
|
|
||||||
maxHeight: '60vh',
|
|
||||||
overflow: 'auto',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{content}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
{btn}
|
{btn}
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
|
|
@ -23,6 +23,7 @@ export const Editor: React.FC<IProps> = ({ user: currentUser, documentId, author
|
||||||
if (!editor) return;
|
if (!editor) return;
|
||||||
editor.commands.setContent(data);
|
editor.commands.setContent(data);
|
||||||
};
|
};
|
||||||
|
|
||||||
event.on(USE_DOCUMENT_VERSION, handler);
|
event.on(USE_DOCUMENT_VERSION, handler);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
|
|
@ -28,6 +28,14 @@ interface IProps {
|
||||||
documentId: string;
|
documentId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ErrorContent = () => {
|
||||||
|
return (
|
||||||
|
<div style={{ margin: '10vh', textAlign: 'center' }}>
|
||||||
|
<SecureDocumentIllustration />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const DocumentEditor: React.FC<IProps> = ({ documentId }) => {
|
export const DocumentEditor: React.FC<IProps> = ({ documentId }) => {
|
||||||
const { isMobile } = IsOnMobile.useHook();
|
const { isMobile } = IsOnMobile.useHook();
|
||||||
const { width: windowWith } = useWindowSize();
|
const { width: windowWith } = useWindowSize();
|
||||||
|
@ -84,9 +92,7 @@ export const DocumentEditor: React.FC<IProps> = ({ documentId }) => {
|
||||||
mode="horizontal"
|
mode="horizontal"
|
||||||
header={
|
header={
|
||||||
<>
|
<>
|
||||||
<Tooltip content="返回" position="bottom">
|
|
||||||
<Button onMouseDown={goback} icon={<IconChevronLeft />} style={{ marginRight: 16 }} />
|
<Button onMouseDown={goback} icon={<IconChevronLeft />} style={{ marginRight: 16 }} />
|
||||||
</Tooltip>
|
|
||||||
<DataRender
|
<DataRender
|
||||||
loading={docAuthLoading}
|
loading={docAuthLoading}
|
||||||
error={docAuthError}
|
error={docAuthError}
|
||||||
|
@ -125,13 +131,7 @@ export const DocumentEditor: React.FC<IProps> = ({ documentId }) => {
|
||||||
<DataRender
|
<DataRender
|
||||||
loading={docAuthLoading}
|
loading={docAuthLoading}
|
||||||
error={docAuthError}
|
error={docAuthError}
|
||||||
errorContent={() => {
|
errorContent={<ErrorContent />}
|
||||||
return (
|
|
||||||
<div style={{ margin: '10vh', textAlign: 'center' }}>
|
|
||||||
<SecureDocumentIllustration />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
normalContent={() => {
|
normalContent={() => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -64,7 +64,8 @@ export const EmojiPicker: React.FC<IProps> = ({ showClear = false, onSelectEmoji
|
||||||
}, [onSelectEmoji]);
|
}, [onSelectEmoji]);
|
||||||
|
|
||||||
const content = useMemo(
|
const content = useMemo(
|
||||||
() => (
|
() =>
|
||||||
|
!visible ? null : (
|
||||||
<div className={styles.wrap}>
|
<div className={styles.wrap}>
|
||||||
<Tabs
|
<Tabs
|
||||||
size="small"
|
size="small"
|
||||||
|
@ -81,7 +82,12 @@ export const EmojiPicker: React.FC<IProps> = ({ showClear = false, onSelectEmoji
|
||||||
>
|
>
|
||||||
{renderedList.map((list) => {
|
{renderedList.map((list) => {
|
||||||
return (
|
return (
|
||||||
<TabPane key={list.title} tab={list.title} itemKey={list.title} style={{ height: 250, overflow: 'auto' }}>
|
<TabPane
|
||||||
|
key={list.title}
|
||||||
|
tab={list.title}
|
||||||
|
itemKey={list.title}
|
||||||
|
style={{ height: 250, overflow: 'auto' }}
|
||||||
|
>
|
||||||
<div className={styles.listWrap}>
|
<div className={styles.listWrap}>
|
||||||
{(list.data || []).map((ex) => (
|
{(list.data || []).map((ex) => (
|
||||||
<div key={ex} onClick={() => selectEmoji(ex)}>
|
<div key={ex} onClick={() => selectEmoji(ex)}>
|
||||||
|
@ -95,7 +101,7 @@ export const EmojiPicker: React.FC<IProps> = ({ showClear = false, onSelectEmoji
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
[showClear, renderedList, selectEmoji, clear]
|
[visible, showClear, renderedList, selectEmoji, clear]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
@ -8,7 +8,7 @@ import { IsOnMobile } from 'hooks/use-on-mobile';
|
||||||
import { useToggle } from 'hooks/use-toggle';
|
import { useToggle } from 'hooks/use-toggle';
|
||||||
import { EmptyBoxIllustration } from 'illustrations/empty-box';
|
import { EmptyBoxIllustration } from 'illustrations/empty-box';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import React, { useCallback } from 'react';
|
import React, { useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
import styles from './index.module.scss';
|
import styles from './index.module.scss';
|
||||||
import { Placeholder } from './placeholder';
|
import { Placeholder } from './placeholder';
|
||||||
|
@ -17,19 +17,19 @@ const { Text } = Typography;
|
||||||
const PAGE_SIZE = 6;
|
const PAGE_SIZE = 6;
|
||||||
|
|
||||||
const MessagesRender = ({ messageData, loading, error, onClick = null, page = 1, onPageChange = null }) => {
|
const MessagesRender = ({ messageData, loading, error, onClick = null, page = 1, onPageChange = null }) => {
|
||||||
const total = (messageData && messageData.total) || 0;
|
const [messages, total] = useMemo(
|
||||||
const messages = (messageData && messageData.data) || [];
|
() => [(messageData && messageData.data) || [], (messageData && messageData.total) || 0],
|
||||||
|
[messageData]
|
||||||
|
);
|
||||||
|
|
||||||
const handleRead = (messageId) => {
|
const handleRead = useCallback(
|
||||||
|
(messageId) => {
|
||||||
onClick && onClick(messageId);
|
onClick && onClick(messageId);
|
||||||
};
|
},
|
||||||
|
[onClick]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
const renderNormalContent = useCallback(() => {
|
||||||
<DataRender
|
|
||||||
loading={loading}
|
|
||||||
loadingContent={<Placeholder />}
|
|
||||||
error={error}
|
|
||||||
normalContent={() => {
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={styles.itemsWrap}
|
className={styles.itemsWrap}
|
||||||
|
@ -81,8 +81,10 @@ const MessagesRender = ({ messageData, loading, error, onClick = null, page = 1,
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}}
|
}, [handleRead, messages, onPageChange, page, total]);
|
||||||
/>
|
|
||||||
|
return (
|
||||||
|
<DataRender loading={loading} loadingContent={<Placeholder />} error={error} normalContent={renderNormalContent} />
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -106,20 +108,22 @@ const MessageBox = () => {
|
||||||
setPage: unreadSetPage,
|
setPage: unreadSetPage,
|
||||||
} = useUnreadMessages();
|
} = useUnreadMessages();
|
||||||
|
|
||||||
const clearAll = () => {
|
const clearAll = useCallback(() => {
|
||||||
Promise.all(
|
Promise.all(
|
||||||
(unreadMsgs.data || []).map((msg) => {
|
(unreadMsgs.data || []).map((msg) => {
|
||||||
return readMessage(msg.id);
|
return readMessage(msg.id);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
};
|
}, [readMessage, unreadMsgs]);
|
||||||
|
|
||||||
const openModalOnMobile = useCallback(() => {
|
const openModalOnMobile = useCallback(() => {
|
||||||
if (!isMobile) return;
|
if (!isMobile) return;
|
||||||
toggleVisible(true);
|
toggleVisible(true);
|
||||||
}, [isMobile, toggleVisible]);
|
}, [isMobile, toggleVisible]);
|
||||||
|
|
||||||
const content = (
|
const content = useMemo(
|
||||||
|
() =>
|
||||||
|
visible ? (
|
||||||
<Tabs
|
<Tabs
|
||||||
type="line"
|
type="line"
|
||||||
size="small"
|
size="small"
|
||||||
|
@ -160,6 +164,27 @@ const MessageBox = () => {
|
||||||
/>
|
/>
|
||||||
</TabPane>
|
</TabPane>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
) : null,
|
||||||
|
[
|
||||||
|
allError,
|
||||||
|
allLoading,
|
||||||
|
allMsgs,
|
||||||
|
allPage,
|
||||||
|
allSetPage,
|
||||||
|
clearAll,
|
||||||
|
readError,
|
||||||
|
readLoading,
|
||||||
|
readMessage,
|
||||||
|
readMsgs,
|
||||||
|
readPage,
|
||||||
|
readSetPage,
|
||||||
|
unreadError,
|
||||||
|
unreadLoading,
|
||||||
|
unreadMsgs,
|
||||||
|
unreadPage,
|
||||||
|
unreadSetPage,
|
||||||
|
visible,
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
const btn = (
|
const btn = (
|
||||||
|
@ -197,6 +222,8 @@ const MessageBox = () => {
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<Dropdown
|
<Dropdown
|
||||||
|
visible={visible}
|
||||||
|
onVisibleChange={toggleVisible}
|
||||||
position="bottomRight"
|
position="bottomRight"
|
||||||
trigger="click"
|
trigger="click"
|
||||||
content={<div style={{ width: 300, padding: '16px 16px 0' }}>{content}</div>}
|
content={<div style={{ width: 300, padding: '16px 16px 0' }}>{content}</div>}
|
||||||
|
@ -210,5 +237,8 @@ const MessageBox = () => {
|
||||||
|
|
||||||
export const Message = () => {
|
export const Message = () => {
|
||||||
const { loading, error } = useUser();
|
const { loading, error } = useUser();
|
||||||
return <DataRender loading={loading} error={error} normalContent={() => <MessageBox />} />;
|
|
||||||
|
const renderNormalContent = useCallback(() => <MessageBox />, []);
|
||||||
|
|
||||||
|
return <DataRender loading={loading} error={error} normalContent={renderNormalContent} />;
|
||||||
};
|
};
|
||||||
|
|
|
@ -11,6 +11,9 @@ interface IProps {
|
||||||
onOk: (arg: ISize) => void;
|
onOk: (arg: ISize) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const containerStyle = { padding: '0 12px 12px' };
|
||||||
|
const inlineBlockStyle = { display: 'inline-block' };
|
||||||
|
|
||||||
export const SizeSetter: React.FC<IProps> = ({ width, maxWidth, height, onOk, children }) => {
|
export const SizeSetter: React.FC<IProps> = ({ width, maxWidth, height, onOk, children }) => {
|
||||||
const $form = useRef<FormApi>();
|
const $form = useRef<FormApi>();
|
||||||
|
|
||||||
|
@ -27,7 +30,7 @@ export const SizeSetter: React.FC<IProps> = ({ width, maxWidth, height, onOk, ch
|
||||||
position={'bottomLeft'}
|
position={'bottomLeft'}
|
||||||
spacing={10}
|
spacing={10}
|
||||||
render={
|
render={
|
||||||
<div style={{ padding: '0 12px 12px' }}>
|
<div style={containerStyle}>
|
||||||
<Form initValues={{ width, height }} getFormApi={(formApi) => ($form.current = formApi)} labelPosition="left">
|
<Form initValues={{ width, height }} getFormApi={(formApi) => ($form.current = formApi)} labelPosition="left">
|
||||||
<Form.Input autofocus label="宽" field="width" {...(maxWidth ? { max: maxWidth } : {})} />
|
<Form.Input autofocus label="宽" field="width" {...(maxWidth ? { max: maxWidth } : {})} />
|
||||||
<Form.Input label="高" field="height" />
|
<Form.Input label="高" field="height" />
|
||||||
|
@ -38,7 +41,7 @@ export const SizeSetter: React.FC<IProps> = ({ width, maxWidth, height, onOk, ch
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<span style={{ display: 'inline-block' }}>{children}</span>
|
<span style={inlineBlockStyle}>{children}</span>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -136,6 +136,7 @@ export const _Tree = ({ data, docAsLink, getDocLink, isShareMode = false, needAd
|
||||||
defaultExpandedKeys={expandedKeys}
|
defaultExpandedKeys={expandedKeys}
|
||||||
expandedKeys={expandedKeys}
|
expandedKeys={expandedKeys}
|
||||||
onExpand={setExpandedKeys}
|
onExpand={setExpandedKeys}
|
||||||
|
motion={false}
|
||||||
/>
|
/>
|
||||||
{needAddDocument && <AddDocument />}
|
{needAddDocument && <AddDocument />}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -16,6 +16,8 @@ interface IProps {
|
||||||
rightNode: React.ReactNode;
|
rightNode: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const style = { width: '100%', height: '100%' };
|
||||||
|
|
||||||
export const AppDoubleColumnLayout: React.FC<IProps> = ({ leftNode, rightNode }) => {
|
export const AppDoubleColumnLayout: React.FC<IProps> = ({ leftNode, rightNode }) => {
|
||||||
const { minWidth, maxWidth, width, isCollapsed, updateWidth, toggleCollapsed } = useDragableWidth();
|
const { minWidth, maxWidth, width, isCollapsed, updateWidth, toggleCollapsed } = useDragableWidth();
|
||||||
const debounceUpdate = useMemo(() => throttle(updateWidth, 200), [updateWidth]);
|
const debounceUpdate = useMemo(() => throttle(updateWidth, 200), [updateWidth]);
|
||||||
|
@ -25,7 +27,7 @@ export const AppDoubleColumnLayout: React.FC<IProps> = ({ leftNode, rightNode })
|
||||||
<AppRouterHeader />
|
<AppRouterHeader />
|
||||||
<SemiLayout className={styles.contentWrap}>
|
<SemiLayout className={styles.contentWrap}>
|
||||||
<SplitPane minSize={minWidth} maxSize={maxWidth} size={width} onChange={debounceUpdate}>
|
<SplitPane minSize={minWidth} maxSize={maxWidth} size={width} onChange={debounceUpdate}>
|
||||||
<Sider style={{ width: '100%', height: '100%' }} className={styles.leftWrap}>
|
<Sider style={style} className={styles.leftWrap}>
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
icon={isCollapsed ? <IconChevronRight /> : <IconChevronLeft />}
|
icon={isCollapsed ? <IconChevronRight /> : <IconChevronLeft />}
|
||||||
|
|
|
@ -9,7 +9,7 @@ import { useRecentDocuments } from 'data/document';
|
||||||
import { useToggle } from 'hooks/use-toggle';
|
import { useToggle } from 'hooks/use-toggle';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import React, { useEffect } from 'react';
|
import React, { useCallback, useEffect } from 'react';
|
||||||
|
|
||||||
import styles from './index.module.scss';
|
import styles from './index.module.scss';
|
||||||
import { Placeholder } from './placeholder';
|
import { Placeholder } from './placeholder';
|
||||||
|
@ -20,20 +20,7 @@ export const RecentDocs = ({ visible }) => {
|
||||||
const { query } = useRouter();
|
const { query } = useRouter();
|
||||||
const { data: recentDocs, loading, error, refresh } = useRecentDocuments(query.organizationId);
|
const { data: recentDocs, loading, error, refresh } = useRecentDocuments(query.organizationId);
|
||||||
|
|
||||||
useEffect(() => {
|
const renderNormalContent = useCallback(() => {
|
||||||
if (visible) {
|
|
||||||
refresh();
|
|
||||||
}
|
|
||||||
}, [visible, refresh]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Tabs type="line" size="small">
|
|
||||||
<TabPane tab="文档" itemKey="docs">
|
|
||||||
<DataRender
|
|
||||||
loading={loading}
|
|
||||||
loadingContent={<Placeholder />}
|
|
||||||
error={error}
|
|
||||||
normalContent={() => {
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.itemsWrap} style={{ margin: '0 -16px' }}>
|
<div className={styles.itemsWrap} style={{ margin: '0 -16px' }}>
|
||||||
{recentDocs && recentDocs.length ? (
|
{recentDocs && recentDocs.length ? (
|
||||||
|
@ -65,11 +52,7 @@ export const RecentDocs = ({ visible }) => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.rightWrap}>
|
<div className={styles.rightWrap}>
|
||||||
<DocumentStar
|
<DocumentStar organizationId={doc.organizationId} wikiId={doc.wikiId} documentId={doc.id} />
|
||||||
organizationId={doc.organizationId}
|
|
||||||
wikiId={doc.wikiId}
|
|
||||||
documentId={doc.id}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
|
@ -81,7 +64,22 @@ export const RecentDocs = ({ visible }) => {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}}
|
}, [recentDocs]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (visible) {
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
}, [visible, refresh]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tabs type="line" size="small">
|
||||||
|
<TabPane tab="文档" itemKey="docs">
|
||||||
|
<DataRender
|
||||||
|
loading={loading}
|
||||||
|
loadingContent={<Placeholder />}
|
||||||
|
error={error}
|
||||||
|
normalContent={renderNormalContent}
|
||||||
/>
|
/>
|
||||||
</TabPane>
|
</TabPane>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
@ -109,6 +107,8 @@ export const RecentMobileTrigger = ({ toggleVisible }) => {
|
||||||
return <span onClick={toggleVisible}>最近</span>;
|
return <span onClick={toggleVisible}>最近</span>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const dropdownContainerStyle = { width: 300, padding: '16px 16px 0' };
|
||||||
|
|
||||||
export const Recent = () => {
|
export const Recent = () => {
|
||||||
const [visible, toggleVisible] = useToggle(false);
|
const [visible, toggleVisible] = useToggle(false);
|
||||||
|
|
||||||
|
@ -120,7 +120,7 @@ export const Recent = () => {
|
||||||
visible={visible}
|
visible={visible}
|
||||||
onVisibleChange={toggleVisible}
|
onVisibleChange={toggleVisible}
|
||||||
content={
|
content={
|
||||||
<div style={{ width: 300, padding: '16px 16px 0' }}>
|
<div style={dropdownContainerStyle}>
|
||||||
<RecentDocs visible={visible} />
|
<RecentDocs visible={visible} />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { useStarWikisInOrganization } from 'data/star';
|
||||||
import { useWikiDetail } from 'data/wiki';
|
import { useWikiDetail } from 'data/wiki';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import React from 'react';
|
import React, { useCallback } from 'react';
|
||||||
|
|
||||||
import styles from './index.module.scss';
|
import styles from './index.module.scss';
|
||||||
import { Placeholder } from './placeholder';
|
import { Placeholder } from './placeholder';
|
||||||
|
@ -24,6 +24,58 @@ const WikiContent = () => {
|
||||||
} = useStarWikisInOrganization(query.organizationId);
|
} = useStarWikisInOrganization(query.organizationId);
|
||||||
const { data: currentWiki } = useWikiDetail(query.wikiId);
|
const { data: currentWiki } = useWikiDetail(query.wikiId);
|
||||||
|
|
||||||
|
const renderNormalContent = useCallback(() => {
|
||||||
|
return (
|
||||||
|
<div className={styles.itemsWrap}>
|
||||||
|
{starWikis && starWikis.length ? (
|
||||||
|
starWikis.map((wiki) => {
|
||||||
|
return (
|
||||||
|
<div className={styles.itemWrap} key={wiki.id}>
|
||||||
|
<Link
|
||||||
|
href={{
|
||||||
|
pathname: '/app/org/[organizationId]/wiki/[wikiId]',
|
||||||
|
query: {
|
||||||
|
organizationId: wiki.organizationId,
|
||||||
|
wikiId: wiki.id,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<a className={styles.item}>
|
||||||
|
<div className={styles.leftWrap}>
|
||||||
|
<Avatar
|
||||||
|
shape="square"
|
||||||
|
size="small"
|
||||||
|
src={wiki.avatar}
|
||||||
|
style={{
|
||||||
|
marginRight: 8,
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
borderRadius: 4,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{wiki.name.charAt(0)}
|
||||||
|
</Avatar>
|
||||||
|
<div>
|
||||||
|
<Text ellipsis={{ showTooltip: true }} style={{ width: 180 }}>
|
||||||
|
{wiki.name}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.rightWrap}>
|
||||||
|
<WikiStar organizationId={wiki.organizationId} wikiId={wiki.id} onChange={refreshStarWikis} />
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<Empty message="收藏的知识库会出现在此处" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}, [refreshStarWikis, starWikis]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{currentWiki && (
|
{currentWiki && (
|
||||||
|
@ -85,61 +137,7 @@ const WikiContent = () => {
|
||||||
loading={loading}
|
loading={loading}
|
||||||
loadingContent={<Placeholder />}
|
loadingContent={<Placeholder />}
|
||||||
error={error}
|
error={error}
|
||||||
normalContent={() => {
|
normalContent={renderNormalContent}
|
||||||
return (
|
|
||||||
<div className={styles.itemsWrap}>
|
|
||||||
{starWikis && starWikis.length ? (
|
|
||||||
starWikis.map((wiki) => {
|
|
||||||
return (
|
|
||||||
<div className={styles.itemWrap} key={wiki.id}>
|
|
||||||
<Link
|
|
||||||
href={{
|
|
||||||
pathname: '/app/org/[organizationId]/wiki/[wikiId]',
|
|
||||||
query: {
|
|
||||||
organizationId: wiki.organizationId,
|
|
||||||
wikiId: wiki.id,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<a className={styles.item}>
|
|
||||||
<div className={styles.leftWrap}>
|
|
||||||
<Avatar
|
|
||||||
shape="square"
|
|
||||||
size="small"
|
|
||||||
src={wiki.avatar}
|
|
||||||
style={{
|
|
||||||
marginRight: 8,
|
|
||||||
width: 24,
|
|
||||||
height: 24,
|
|
||||||
borderRadius: 4,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{wiki.name.charAt(0)}
|
|
||||||
</Avatar>
|
|
||||||
<div>
|
|
||||||
<Text ellipsis={{ showTooltip: true }} style={{ width: 180 }}>
|
|
||||||
{wiki.name}
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className={styles.rightWrap}>
|
|
||||||
<WikiStar
|
|
||||||
organizationId={wiki.organizationId}
|
|
||||||
wikiId={wiki.id}
|
|
||||||
onChange={refreshStarWikis}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
) : (
|
|
||||||
<Empty message="收藏的知识库会出现在此处" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<Dropdown.Divider />
|
<Dropdown.Divider />
|
||||||
<div className={styles.itemWrap}>
|
<div className={styles.itemWrap}>
|
||||||
|
|
|
@ -6,17 +6,17 @@ import styles from './index.module.scss';
|
||||||
|
|
||||||
const { Content } = SemiLayout;
|
const { Content } = SemiLayout;
|
||||||
|
|
||||||
|
const style = {
|
||||||
|
padding: '16px 24px',
|
||||||
|
backgroundColor: 'var(--semi-color-bg-0)',
|
||||||
|
};
|
||||||
|
|
||||||
export const AppSingleColumnLayout: React.FC = ({ children }) => {
|
export const AppSingleColumnLayout: React.FC = ({ children }) => {
|
||||||
return (
|
return (
|
||||||
<SemiLayout className={styles.wrap}>
|
<SemiLayout className={styles.wrap}>
|
||||||
<AppRouterHeader />
|
<AppRouterHeader />
|
||||||
<SemiLayout className={styles.contentWrap}>
|
<SemiLayout className={styles.contentWrap}>
|
||||||
<Content
|
<Content style={style}>
|
||||||
style={{
|
|
||||||
padding: '16px 24px',
|
|
||||||
backgroundColor: 'var(--semi-color-bg-0)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div>{children}</div>
|
<div>{children}</div>
|
||||||
</Content>
|
</Content>
|
||||||
</SemiLayout>
|
</SemiLayout>
|
||||||
|
|
|
@ -16,6 +16,8 @@ interface IProps {
|
||||||
rightNode: React.ReactNode;
|
rightNode: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const style = { width: '100%', height: '100%' };
|
||||||
|
|
||||||
export const DoubleColumnLayout: React.FC<IProps> = ({ leftNode, rightNode }) => {
|
export const DoubleColumnLayout: React.FC<IProps> = ({ leftNode, rightNode }) => {
|
||||||
const { minWidth, maxWidth, width, isCollapsed, updateWidth, toggleCollapsed } = useDragableWidth();
|
const { minWidth, maxWidth, width, isCollapsed, updateWidth, toggleCollapsed } = useDragableWidth();
|
||||||
const debounceUpdate = useMemo(() => throttle(updateWidth, 200), [updateWidth]);
|
const debounceUpdate = useMemo(() => throttle(updateWidth, 200), [updateWidth]);
|
||||||
|
@ -25,7 +27,7 @@ export const DoubleColumnLayout: React.FC<IProps> = ({ leftNode, rightNode }) =>
|
||||||
<RouterHeader />
|
<RouterHeader />
|
||||||
<SemiLayout className={styles.contentWrap}>
|
<SemiLayout className={styles.contentWrap}>
|
||||||
<SplitPane minSize={minWidth} maxSize={maxWidth} size={width} onChange={debounceUpdate}>
|
<SplitPane minSize={minWidth} maxSize={maxWidth} size={width} onChange={debounceUpdate}>
|
||||||
<Sider style={{ width: '100%', height: '100%' }} className={styles.leftWrap}>
|
<Sider style={style} className={styles.leftWrap}>
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
icon={isCollapsed ? <IconChevronRight /> : <IconChevronLeft />}
|
icon={isCollapsed ? <IconChevronRight /> : <IconChevronLeft />}
|
||||||
|
|
|
@ -15,6 +15,8 @@ interface IProps {
|
||||||
rightNode: React.ReactNode;
|
rightNode: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const style = { width: '100%', height: '100%' };
|
||||||
|
|
||||||
export const PublicDoubleColumnLayout: React.FC<IProps> = ({ leftNode, rightNode }) => {
|
export const PublicDoubleColumnLayout: React.FC<IProps> = ({ leftNode, rightNode }) => {
|
||||||
const { minWidth, maxWidth, width, isCollapsed, updateWidth, toggleCollapsed } = useDragableWidth();
|
const { minWidth, maxWidth, width, isCollapsed, updateWidth, toggleCollapsed } = useDragableWidth();
|
||||||
const debounceUpdate = useMemo(() => throttle(updateWidth, 200), [updateWidth]);
|
const debounceUpdate = useMemo(() => throttle(updateWidth, 200), [updateWidth]);
|
||||||
|
@ -22,7 +24,7 @@ export const PublicDoubleColumnLayout: React.FC<IProps> = ({ leftNode, rightNode
|
||||||
return (
|
return (
|
||||||
<SemiLayout className={styles.wrap}>
|
<SemiLayout className={styles.wrap}>
|
||||||
<SplitPane minSize={minWidth} maxSize={maxWidth} size={width} onChange={debounceUpdate}>
|
<SplitPane minSize={minWidth} maxSize={maxWidth} size={width} onChange={debounceUpdate}>
|
||||||
<Sider style={{ width: '100%', height: '100%' }} className={styles.leftWrap}>
|
<Sider style={style} className={styles.leftWrap}>
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
icon={isCollapsed ? <IconChevronRight /> : <IconChevronLeft />}
|
icon={isCollapsed ? <IconChevronRight /> : <IconChevronLeft />}
|
||||||
|
|
|
@ -6,17 +6,17 @@ import styles from './index.module.scss';
|
||||||
|
|
||||||
const { Content } = SemiLayout;
|
const { Content } = SemiLayout;
|
||||||
|
|
||||||
|
const style = {
|
||||||
|
padding: '16px 24px',
|
||||||
|
backgroundColor: 'var(--semi-color-bg-0)',
|
||||||
|
};
|
||||||
|
|
||||||
export const SingleColumnLayout: React.FC = ({ children }) => {
|
export const SingleColumnLayout: React.FC = ({ children }) => {
|
||||||
return (
|
return (
|
||||||
<SemiLayout className={styles.wrap}>
|
<SemiLayout className={styles.wrap}>
|
||||||
<RouterHeader />
|
<RouterHeader />
|
||||||
<SemiLayout className={styles.contentWrap}>
|
<SemiLayout className={styles.contentWrap}>
|
||||||
<Content
|
<Content style={style}>
|
||||||
style={{
|
|
||||||
padding: '16px 24px',
|
|
||||||
backgroundColor: 'var(--semi-color-bg-0)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div>{children}</div>
|
<div>{children}</div>
|
||||||
</Content>
|
</Content>
|
||||||
</SemiLayout>
|
</SemiLayout>
|
||||||
|
|
|
@ -132,7 +132,10 @@ export class BubbleMenuView {
|
||||||
trigger: 'manual',
|
trigger: 'manual',
|
||||||
placement: 'top',
|
placement: 'top',
|
||||||
hideOnClick: 'toggle',
|
hideOnClick: 'toggle',
|
||||||
...Object.assign({ zIndex: 999 }, this.tippyOptions),
|
...Object.assign(
|
||||||
|
{ zIndex: 999, duration: 200, animation: 'shift-toward-subtle', moveTransition: 'transform 0.2s ease-in-out' },
|
||||||
|
this.tippyOptions
|
||||||
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
// maybe we have to hide tippy on its own blur event as well
|
// maybe we have to hide tippy on its own blur event as well
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useEffect, useRef } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { BubbleMenuPlugin, BubbleMenuPluginProps } from './bubble-menu-plugin';
|
import { BubbleMenuPlugin, BubbleMenuPluginProps } from './bubble-menu-plugin';
|
||||||
|
|
||||||
|
@ -9,11 +9,9 @@ export type BubbleMenuProps = Omit<Optional<BubbleMenuPluginProps, 'pluginKey'>,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const BubbleMenu: React.FC<BubbleMenuProps> = (props) => {
|
export const BubbleMenu: React.FC<BubbleMenuProps> = (props) => {
|
||||||
const $element = useRef<HTMLDivElement | null>(null);
|
const [element, setElement] = useState<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const element = $element.current;
|
|
||||||
|
|
||||||
if (!element) {
|
if (!element) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -46,10 +44,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]);
|
}, [props.editor, element]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={$element} className={props.className} style={{ visibility: 'hidden' }}>
|
<div ref={setElement} className={props.className} style={{ visibility: 'hidden' }}>
|
||||||
{props.children}
|
{props.children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -63,7 +63,7 @@ export const COMMANDS: ICommand[] = [
|
||||||
label: '表格',
|
label: '表格',
|
||||||
custom: (editor, runCommand) => (
|
custom: (editor, runCommand) => (
|
||||||
<Popover
|
<Popover
|
||||||
key="table"
|
key="custom-table"
|
||||||
showArrow
|
showArrow
|
||||||
position="rightTop"
|
position="rightTop"
|
||||||
zIndex={10000}
|
zIndex={10000}
|
||||||
|
@ -93,7 +93,7 @@ export const COMMANDS: ICommand[] = [
|
||||||
label: '布局',
|
label: '布局',
|
||||||
custom: (editor, runCommand) => (
|
custom: (editor, runCommand) => (
|
||||||
<Popover
|
<Popover
|
||||||
key="table"
|
key="custom-columns"
|
||||||
showArrow
|
showArrow
|
||||||
position="rightTop"
|
position="rightTop"
|
||||||
zIndex={10000}
|
zIndex={10000}
|
||||||
|
|
|
@ -47,35 +47,8 @@ export const DocumentReferenceBubbleMenu = ({ editor }) => {
|
||||||
const copyMe = useCallback(() => copyNode(DocumentReference.name, editor), [editor]);
|
const copyMe = useCallback(() => copyNode(DocumentReference.name, editor), [editor]);
|
||||||
const deleteMe = useCallback(() => deleteNode(DocumentReference.name, editor), [editor]);
|
const deleteMe = useCallback(() => deleteNode(DocumentReference.name, editor), [editor]);
|
||||||
|
|
||||||
useEffect(() => {
|
const renderNormalContent = useCallback(
|
||||||
if (defaultShowPicker && user && createUser === user.id) {
|
() => (
|
||||||
toggleVisible(true);
|
|
||||||
editor.chain().updateAttributes(DocumentReference.name, { defaultShowPicker: false }).focus().run();
|
|
||||||
}
|
|
||||||
}, [editor, defaultShowPicker, toggleVisible, createUser, user]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<BubbleMenu
|
|
||||||
className={'bubble-menu'}
|
|
||||||
editor={editor}
|
|
||||||
pluginKey="document-reference-bubble-menu"
|
|
||||||
shouldShow={shouldShow}
|
|
||||||
tippyOptions={{ maxWidth: 'calc(100vw - 100px)' }}
|
|
||||||
>
|
|
||||||
<Space spacing={4}>
|
|
||||||
<Tooltip content="复制">
|
|
||||||
<Button onClick={copyMe} icon={<IconCopy />} type="tertiary" theme="borderless" size="small" />
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<Popover
|
|
||||||
spacing={15}
|
|
||||||
visible={visible}
|
|
||||||
onVisibleChange={toggleVisible}
|
|
||||||
content={
|
|
||||||
<DataRender
|
|
||||||
loading={loading}
|
|
||||||
error={error}
|
|
||||||
normalContent={() => (
|
|
||||||
<List
|
<List
|
||||||
size="small"
|
size="small"
|
||||||
style={{ maxHeight: 320, overflow: 'auto' }}
|
style={{ maxHeight: 320, overflow: 'auto' }}
|
||||||
|
@ -100,9 +73,35 @@ export const DocumentReferenceBubbleMenu = ({ editor }) => {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
),
|
||||||
/>
|
[selectDoc, tocs]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (defaultShowPicker && user && createUser === user.id) {
|
||||||
|
toggleVisible(true);
|
||||||
|
editor.chain().updateAttributes(DocumentReference.name, { defaultShowPicker: false }).focus().run();
|
||||||
}
|
}
|
||||||
|
}, [editor, defaultShowPicker, toggleVisible, createUser, user]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BubbleMenu
|
||||||
|
className={'bubble-menu'}
|
||||||
|
editor={editor}
|
||||||
|
pluginKey="document-reference-bubble-menu"
|
||||||
|
shouldShow={shouldShow}
|
||||||
|
tippyOptions={{ maxWidth: 'calc(100vw - 100px)' }}
|
||||||
|
>
|
||||||
|
<Space spacing={4}>
|
||||||
|
<Tooltip content="复制">
|
||||||
|
<Button onClick={copyMe} icon={<IconCopy />} type="tertiary" theme="borderless" size="small" />
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Popover
|
||||||
|
spacing={15}
|
||||||
|
visible={visible}
|
||||||
|
onVisibleChange={toggleVisible}
|
||||||
|
content={<DataRender loading={loading} error={error} normalContent={renderNormalContent} />}
|
||||||
trigger="click"
|
trigger="click"
|
||||||
position="bottomLeft"
|
position="bottomLeft"
|
||||||
showArrow
|
showArrow
|
||||||
|
|
|
@ -4,6 +4,14 @@ import { Editor } from 'tiptap/core';
|
||||||
import { Title } from 'tiptap/core/extensions/title';
|
import { Title } from 'tiptap/core/extensions/title';
|
||||||
import { useActive } from 'tiptap/core/hooks/use-active';
|
import { useActive } from 'tiptap/core/hooks/use-active';
|
||||||
|
|
||||||
|
const containerStyle = { width: 90, marginRight: 10 };
|
||||||
|
const h1Style = { margin: 0, fontSize: '1.3em' };
|
||||||
|
const h2Style = { margin: 0, fontSize: '1.1em' };
|
||||||
|
const h3Style = { margin: 0, fontSize: '1.0em' };
|
||||||
|
const h4Style = { margin: 0, fontSize: '0.9em' };
|
||||||
|
const h5Style = { margin: 0, fontSize: '0.8em' };
|
||||||
|
const h6Style = { margin: 0, fontSize: '0.8em' };
|
||||||
|
|
||||||
export const Heading: React.FC<{ editor: Editor }> = ({ editor }) => {
|
export const Heading: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
const isTitleActive = useActive(editor, Title.name);
|
const isTitleActive = useActive(editor, Title.name);
|
||||||
const isH1 = useActive(editor, 'heading', { level: 1 });
|
const isH1 = useActive(editor, 'heading', { level: 1 });
|
||||||
|
@ -12,6 +20,7 @@ export const Heading: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
const isH4 = useActive(editor, 'heading', { level: 4 });
|
const isH4 = useActive(editor, 'heading', { level: 4 });
|
||||||
const isH5 = useActive(editor, 'heading', { level: 5 });
|
const isH5 = useActive(editor, 'heading', { level: 5 });
|
||||||
const isH6 = useActive(editor, 'heading', { level: 6 });
|
const isH6 = useActive(editor, 'heading', { level: 6 });
|
||||||
|
|
||||||
const current = useMemo(() => {
|
const current = useMemo(() => {
|
||||||
if (isH1) return 1;
|
if (isH1) return 1;
|
||||||
if (isH2) return 2;
|
if (isH2) return 2;
|
||||||
|
@ -34,25 +43,25 @@ export const Heading: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Select disabled={isTitleActive} value={current} onChange={toggle} style={{ width: 90, marginRight: 10 }}>
|
<Select disabled={isTitleActive} value={current} onChange={toggle} style={containerStyle}>
|
||||||
<Select.Option value="paragraph">正文</Select.Option>
|
<Select.Option value="paragraph">正文</Select.Option>
|
||||||
<Select.Option value={1}>
|
<Select.Option value={1}>
|
||||||
<h1 style={{ margin: 0, fontSize: '1.3em' }}>标题1</h1>
|
<h1 style={h1Style}>标题1</h1>
|
||||||
</Select.Option>
|
</Select.Option>
|
||||||
<Select.Option value={2}>
|
<Select.Option value={2}>
|
||||||
<h2 style={{ margin: 0, fontSize: '1.1em' }}>标题2</h2>
|
<h2 style={h2Style}>标题2</h2>
|
||||||
</Select.Option>
|
</Select.Option>
|
||||||
<Select.Option value={3}>
|
<Select.Option value={3}>
|
||||||
<h3 style={{ margin: 0, fontSize: '1.0em' }}>标题3</h3>
|
<h3 style={h3Style}>标题3</h3>
|
||||||
</Select.Option>
|
</Select.Option>
|
||||||
<Select.Option value={4}>
|
<Select.Option value={4}>
|
||||||
<h4 style={{ margin: 0, fontSize: '0.9em' }}>标题4</h4>
|
<h4 style={h4Style}>标题4</h4>
|
||||||
</Select.Option>
|
</Select.Option>
|
||||||
<Select.Option value={5}>
|
<Select.Option value={5}>
|
||||||
<h5 style={{ margin: 0, fontSize: '0.8em' }}>标题5</h5>
|
<h5 style={h5Style}>标题5</h5>
|
||||||
</Select.Option>
|
</Select.Option>
|
||||||
<Select.Option value={6}>
|
<Select.Option value={6}>
|
||||||
<h6 style={{ margin: 0, fontSize: '0.8em' }}>标题6</h6>
|
<h6 style={h6Style}>标题6</h6>
|
||||||
</Select.Option>
|
</Select.Option>
|
||||||
</Select>
|
</Select>
|
||||||
);
|
);
|
||||||
|
|
|
@ -10,6 +10,29 @@ import { useActive } from 'tiptap/core/hooks/use-active';
|
||||||
|
|
||||||
import { COMMANDS, insertMenuLRUCache, transformToCommands } from '../commands';
|
import { COMMANDS, insertMenuLRUCache, transformToCommands } from '../commands';
|
||||||
|
|
||||||
|
const _CommandRender = ({ commands, editor, runCommand }) => {
|
||||||
|
return (
|
||||||
|
<Dropdown.Menu>
|
||||||
|
{commands.map((command, index) => {
|
||||||
|
return command.title ? (
|
||||||
|
<Dropdown.Title key={'title' + index}>{command.title}</Dropdown.Title>
|
||||||
|
) : command.custom ? (
|
||||||
|
command.custom(editor, runCommand)
|
||||||
|
) : (
|
||||||
|
<Dropdown.Item key={index + '-' + command.label} onClick={runCommand(command)}>
|
||||||
|
{command.icon}
|
||||||
|
{command.label}
|
||||||
|
</Dropdown.Item>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Dropdown.Menu>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const CommandRender = React.memo(_CommandRender, (prevProps, nextProps) => {
|
||||||
|
return prevProps.commands.length === nextProps.commands.length;
|
||||||
|
});
|
||||||
|
|
||||||
export const Insert: React.FC<{ editor: Editor }> = ({ editor }) => {
|
export const Insert: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
const [recentUsed, setRecentUsed] = useState([]);
|
const [recentUsed, setRecentUsed] = useState([]);
|
||||||
|
@ -19,7 +42,7 @@ export const Insert: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
const renderedCommands = useMemo(
|
const renderedCommands = useMemo(
|
||||||
() =>
|
() =>
|
||||||
(recentUsed.length ? [{ title: '最近使用' }, ...recentUsed, ...COMMANDS] : COMMANDS).filter((command) => {
|
(recentUsed.length ? [{ title: '最近使用' }, ...recentUsed, ...COMMANDS] : COMMANDS).filter((command) => {
|
||||||
return command.label === '表格' || command.label === '布局' ? 'custom' in command : true;
|
return command.label === '表格' || command.label === '布局' ? Boolean(command) : true;
|
||||||
}),
|
}),
|
||||||
[recentUsed]
|
[recentUsed]
|
||||||
);
|
);
|
||||||
|
@ -55,22 +78,7 @@ export const Insert: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
maxHeight: 'calc(90vh - 120px)',
|
maxHeight: 'calc(90vh - 120px)',
|
||||||
overflowY: 'auto',
|
overflowY: 'auto',
|
||||||
}}
|
}}
|
||||||
render={
|
render={visible ? <CommandRender commands={renderedCommands} editor={editor} runCommand={runCommand} /> : null}
|
||||||
<Dropdown.Menu>
|
|
||||||
{renderedCommands.map((command, index) => {
|
|
||||||
return command.title ? (
|
|
||||||
<Dropdown.Title key={'title' + index}>{command.title}</Dropdown.Title>
|
|
||||||
) : command.custom ? (
|
|
||||||
command.custom(editor, runCommand)
|
|
||||||
) : (
|
|
||||||
<Dropdown.Item key={index + '-' + command.label} onClick={runCommand(command)}>
|
|
||||||
{command.icon}
|
|
||||||
{command.label}
|
|
||||||
</Dropdown.Item>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Dropdown.Menu>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<Tooltip content="插入">
|
<Tooltip content="插入">
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { Button, Input, Popover, SideSheet, Space, Typography } from '@douyinfe/semi-ui';
|
import { Button, Input, Popover, SideSheet, Space, Typography } from '@douyinfe/semi-ui';
|
||||||
import { IconSearchReplace } from 'components/icons';
|
import { IconSearchReplace } from 'components/icons';
|
||||||
import { Tooltip } from 'components/tooltip';
|
import { Tooltip } from 'components/tooltip';
|
||||||
|
import deepEqual from 'deep-equal';
|
||||||
import { IsOnMobile } from 'hooks/use-on-mobile';
|
import { IsOnMobile } from 'hooks/use-on-mobile';
|
||||||
import { useToggle } from 'hooks/use-toggle';
|
import { useToggle } from 'hooks/use-toggle';
|
||||||
import React, { useCallback, useEffect, useState } from 'react';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
|
@ -9,6 +10,9 @@ import { ON_SEARCH_RESULTS, SearchNReplace } from 'tiptap/core/extensions/search
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
|
|
||||||
|
const headerStyle: React.CSSProperties = { borderBottom: '1px solid var(--semi-color-border)' };
|
||||||
|
const marginBottomStyle: React.CSSProperties = { marginBottom: 12 };
|
||||||
|
|
||||||
export const Search: React.FC<{ editor: Editor }> = ({ editor }) => {
|
export const Search: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
const { isMobile } = IsOnMobile.useHook();
|
const { isMobile } = IsOnMobile.useHook();
|
||||||
const [visible, toggleVisible] = useToggle(false);
|
const [visible, toggleVisible] = useToggle(false);
|
||||||
|
@ -32,16 +36,18 @@ export const Search: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
}, [visible]);
|
}, [visible]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!visible) return;
|
||||||
if (editor && editor.commands && editor.commands.setSearchTerm) {
|
if (editor && editor.commands && editor.commands.setSearchTerm) {
|
||||||
editor.commands.setSearchTerm(searchValue);
|
editor.commands.setSearchTerm(searchValue);
|
||||||
}
|
}
|
||||||
}, [searchValue, editor]);
|
}, [visible, searchValue, editor]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!visible) return;
|
||||||
if (editor && editor.commands && editor.commands.setReplaceTerm) {
|
if (editor && editor.commands && editor.commands.setReplaceTerm) {
|
||||||
editor.commands.setReplaceTerm(replaceValue);
|
editor.commands.setReplaceTerm(replaceValue);
|
||||||
}
|
}
|
||||||
}, [replaceValue, editor]);
|
}, [visible, replaceValue, editor]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!editor) return;
|
if (!editor) return;
|
||||||
|
@ -51,10 +57,12 @@ export const Search: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
if (!searchExtension) return;
|
if (!searchExtension) return;
|
||||||
|
|
||||||
const listener = () => {
|
const listener = () => {
|
||||||
|
if (!visible) return;
|
||||||
|
|
||||||
const currentIndex = searchExtension ? searchExtension.options.currentIndex : -1;
|
const currentIndex = searchExtension ? searchExtension.options.currentIndex : -1;
|
||||||
const results = searchExtension ? searchExtension.options.results : [];
|
const results = searchExtension ? searchExtension.options.results : [];
|
||||||
setCurrentIndex(currentIndex);
|
setCurrentIndex((preIndex) => (preIndex !== currentIndex ? currentIndex : preIndex));
|
||||||
setResults(results);
|
setResults((prevResults) => (deepEqual(prevResults, results) ? prevResults : results));
|
||||||
};
|
};
|
||||||
|
|
||||||
editor.eventEmitter && editor.eventEmitter.on(ON_SEARCH_RESULTS, listener);
|
editor.eventEmitter && editor.eventEmitter.on(ON_SEARCH_RESULTS, listener);
|
||||||
|
@ -63,11 +71,11 @@ export const Search: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
if (!searchExtension) return;
|
if (!searchExtension) return;
|
||||||
editor.eventEmitter && editor.eventEmitter.off(ON_SEARCH_RESULTS, listener);
|
editor.eventEmitter && editor.eventEmitter.off(ON_SEARCH_RESULTS, listener);
|
||||||
};
|
};
|
||||||
}, [editor]);
|
}, [visible, editor]);
|
||||||
|
|
||||||
const content = (
|
const content = (
|
||||||
<div style={{ padding: isMobile ? '24px 0' : 0 }}>
|
<div style={{ padding: isMobile ? '24px 0' : 0 }}>
|
||||||
<div style={{ marginBottom: 12 }}>
|
<div style={marginBottomStyle}>
|
||||||
<Text type="tertiary">查找</Text>
|
<Text type="tertiary">查找</Text>
|
||||||
<Input
|
<Input
|
||||||
autofocus
|
autofocus
|
||||||
|
@ -76,7 +84,7 @@ export const Search: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
suffix={results.length ? `${currentIndex + 1}/${results.length}` : ''}
|
suffix={results.length ? `${currentIndex + 1}/${results.length}` : ''}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ marginBottom: 12 }}>
|
<div style={marginBottomStyle}>
|
||||||
<Text type="tertiary">替换为</Text>
|
<Text type="tertiary">替换为</Text>
|
||||||
<Input value={replaceValue} onChange={setReplaceValue} />
|
<Input value={replaceValue} onChange={setReplaceValue} />
|
||||||
</div>
|
</div>
|
||||||
|
@ -113,7 +121,7 @@ export const Search: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
{isMobile ? (
|
{isMobile ? (
|
||||||
<>
|
<>
|
||||||
<SideSheet
|
<SideSheet
|
||||||
headerStyle={{ borderBottom: '1px solid var(--semi-color-border)' }}
|
headerStyle={headerStyle}
|
||||||
placement="bottom"
|
placement="bottom"
|
||||||
title={'查找替换'}
|
title={'查找替换'}
|
||||||
visible={visible}
|
visible={visible}
|
||||||
|
|
|
@ -138,17 +138,7 @@ export const CollaborationEditor = forwardRef((props: ICollaborationEditorProps,
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.wrap}>
|
<div className={styles.wrap}>
|
||||||
<DataRender
|
<DataRender loading={loading} error={error} errorContent={renderError} normalContent={renderEditor} />
|
||||||
loading={loading}
|
|
||||||
loadingContent={
|
|
||||||
<div style={{ margin: 'auto' }}>
|
|
||||||
<Spin />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
error={error}
|
|
||||||
errorContent={renderError}
|
|
||||||
normalContent={renderEditor}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue