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