Merge pull request #264 from Hello-job/hotfix/verify

Hotfix/verify
This commit is contained in:
fantasticit 2024-03-18 09:28:51 +08:00 committed by GitHub
commit 48b446747b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
432 changed files with 6724 additions and 2913 deletions

View File

@ -1,5 +1,21 @@
# think
## 声明
1. 请先阅读[提问的智慧](https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way/blob/main/README-zh_CN.md)
2. 为什么停止开发了?
1. 对于文档类产品,无法做出独立的 library 或 framework 给不同需求的团队(或个人),这使得我不确定这件事的意义
2. 对于独立编辑器开发,无论最终以何种形态存在,其表现还是为应用,而非框架(或依赖),能做到的也许只是一种示范
3. 作者本身专攻前端,对高性能、扩展性良好的后端架构心有余而力不足,同时也缺乏专业的运维知识(欢迎赐教)
4. 对于 ProseMirror 和 yjs 本身还有许多玩法,但是精力不足
1. 类似金山文档的表格体验
2. 类似飞书文档的拖拽到节点前后生成分栏
3. markdown 、txt、office 文件的导入导出office 方面可能需要后端协助java poi 是一个可行的选择)
4. 从 office 套件粘贴到编辑器,保留格式和图片(前端可独立完成,思路可参考 TinyCME 的 PowerPaste 和 RTF
5. 基于 yjs 的版本备份和恢复(部分同学提出增量保存 diff个人还是建议全量 snapshot
6. 基于 yjs 的协同开发(比如结合 luckysheet
3. 如果有好的工作和想法,可以和作者联系(发送邮件)
## 简介
Think 是一款开源知识管理工具。通过独立的知识库空间,结构化地组织在线协作文档,实现知识的积累与沉淀,促进知识的复用与流通。同时支持多人协作文档。使用的技术如下:
@ -15,12 +31,6 @@ Think 是一款开源知识管理工具。通过独立的知识库空间,结
[云策文档](https://think.codingit.cn)已经部署上线,可前往注册使用。
## 交流群
欢迎进群交流。
<img width="300" alt="image" src="https://user-images.githubusercontent.com/26452939/184578110-62b49297-da6f-4623-945a-3a03550d924f.PNG">
## 预览
<details>

View File

@ -3,7 +3,7 @@
"private": true,
"author": "fantasticit",
"scripts": {
"clean": "npx rimraf ./node_modules ./packages/**/node_modules",
"clean": "npx rimraf ./node_modules ./packages/**/node_modules ./packages/**/.next",
"dev": "concurrently 'pnpm:dev:*'",
"dev:server": "pnpm run --dir packages/server dev",
"dev:client": "pnpm run --dir packages/client dev",

View File

@ -54,7 +54,21 @@ module.exports = {
'react/react-in-jsx-scope': 'off',
'react/prop-types': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'simple-import-sort/imports': 'error',
'simple-import-sort/imports': [
'error',
{
groups: [
['react'],
['@douyinfe(.*)$'],
['(@)?think(.*)$'],
['(@)?tiptap(.*)$'],
['^@?\\w'],
['@/(.*)'],
['^[./]'],
['(.*).module.scss'],
],
},
],
'simple-import-sort/exports': 'error',
},
ignorePatterns: ['dist/', 'node_modules', 'scripts', 'examples'],

View File

@ -8,11 +8,14 @@
"pm2": "pm2 start npm --name @think/client -- start"
},
"dependencies": {
"@douyinfe/semi-icons": "^2.3.1",
"@douyinfe/semi-next": "^2.3.1",
"@douyinfe/semi-ui": "^2.3.1",
"@douyinfe/semi-icons": "^2.18.0",
"@douyinfe/semi-next": "^2.18.0",
"@douyinfe/semi-ui": "^2.18.0",
"@excalidraw/excalidraw": "^0.12.0",
"@hocuspocus/provider": "^1.0.0-alpha.29",
"@react-pdf-viewer/core": "3.9.0",
"@react-pdf-viewer/default-layout": "3.9.0",
"@react-pdf-viewer/locales": "^1.0.0",
"@think/config": "workspace:^1.0.0",
"@think/constants": "workspace:^1.0.0",
"@think/domains": "workspace:^1.0.0",
@ -60,15 +63,14 @@
"clone": "^2.1.2",
"cross-env": "^7.0.3",
"deep-equal": "^2.0.5",
"docx": "^7.3.0",
"dompurify": "^2.3.5",
"downloadjs": "^1.4.7",
"html-to-docx": "^1.4.0",
"file-saver": "^2.0.5",
"htmldiff-js": "^1.0.5",
"interactjs": "^1.10.11",
"katex": "^0.15.2",
"kity": "^2.0.4",
"lib0": "^0.2.47",
"lodash.pick": "^4.4.0",
"lowlight": "^2.5.0",
"markdown-it": "^12.3.2",
"markdown-it-anchor": "^8.4.1",
@ -78,6 +80,7 @@
"markdown-it-sup": "^1.0.0",
"next": "12.1.0",
"next-pwa": "^5.5.2",
"pdfjs-dist": "3.1.81",
"prosemirror-codemark": "^0.3.6",
"prosemirror-commands": "^1.3.0",
"prosemirror-markdown": "^1.7.0",
@ -93,7 +96,6 @@
"react-full-screen": "^1.1.1",
"react-helmet": "^6.1.0",
"react-lazy-load-image-component": "^1.5.4",
"react-pdf": "^5.7.2",
"react-query": "^3.39.0",
"react-split-pane": "^0.1.92",
"react-visibility-sensor": "^5.1.1",
@ -110,6 +112,7 @@
"devDependencies": {
"@types/node": "17.0.13",
"@types/react": "17.0.38",
"@types/react-lazy-load-image-component": "^1.6.3",
"@typescript-eslint/eslint-plugin": "^5.21.0",
"@typescript-eslint/parser": "^5.21.0",
"copy-webpack-plugin": "11.0.0",

View File

@ -1,6 +1,7 @@
import { TabPane, Tabs } from '@douyinfe/semi-ui';
import React from 'react';
import { TabPane, Tabs } from '@douyinfe/semi-ui';
import { Mail } from './mail';
import { System } from './system';

View File

@ -1,8 +1,10 @@
import React, { useCallback } from 'react';
import { Banner, Button, Form, Toast } from '@douyinfe/semi-ui';
import { DataRender } from 'components/data-render';
import { useSystemConfig } from 'data/user';
import { useToggle } from 'hooks/use-toggle';
import React, { useCallback } from 'react';
export const Mail = () => {
const { data, loading, error, sendTestEmail, updateSystemConfig } = useSystemConfig();

View File

@ -1,9 +1,11 @@
import React, { useCallback } from 'react';
import { IconHelpCircle } from '@douyinfe/semi-icons';
import { Banner, Button, Form, Toast, Tooltip } from '@douyinfe/semi-ui';
import { DataRender } from 'components/data-render';
import { useSystemConfig } from 'data/user';
import { useToggle } from 'hooks/use-toggle';
import React, { useCallback } from 'react';
export const System = () => {
const { data, loading, error, updateSystemConfig } = useSystemConfig();

View File

@ -1,8 +1,10 @@
import React, { useEffect, useRef } from 'react';
import { IconClose } from '@douyinfe/semi-icons';
import { Banner as SemiBanner } from '@douyinfe/semi-ui';
import { BannerProps } from '@douyinfe/semi-ui/banner';
import { useToggle } from 'hooks/use-toggle';
import React, { useEffect, useRef } from 'react';
interface IProps extends BannerProps {
duration?: number;

View File

@ -1,7 +1,9 @@
import React, { useMemo } from 'react';
import { Dropdown, SideSheet, Typography } from '@douyinfe/semi-ui';
import { IsOnMobile } from 'hooks/use-on-mobile';
import { useToggle } from 'hooks/use-toggle';
import React, { useMemo } from 'react';
import styles from './style.module.scss';
@ -89,25 +91,26 @@ export const ColorPicker: React.FC<{
const [visible, toggleVisible] = useToggle(false);
const content = useMemo(
() => (
<div style={{ padding: isMobile ? '24px 0 24px' : '12px 16px', width: isMobile ? 'auto' : 272 }}>
<div className={styles.emptyWrap} onClick={() => onSetColor(null)}>
<span></span>
<Text></Text>
</div>
() =>
!visible ? null : (
<div style={{ padding: isMobile ? '24px 0 24px' : '12px 16px', width: isMobile ? 'auto' : 272 }}>
<div className={styles.emptyWrap} onClick={() => onSetColor(null)}>
<span></span>
<Text></Text>
</div>
<div className={styles.colorWrap}>
{colors.map((color) => {
return (
<div key={color} className={styles.colorItem} onClick={() => onSetColor(color)}>
<span style={{ backgroundColor: color }}></span>
</div>
);
})}
<div className={styles.colorWrap}>
{colors.map((color) => {
return (
<div key={color} className={styles.colorItem} onClick={() => onSetColor(color)}>
<span style={{ backgroundColor: color }}></span>
</div>
);
})}
</div>
</div>
</div>
),
[onSetColor, isMobile]
),
[onSetColor, isMobile, visible]
);
if (disabled) return <span style={{ display: 'inline-block' }}>{children}</span>;
@ -132,7 +135,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>
)}

View File

@ -1,28 +1,34 @@
import { Spin, Typography } from '@douyinfe/semi-ui';
import { Empty } from 'illustrations/empty';
import React, { useMemo } from 'react';
import { Spin, Typography } from '@douyinfe/semi-ui';
import { Empty } from 'illustrations/empty';
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>;
};
const emptyStyle: React.CSSProperties = {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'column',
position: 'relative',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
};
export const defaultEmpty = (
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'column',
position: 'relative',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
}}
>
<div style={emptyStyle}>
<div>
<Empty />
</div>

View File

@ -1,5 +1,7 @@
import React from 'react';
import deepEqual from 'deep-equal';
import { defaultEmpty, defaultLoading, defaultRenderError, Render } from './constant';
import { LoadingWrap } from './loading';
@ -15,7 +17,7 @@ interface IProps {
normalContent: RenderProps;
}
export const DataRender: React.FC<IProps> = ({
export const _DataRender: React.FC<IProps> = ({
loading,
error,
empty,
@ -36,3 +38,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);
});

View File

@ -1,6 +1,7 @@
import { useToggle } from 'hooks/use-toggle';
import React, { useEffect, useRef } from 'react';
import { useToggle } from 'hooks/use-toggle';
import { Render } from './constant';
export const LoadingWrap = ({ loading, delay = 200, loadingContent, normalContent }) => {

View File

@ -1,8 +1,10 @@
import React from 'react';
import { Button } from '@douyinfe/semi-ui';
import { DocumentCreator as DocumenCreatorForm } from 'components/document/create';
import { useRouterQuery } from 'hooks/use-router-query';
import { useToggle } from 'hooks/use-toggle';
import React from 'react';
interface IProps {
onCreateDocument?: () => void;

View File

@ -1,7 +1,11 @@
import React, { useCallback } from 'react';
import { IconArticle, IconBranch, IconExport, IconHistory, IconMore, IconPlus, IconStar } from '@douyinfe/semi-icons';
import { Button, Dropdown, Space, Typography } from '@douyinfe/semi-ui';
import { ButtonProps } from '@douyinfe/semi-ui/button/Button';
import { IDocument, IOrganization, IWiki } from '@think/domains';
import cls from 'classnames';
import { DocumentCreator } from 'components/document/create';
import { DocumentDeletor } from 'components/document/delete';
@ -12,7 +16,6 @@ import { DocumentStar } from 'components/document/star';
import { DocumentStyle } from 'components/document/style';
import { DocumentVersionTrigger } from 'components/document/version';
import { useToggle } from 'hooks/use-toggle';
import React, { useCallback } from 'react';
import styles from './index.module.scss';
@ -78,6 +81,7 @@ export const DocumentActions: React.FC<IProps> = ({
position="bottomLeft"
visible={popoverVisible}
onVisibleChange={wrapOnVisibleChange}
stopPropagation={true}
content={
<Dropdown.Menu style={{ minWidth: 112 }}>
{showCreateDocument && (
@ -169,24 +173,6 @@ export const DocumentActions: React.FC<IProps> = ({
/>
)}
{!hideDocumentVersion && (
<DocumentStyle
key="style"
render={({ onClick }) => {
return (
<Dropdown.Item onClick={onClick}>
<Text>
<Space>
<IconArticle />
</Space>
</Text>
</Dropdown.Item>
);
}}
/>
)}
{document && (
<DocumentExporter
document={document}

View File

@ -1,13 +1,16 @@
import { useCallback } from 'react';
import { IconEdit, IconUser } from '@douyinfe/semi-icons';
import { Avatar, Button, Skeleton, Space, Tooltip, Typography } from '@douyinfe/semi-ui';
import type { IDocument } from '@think/domains';
import { DocumentShare } from 'components/document/share';
import { DocumentStar } from 'components/document/star';
import { IconDocument } from 'components/icons/IconDocument';
import { LocaleTime } from 'components/locale-time';
import Link from 'next/link';
import Router from 'next/router';
import { useCallback } from 'react';
import styles from './index.module.scss';

View File

@ -1,12 +1,14 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { IconUserAdd } from '@douyinfe/semi-icons';
import { Avatar, AvatarGroup, Button, Dropdown, Modal, Toast, Tooltip } from '@douyinfe/semi-ui';
import { Avatar, AvatarGroup, Button, Dropdown, Modal, Popover, Toast, Tooltip, Typography } from '@douyinfe/semi-ui';
import { Members } from 'components/members';
import { useDoumentMembers } from 'data/document';
import { useUser } from 'data/user';
import { event, JOIN_USER } from 'event';
import { IsOnMobile } from 'hooks/use-on-mobile';
import { useToggle } from 'hooks/use-toggle';
import React, { useEffect, useMemo, useRef, useState } from 'react';
interface IProps {
wikiId: string;
@ -14,27 +16,42 @@ interface IProps {
disabled?: boolean;
}
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(
() => (
<div style={{ padding: '24px 0' }}>
<Members
id={documentId}
hook={useDoumentMembers}
descriptions={[
'权限继承:默认继承知识库成员权限',
'超级管理员:组织超级管理员、知识库超级管理员和文档创建者',
]}
/>
</div>
),
[documentId]
() =>
!visible ? null : (
<div style={{ padding: '24px 0' }}>
<Members
id={documentId}
hook={useDoumentMembers}
descriptions={[
'权限继承:默认继承知识库成员权限',
'超级管理员:组织超级管理员、知识库超级管理员和文档创建者',
]}
/>
</div>
),
[visible, documentId]
);
const btn = useMemo(
() => (
<Button theme="borderless" type="tertiary" disabled={disabled} icon={<IconUserAdd />} onClick={toggleVisible} />
@ -42,6 +59,27 @@ export const DocumentCollaboration: React.FC<IProps> = ({ wikiId, documentId, di
[disabled, toggleVisible]
);
const renderMore = useCallback((restNumber, restAvatars) => {
const content = restAvatars.map((avatar, index) => {
return (
<div style={{ paddingBottom: 12 }} key={index}>
{React.cloneElement(avatar, { size: 'extra-small' })}
<Text style={{ marginLeft: 8, fontSize: 14 }}>{avatar.props.content}</Text>
</div>
);
});
return (
<Popover
content={<div style={{ maxHeight: '50vh', overflow: 'auto' }}>{content}</div>}
autoAdjustOverflow={false}
position={'bottomRight'}
style={{ padding: '12px 8px', paddingBottom: 0 }}
>
<Avatar size="extra-small">{`+${restNumber}`}</Avatar>
</Popover>
);
}, []);
useEffect(() => {
const handler = (users) => {
const joinUsers = users
@ -49,35 +87,40 @@ export const DocumentCollaboration: React.FC<IProps> = ({ wikiId, documentId, di
.filter((state) => state.user)
.map((state) => ({ ...state.user, clientId: state.clientId }));
joinUsers
const otherUsers = joinUsers
.filter(Boolean)
.filter((joinUser) => {
return joinUser.name !== currentUser.name;
})
.forEach((joinUser) => {
if (!toastedUsersRef.current.includes(joinUser.clientId)) {
Toast.info(`${joinUser.name}-${joinUser.clientId}加入文档`);
toastedUsersRef.current.push(joinUser.clientId);
}
.filter((joinUser) => {
return !toastedUsersRef.current.includes(joinUser.clientId);
});
if (otherUsers.length) {
Toast.info(`${otherUsers[0].name}${otherUsers.length}个用户加入文档`);
otherUsers.forEach((joinUser) => {
toastedUsersRef.current.push(joinUser.clientId);
});
}
setCollaborationUsers(joinUsers);
};
event.on(JOIN_USER, handler);
return () => {
toastedUsersRef.current = [];
event.off(JOIN_USER, handler);
toastedUsersRef.current = [];
};
}, [currentUser]);
return (
<>
<AvatarGroup maxCount={5} size="extra-small">
<AvatarGroup maxCount={2} renderMore={renderMore} size="extra-small">
{collaborationUsers.map((user) => {
return (
<Tooltip key={user.id} content={`${user.name}-${user.clientId}`} position="bottom">
<Tooltip key={user.clientId} content={`${user.name}-${user.clientId}`} position="bottom">
<Avatar src={user.avatar} size="extra-small">
{user.name && user.name.charAt(0)}
</Avatar>
@ -85,6 +128,7 @@ export const DocumentCollaboration: React.FC<IProps> = ({ wikiId, documentId, di
);
})}
</AvatarGroup>
{isMobile ? (
<>
<Modal
@ -93,7 +137,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>
@ -106,19 +150,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>

View File

@ -1,6 +1,7 @@
import type { IComment } from '@think/domains';
import React from 'react';
import type { IComment } from '@think/domains';
import { CommentItem } from './item';
interface IProps {

View File

@ -1,9 +1,12 @@
import React from 'react';
import { IconUser } from '@douyinfe/semi-icons';
import { Avatar, Popconfirm, Skeleton, Space, Typography } from '@douyinfe/semi-ui';
import type { IComment, IUser } from '@think/domains';
import { LocaleTime } from 'components/locale-time';
import { useUser } from 'data/user';
import React from 'react';
import styles from './index.module.scss';

View File

@ -1,13 +1,17 @@
import React, { useCallback, useRef, useState } from 'react';
import { Avatar, Banner, Button, Pagination, Space, Spin, Typography } from '@douyinfe/semi-ui';
import { EditorContent, useEditor } from 'tiptap/core';
import { CommentKit, CommentMenuBar } from 'tiptap/editor';
import { DataRender } from 'components/data-render';
import { useComments } from 'data/comment';
import { useUser } from 'data/user';
import { useToggle } from 'hooks/use-toggle';
import React, { useCallback, useRef, useState } from 'react';
import { EditorContent, useEditor } from 'tiptap/core';
import { CommentKit, CommentMenuBar } from 'tiptap/editor';
import { Comments } from './comments';
import styles from './index.module.scss';
interface IProps {

View File

@ -1,12 +1,15 @@
import { Dispatch, SetStateAction, useCallback, useEffect, useState } from 'react';
import { Checkbox, Modal, TabPane, Tabs } from '@douyinfe/semi-ui';
import { IDocument, IWiki } from '@think/domains';
import { TemplateCardEmpty } from 'components/template/card';
import { TemplateList } from 'components/template/list';
import { useCreateDocument } from 'data/document';
import { useOwnTemplates, usePublicTemplates } from 'data/template';
import { useRouterQuery } from 'hooks/use-router-query';
import Router from 'next/router';
import { Dispatch, SetStateAction, useCallback, useEffect, useState } from 'react';
import styles from './index.module.scss';

View File

@ -1,9 +1,11 @@
import React, { useCallback, useMemo } from 'react';
import { IconDelete } from '@douyinfe/semi-icons';
import { Popconfirm, Space, Typography } from '@douyinfe/semi-ui';
import { useDeleteDocument } from 'data/document';
import { useRouterQuery } from 'hooks/use-router-query';
import Router from 'next/router';
import React, { useCallback, useMemo } from 'react';
interface IProps {
wikiId: string;

View File

@ -1,9 +1,12 @@
import React, { useEffect, useRef } from 'react';
import { IAuthority, ILoginUser } from '@think/domains';
import { CollaborationEditor, ICollaborationRefProps } from 'tiptap/editor';
import cls from 'classnames';
import { event, triggerChangeDocumentTitle, triggerJoinUser, USE_DOCUMENT_VERSION } from 'event';
import { useMount } from 'hooks/use-mount';
import React, { useEffect, useRef } from 'react';
import { CollaborationEditor, ICollaborationRefProps } from 'tiptap/editor';
import styles from './index.module.scss';
@ -23,6 +26,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 () => {

View File

@ -1,5 +1,8 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { IconChevronLeft } from '@douyinfe/semi-icons';
import { Button, Nav, Skeleton, Space, Tooltip, Typography } from '@douyinfe/semi-ui';
import { DataRender } from 'components/data-render';
import { Divider } from 'components/divider';
import { DocumentCollaboration } from 'components/document/collaboration';
@ -16,10 +19,10 @@ import { IsOnMobile } from 'hooks/use-on-mobile';
import { useWindowSize } from 'hooks/use-window-size';
import { SecureDocumentIllustration } from 'illustrations/secure-document';
import Router from 'next/router';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { DocumentActions } from '../actions';
import { Editor } from './editor';
import styles from './index.module.scss';
const { Text } = Typography;
@ -28,6 +31,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();
@ -62,7 +73,7 @@ export const DocumentEditor: React.FC<IProps> = ({ documentId }) => {
{document && (
<DocumentActions organizationId={document.organizationId} wikiId={document.wikiId} documentId={documentId} />
)}
<DocumentVersion documentId={documentId} onSelect={triggerUseDocumentVersion} />
<DocumentVersion key={'edit'} documentId={documentId} onSelect={triggerUseDocumentVersion} />
</Space>
),
[documentId, document, authority]
@ -84,9 +95,7 @@ export const DocumentEditor: React.FC<IProps> = ({ documentId }) => {
mode="horizontal"
header={
<>
<Tooltip content="返回" position="bottom">
<Button onMouseDown={goback} icon={<IconChevronLeft />} style={{ marginRight: 16 }} />
</Tooltip>
<Button onMouseDown={goback} icon={<IconChevronLeft />} style={{ marginRight: 16 }} />
<DataRender
loading={docAuthLoading}
error={docAuthError}
@ -125,13 +134,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 (
<>

View File

@ -1,19 +1,24 @@
import { Badge, Button, Dropdown, Modal, Space, Typography } from '@douyinfe/semi-ui';
import { IDocument } from '@think/domains';
import { IconJSON, IconMarkdown, IconPDF, IconWord } from 'components/icons';
import { useDocumentDetail } from 'data/document';
import download from 'downloadjs';
import { safeJSONParse, safeJSONStringify } from 'helpers/json';
import { IsOnMobile } from 'hooks/use-on-mobile';
import { useToggle } from 'hooks/use-toggle';
import React, { useCallback, useEffect, useMemo } from 'react';
import { Badge, Button, Dropdown, Modal, Space, Typography } from '@douyinfe/semi-ui';
import { IDocument } from '@think/domains';
import { createEditor } from 'tiptap/core';
import { AllExtensions } from 'tiptap/core/all-kit';
import { prosemirrorToMarkdown } from 'tiptap/markdown/prosemirror-to-markdown';
import styles from './index.module.scss';
import { IconJSON, IconMarkdown, IconPDF, IconWord } from 'components/icons';
import { useDocumentDetail } from 'data/document';
import FileSaver from 'file-saver';
import { safeJSONParse, safeJSONStringify } from 'helpers/json';
import { IsOnMobile } from 'hooks/use-on-mobile';
import { useToggle } from 'hooks/use-toggle';
import { printEditorContent } from './pdf';
import styles from './index.module.scss';
const { Text } = Typography;
interface IProps {
@ -40,18 +45,21 @@ export const DocumentExporter: React.FC<IProps> = ({ document, render }) => {
const exportMarkdown = useCallback(() => {
const md = prosemirrorToMarkdown({ content: editor.state.doc.slice(0).content });
download(md, `${document.title}.md`, 'text/plain');
const blob = new Blob([md], { type: 'text/plain;charset=utf-8' });
FileSaver.saveAs(blob, `${document.title}.md`);
}, [document, editor]);
const exportJSON = useCallback(() => {
download(safeJSONStringify(editor.getJSON()), `${document.title}.json`, 'text/plain');
const blob = new Blob([safeJSONStringify(editor.getJSON())], { type: 'text/plain;charset=utf-8' });
FileSaver.saveAs(blob, `${document.title}.json`);
}, [document, editor]);
const exportWord = useCallback(() => {
const editorContent = editor.view.dom.closest('.ProseMirror');
if (editorContent) {
exportDocx(editorContent.outerHTML).then((res) => {
download(Buffer.from(res as Buffer), `${document.title}.docx`);
const blob = new Blob([Buffer.from(res as Buffer)], { type: 'text/plain;charset=utf-8' });
FileSaver.saveAs(blob, `${document.title}.docx`);
});
}
}, [editor, exportDocx, document]);
@ -70,7 +78,7 @@ export const DocumentExporter: React.FC<IProps> = ({ document, render }) => {
}}
>
<Space>
<div className={styles.templateItem} onClick={exportMarkdown}>
<div className={styles.templateItem} onMouseDown={exportMarkdown}>
<header>
<IconMarkdown style={{ fontSize: 40 }} />
</header>
@ -82,7 +90,7 @@ export const DocumentExporter: React.FC<IProps> = ({ document, render }) => {
</footer>
</div>
<div className={styles.templateItem} onClick={exportJSON}>
<div className={styles.templateItem} onMouseDown={exportJSON}>
<header>
<IconJSON style={{ fontSize: 40 }} />
</header>
@ -94,7 +102,7 @@ export const DocumentExporter: React.FC<IProps> = ({ document, render }) => {
</footer>
</div>
<div className={styles.templateItem} onClick={exportWord}>
<div className={styles.templateItem} onMouseDown={exportWord}>
<header>
<Badge count="beta" type="danger">
<IconWord style={{ fontSize: 40 }} />
@ -108,7 +116,7 @@ export const DocumentExporter: React.FC<IProps> = ({ document, render }) => {
</footer>
</div>
<div className={styles.templateItem} onClick={exportPDF}>
<div className={styles.templateItem} onMouseDown={exportPDF}>
<header>
<Badge count="beta" type="danger">
<IconPDF style={{ fontSize: 40 }} />
@ -166,7 +174,7 @@ export const DocumentExporter: React.FC<IProps> = ({ document, render }) => {
<Dropdown
visible={visible}
onVisibleChange={toggleVisible}
trigger="click"
trigger="custom"
position="bottomRight"
content={<div style={{ padding: '0 16px' }}>{content}</div>}
>

View File

@ -9,8 +9,7 @@ function printHtml(dom: Element) {
const content: string = style + dom.outerHTML;
const iframe: HTMLIFrameElement = document.createElement('iframe');
iframe.id = 'el-tiptap-iframe';
iframe.setAttribute('style', 'position: absolute; width: 0; height: 0; top: -10px; left: -10px;');
iframe.setAttribute('style', 'position: absolute; width: 0; height: 0; top: 0; left: 0;');
document.body.appendChild(iframe);
const frameWindow = iframe.contentWindow;

View File

@ -1,15 +1,18 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { FullScreen, useFullScreenHandle } from 'react-full-screen';
import { IconShrinkScreenStroked } from '@douyinfe/semi-icons';
import { Button, Space, Tooltip, Typography } from '@douyinfe/semi-ui';
import { EditorContent, useEditor } from '@tiptap/react';
import { CollaborationKit, Document } from 'tiptap/editor';
import cls from 'classnames';
import { IconFullscreen } from 'components/icons/IconFullscreen';
import { IconPencil } from 'components/icons/IconPencil';
import { safeJSONParse } from 'helpers/json';
import { useDrawingCursor } from 'hooks/use-cursor';
import { useToggle } from 'hooks/use-toggle';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { FullScreen, useFullScreenHandle } from 'react-full-screen';
import { CollaborationKit, Document } from 'tiptap/editor';
import styles from './index.module.scss';
@ -74,11 +77,14 @@ export const DocumentFullscreen: React.FC<IProps> = ({ data }) => {
const [isDrawing, toggleDrawing] = useToggle(false);
const [cover, setCover] = useState('');
const editor = useEditor({
editable: false,
extensions: CollaborationKit.filter((ext) => ['title', 'doc'].indexOf(ext.name) < 0).concat(Document),
content: { type: 'doc', content: [] },
});
const editor = useEditor(
{
editable: false,
extensions: CollaborationKit.filter((ext) => ['title', 'doc'].indexOf(ext.name) < 0).concat(Document),
content: { type: 'doc', content: [] },
},
[]
);
const startPowerpoint = useCallback(() => {
toggleVisible(true);

View File

@ -1,9 +1,12 @@
import React, { useCallback } from 'react';
import { IconLink } from '@douyinfe/semi-icons';
import { Space, Typography } from '@douyinfe/semi-ui';
import { IDocument, IOrganization, IWiki } from '@think/domains';
import { copy } from 'helpers/copy';
import { buildUrl } from 'helpers/url';
import React, { useCallback } from 'react';
interface IProps {
organizationId: IOrganization['id'];
@ -14,6 +17,8 @@ interface IProps {
const { Text } = Typography;
const style = { cursor: 'pointer' };
export const DocumentLinkCopyer: React.FC<IProps> = ({ organizationId, wikiId, documentId, render }) => {
const handle = useCallback(() => {
copy(buildUrl(`/app/org/${organizationId}/wiki/${wikiId}/doc/${documentId}`));
@ -29,7 +34,7 @@ export const DocumentLinkCopyer: React.FC<IProps> = ({ organizationId, wikiId, d
return render ? (
<>{render({ copy: handle, children: content })}</>
) : (
<Text onClick={handle} style={{ cursor: 'pointer' }}>
<Text onClick={handle} style={style}>
{content}
</Text>
);

View File

@ -1,25 +1,28 @@
import React from 'react';
import { IconUser } from '@douyinfe/semi-icons';
import { Avatar, Space } from '@douyinfe/semi-ui';
import { IDocument } from '@think/domains';
import { LocaleTime } from 'components/locale-time';
import React from 'react';
interface IProps {
document: IDocument;
}
const style = {
borderTop: '1px solid var(--semi-color-border)',
marginTop: '0.75em',
padding: '16px 0',
fontSize: 13,
fontWeight: 'normal',
color: 'var(--semi-color-text-0)',
};
export const Author: React.FC<IProps> = ({ document }) => {
return (
<div
style={{
borderTop: '1px solid var(--semi-color-border)',
marginTop: '0.75em',
padding: '16px 0',
fontSize: 13,
fontWeight: 'normal',
color: 'var(--semi-color-text-0)',
}}
>
<div style={style}>
<Space>
<Avatar size="small" src={document && document.createUser && document.createUser.avatar}>
<IconUser />

View File

@ -1,8 +1,13 @@
import React, { useCallback, useMemo } from 'react';
import { createPortal } from 'react-dom';
import { IconEdit } from '@douyinfe/semi-icons';
import { Button, Layout, Nav, Skeleton, Space, Spin, Tooltip, Typography } from '@douyinfe/semi-ui';
import { CollaborationEditor } from 'tiptap/editor';
import { DataRender } from 'components/data-render';
import { DocumentCollaboration } from 'components/document/collaboration';
import { DocumentShare } from 'components/document/share';
import { DocumentStar } from 'components/document/star';
import { DocumentStyle } from 'components/document/style';
import { DocumentVersion } from 'components/document/version';
@ -14,13 +19,11 @@ import { useMount } from 'hooks/use-mount';
import { IsOnMobile } from 'hooks/use-on-mobile';
import { useWindowSize } from 'hooks/use-window-size';
import Router from 'next/router';
import React, { useCallback, useMemo } from 'react';
import { createPortal } from 'react-dom';
import { CollaborationEditor } from 'tiptap/editor';
import { DocumentActions } from '../actions';
import { DocumentFullscreen } from '../fullscreen';
import { Author } from './author';
import styles from './index.module.scss';
const { Header } = Layout;
@ -30,6 +33,14 @@ interface IProps {
documentId: string;
}
const loadingStyle = {
minHeight: 240,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
margin: 'auto',
};
export const DocumentReader: React.FC<IProps> = ({ documentId }) => {
const { isMobile } = IsOnMobile.useHook();
const mounted = useMount();
@ -37,6 +48,7 @@ export const DocumentReader: React.FC<IProps> = ({ documentId }) => {
const { user } = useUser();
const { data: documentAndAuth, loading: docAuthLoading, error: docAuthError } = useDocumentDetail(documentId);
const { document, authority } = documentAndAuth || {};
const [readable, editable] = useMemo(() => {
if (!authority) return [false, false];
return [authority.readable, authority.editable];
@ -84,6 +96,7 @@ export const DocumentReader: React.FC<IProps> = ({ documentId }) => {
documentId={documentId}
/>
)}
<DocumentStyle key="style" />
<Tooltip key="edit" content="编辑" position="bottom">
<Button disabled={!editable} icon={<IconEdit />} onMouseDown={gotoEdit} />
</Tooltip>
@ -131,15 +144,7 @@ export const DocumentReader: React.FC<IProps> = ({ documentId }) => {
<DataRender
loading={docAuthLoading}
loadingContent={
<div
style={{
minHeight: 240,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
margin: 'auto',
}}
>
<div style={loadingStyle}>
<Spin />
</div>
}

View File

@ -9,7 +9,6 @@ import { Seo } from 'components/seo';
import { Theme } from 'components/theme';
import { User } from 'components/user';
import { usePublicDocumentDetail } from 'data/document';
import { useDocumentStyle } from 'hooks/use-document-style';
import { useMount } from 'hooks/use-mount';
import { IsOnMobile } from 'hooks/use-on-mobile';
import { SecureDocumentIllustration } from 'illustrations/secure-document';
@ -38,12 +37,8 @@ export const DocumentPublicReader: React.FC<IProps> = ({ documentId, hideLogo =
const mounted = useMount();
const { wikiId: currentWikiId } = useRouterQuery<{ wikiId: IWiki['id']; documentId: IDocument['id'] }>();
const { data, loading, error, query } = usePublicDocumentDetail(documentId);
const { width, fontSize } = useDocumentStyle();
const { isMobile } = IsOnMobile.useHook();
const editorWrapClassNames = useMemo(() => {
return width === 'standardWidth' ? styles.isStandardWidth : styles.isFullWidth;
}, [width]);
const renderAuthor = useCallback(
(element) => {
if (!document) return null;

View File

@ -1,12 +1,15 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { IconLink } from '@douyinfe/semi-icons';
import { Button, Dropdown, Input, Modal, Space, Toast, Typography } from '@douyinfe/semi-ui';
import { isPublicDocument } from '@think/domains';
import { useDocumentDetail } from 'data/document';
import { getDocumentShareURL } from 'helpers/url';
import { IsOnMobile } from 'hooks/use-on-mobile';
import { useToggle } from 'hooks/use-toggle';
import { ShareIllustration } from 'illustrations/share';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
interface IProps {
documentId: string;

View File

@ -1,11 +1,14 @@
import { IconStar } from '@douyinfe/semi-icons';
import { Button, Tooltip } from '@douyinfe/semi-ui';
import { IDocument, IOrganization, IWiki } from '@think/domains';
import { useDocumentStarToggle } from 'data/star';
import { useToggle } from 'hooks/use-toggle';
import React, { useCallback } from 'react';
import VisibilitySensor from 'react-visibility-sensor';
import { IconStar } from '@douyinfe/semi-icons';
import { Button, Tooltip } from '@douyinfe/semi-ui';
import { IDocument, IOrganization, IWiki } from '@think/domains';
import { useDocumentStarToggle } from 'data/star';
import { useToggle } from 'hooks/use-toggle';
interface IProps {
organizationId: IOrganization['id'];
wikiId: IWiki['id'];
@ -33,6 +36,15 @@ export const DocumentStar: React.FC<IProps> = ({ organizationId, wikiId, documen
[toggleVisible]
);
const toggleStarAction = useCallback(
(e) => {
e.stopPropagation();
e.preventDefault();
toggleStar();
},
[toggleStar]
);
return (
<VisibilitySensor onChange={onViewportChange}>
{render ? (
@ -46,11 +58,7 @@ export const DocumentStar: React.FC<IProps> = ({ organizationId, wikiId, documen
color: data ? 'rgba(var(--semi-amber-4), 1)' : 'rgba(var(--semi-grey-3), 1)',
}}
disabled={disabled}
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
toggleStar();
}}
onClick={toggleStarAction}
/>
</Tooltip>
)}

View File

@ -1,10 +1,12 @@
import React, { useMemo } from 'react';
import { IconArticle } from '@douyinfe/semi-icons';
import { Button, Dropdown, Radio, RadioGroup, Slider, Typography } from '@douyinfe/semi-ui';
import { throttle } from 'helpers/throttle';
import { useDocumentStyle } from 'hooks/use-document-style';
import { IsOnMobile } from 'hooks/use-on-mobile';
import { useToggle } from 'hooks/use-toggle';
import React, { useMemo } from 'react';
import styles from './index.module.scss';
@ -32,14 +34,8 @@ export const DocumentStyle: React.FC<IProps> = ({ render }) => {
position={isMobile ? 'topRight' : 'bottomLeft'}
visible={visible}
onVisibleChange={toggleVisible}
onClickOutSide={toggleVisible}
content={
<div
className={styles.wrap}
onClick={(e) => {
e.stopPropagation();
}}
>
<div className={styles.wrap}>
<div className={styles.item}>
<Text></Text>
<Text style={{ fontSize: '0.8em' }}> {fontSize}px</Text>

View File

@ -25,7 +25,6 @@
main {
padding: 24px;
overflow: auto;
background-color: var(--semi-color-bg-0);
flex: 1;
&.isMobile {

View File

@ -1,6 +1,11 @@
import React, { useCallback, useEffect, useState } from 'react';
import { IconChevronLeft } from '@douyinfe/semi-icons';
import { Button, Modal, Select, Space, Tag, Typography } from '@douyinfe/semi-ui';
import { EditorContent, useEditor } from '@tiptap/react';
import { CollaborationKit } from 'tiptap/editor';
import cls from 'classnames';
import { DataRender } from 'components/data-render';
import { LocaleTime } from 'components/locale-time';
@ -9,8 +14,6 @@ import { generateDiffHtml } from 'helpers/generate-html';
import { safeJSONParse } from 'helpers/json';
import { DocumentVersionControl } from 'hooks/use-document-version';
import { IsOnMobile } from 'hooks/use-on-mobile';
import React, { useCallback, useEffect, useState } from 'react';
import { CollaborationKit } from 'tiptap/editor';
import styles from './index.module.scss';
@ -31,16 +34,19 @@ export const DocumentVersion: React.FC<Partial<IProps>> = ({ documentId, onSelec
const [selectedVersion, setSelectedVersion] = useState(null);
const [diffVersion, setDiffVersion] = useState(null);
const editor = useEditor({
editable: false,
editorProps: {
attributes: {
class: 'is-editable',
const editor = useEditor(
{
editable: false,
editorProps: {
attributes: {
class: 'is-editable',
},
},
extensions: CollaborationKit,
content: { type: 'doc', content: [] },
},
extensions: CollaborationKit,
content: { type: 'doc', content: [] },
});
[]
);
const close = useCallback(() => {
toggleVisible(false);
@ -123,8 +129,8 @@ export const DocumentVersion: React.FC<Partial<IProps>> = ({ documentId, onSelec
</Select>
<div style={{ paddingLeft: '8px' }}></div>
<Space style={{ marginLeft: 12 }}>
<Tag style={{ backgroundColor: '#e9ffe9' }}></Tag>
<Tag style={{ backgroundColor: '#ffeaea' }}></Tag>
<Tag style={{ backgroundColor: '#e9ffe9', color: '#333' }}></Tag>
<Tag style={{ backgroundColor: '#ffeaea', color: '#333' }}></Tag>
</Space>
</div>
)}
@ -153,8 +159,8 @@ export const DocumentVersion: React.FC<Partial<IProps>> = ({ documentId, onSelec
empty={!loading && !data.length}
normalContent={() => (
<div className={styles.contentWrap}>
<main className={cls('container', isMobile && styles.isMobile)}>
<div>
<main className={cls(isMobile && styles.isMobile)}>
<div className="container">
{diffVersion ? (
<div id="diff-visual" className="ProseMirror"></div>
) : (

View File

@ -1,10 +1,13 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { Button, Popover, SideSheet, TabPane, Tabs } from '@douyinfe/semi-ui';
import { createKeysLocalStorageLRUCache } from 'helpers/lru-cache';
import { IsOnMobile } from 'hooks/use-on-mobile';
import { useToggle } from 'hooks/use-toggle';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { ACTIVITIES, EXPRESSIONES, GESTURES, OBJECTS, SKY_WEATHER, SYMBOLS } from './constants';
import styles from './index.module.scss';
const emojiLocalStorageLRUCache = createKeysLocalStorageLRUCache('EMOJI_PICKER', 20);
@ -64,38 +67,44 @@ export const EmojiPicker: React.FC<IProps> = ({ showClear = false, onSelectEmoji
}, [onSelectEmoji]);
const content = useMemo(
() => (
<div className={styles.wrap}>
<Tabs
size="small"
lazyRender
keepDOM
tabBarExtraContent={
showClear ? (
<Button size="small" onClick={clear}>
</Button>
) : null
}
collapsible
>
{renderedList.map((list) => {
return (
<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)}>
{ex}
</div>
))}
</div>
</TabPane>
);
})}
</Tabs>
</div>
),
[showClear, renderedList, selectEmoji, clear]
() =>
!visible ? null : (
<div className={styles.wrap}>
<Tabs
size="small"
lazyRender
keepDOM
tabBarExtraContent={
showClear ? (
<Button size="small" onClick={clear}>
</Button>
) : null
}
collapsible
>
{renderedList.map((list) => {
return (
<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)}>
{ex}
</div>
))}
</div>
</TabPane>
);
})}
</Tabs>
</div>
),
[visible, showClear, renderedList, selectEmoji, clear]
);
useEffect(() => {

View File

@ -1,6 +1,7 @@
import { Typography } from '@douyinfe/semi-ui';
import React from 'react';
import { Typography } from '@douyinfe/semi-ui';
interface IProps {
illustration?: React.ReactNode;
message: React.ReactNode;

View File

@ -1,11 +1,10 @@
import React, { MouseEventHandler } from 'react';
type CellProperties = {
active: boolean;
hover: boolean;
disabled: boolean;
cellSize: number;
onClick: MouseEventHandler<HTMLDivElement>;
onMouseDown: MouseEventHandler<HTMLDivElement>;
onMouseEnter: MouseEventHandler<HTMLDivElement>;
styles: Record<string, React.CSSProperties>;
id: string;
@ -38,7 +37,7 @@ const getMergedStyle = (baseStyles, styles, styleClass) => ({
...(styles && styles[styleClass] ? styles[styleClass] : {}),
});
export const GridCell = ({ active, hover, disabled, onClick, onMouseEnter, cellSize, styles, id }: CellProperties) => {
export const GridCell = ({ hover, disabled, onMouseDown, onMouseEnter, cellSize, styles, id }: CellProperties) => {
const baseStyles = getBaseStyles(cellSize);
const cellStyles = {
cell: getMergedStyle(baseStyles, styles, 'cell'),
@ -52,11 +51,10 @@ export const GridCell = ({ active, hover, disabled, onClick, onMouseEnter, cellS
id={id}
style={{
...cellStyles.cell,
...(active && cellStyles.active),
...(hover && cellStyles.hover),
...(!active && disabled && cellStyles.disabled),
...(disabled && cellStyles.disabled),
}}
onClick={onClick}
onMouseDown={onMouseDown}
onMouseEnter={onMouseEnter}
/>
);

View File

@ -1,6 +1,7 @@
import { Typography } from '@douyinfe/semi-ui';
import React, { useCallback, useMemo, useState } from 'react';
import { Typography } from '@douyinfe/semi-ui';
import { GridCell } from './grid-cell';
const { Text } = Typography;
@ -44,10 +45,6 @@ export const GridSelect = ({
cellSize = 16,
styles,
}: RegionSelectionProps) => {
const [activeCell, setActiveCell] = useState<CoordsType>({
x: -1,
y: -1,
});
const [hoverCell, setHoverCell] = useState<CoordsType>(null);
const onClick = useCallback(
@ -60,6 +57,15 @@ export const GridSelect = ({
[onSelect]
);
const onClickPanel = useCallback(() => {
if (hoverCell.x + 1 > 0 && hoverCell.y + 1 > 0) {
onSelect({
rows: hoverCell.y + 1,
cols: hoverCell.x + 1,
});
}
}, [hoverCell, onSelect]);
const onHover = useCallback(({ x, y, isCellDisabled }) => {
if (isCellDisabled) {
return setHoverCell(null);
@ -71,16 +77,17 @@ export const GridSelect = ({
const cells = [];
for (let y = 0; y < rows; y++) {
for (let x = 0; x < cols; x++) {
const isActive = x <= activeCell.x && y <= activeCell.y;
const isHover = hoverCell && x <= hoverCell.x && y <= hoverCell.y;
const isCellDisabled = disabled;
cells.push(
<GridCell
id={x + '-' + y}
key={x + '-' + y}
onClick={() => onClick({ x, y, isCellDisabled })}
onMouseDown={(e) => {
e.stopPropagation();
onClick({ x, y, isCellDisabled });
}}
onMouseEnter={onHover.bind(null, { x, y, isCellDisabled })}
active={isActive}
hover={isHover}
disabled={isCellDisabled}
styles={styles}
@ -90,12 +97,12 @@ export const GridSelect = ({
}
}
return cells;
}, [rows, cols, disabled, activeCell.x, activeCell.y, cellSize, hoverCell, styles, onClick, onHover]);
}, [rows, cols, disabled, cellSize, hoverCell, styles, onClick, onHover]);
const baseStyles = useMemo(() => getBaseStyles(cols, cellSize), [cols, cellSize]);
return (
<div>
<div onMouseDown={onClickPanel}>
<div
style={
{

View File

@ -0,0 +1,43 @@
import { Icon } from '@douyinfe/semi-ui';
export const IconAddColBefore: React.FC<{ style?: React.CSSProperties }> = ({ style = {} }) => {
return (
<Icon
style={style}
svg={
<svg width="1em" height="1em" fill="currentColor" viewBox="0 0 24 24">
<path fill="none" d="M0 0H24V24H0z" />
<path d="M20 3c.552 0 1 .448 1 1v16c0 .552-.448 1-1 1h-6c-.552 0-1-.448-1-1V4c0-.552.448-1 1-1h6zm-1 2h-4v14h4V5zM6 7c2.761 0 5 2.239 5 5s-2.239 5-5 5-5-2.239-5-5 2.239-5 5-5zm1 2H5v1.999L3 11v2l2-.001V15h2v-2.001L9 13v-2l-2-.001V9z" />
</svg>
}
/>
);
};
export const IconAddColAfter: React.FC<{ style?: React.CSSProperties }> = ({ style = {} }) => {
return (
<Icon
style={style}
svg={
<svg width="1em" height="1em" fill="currentColor" viewBox="0 0 24 24">
<path fill="none" d="M0 0H24V24H0z" />
<path d="M10 3c.552 0 1 .448 1 1v16c0 .552-.448 1-1 1H4c-.552 0-1-.448-1-1V4c0-.552.448-1 1-1h6zM9 5H5v14h4V5zm9 2c2.761 0 5 2.239 5 5s-2.239 5-5 5-5-2.239-5-5 2.239-5 5-5zm1 2h-2v1.999L15 11v2l2-.001V15h2v-2.001L21 13v-2l-2-.001V9z" />
</svg>
}
/>
);
};
export const IconDeleteCol: React.FC<{ style?: React.CSSProperties }> = ({ style = {} }) => {
return (
<Icon
style={style}
svg={
<svg width="1em" height="1em" fill="currentColor" viewBox="0 0 24 24">
<path fill="none" d="M0 0H24V24H0z" />
<path d="M12 3c.552 0 1 .448 1 1v8c.835-.628 1.874-1 3-1 2.761 0 5 2.239 5 5s-2.239 5-5 5c-1.032 0-1.99-.313-2.787-.848L13 20c0 .552-.448 1-1 1H6c-.552 0-1-.448-1-1V4c0-.552.448-1 1-1h6zm-1 2H7v14h4V5zm8 10h-6v2h6v-2z" />
</svg>
}
/>
);
};

View File

@ -1,6 +1,7 @@
import { Icon } from '@douyinfe/semi-ui';
import React from 'react';
import { Icon } from '@douyinfe/semi-ui';
export const IconDocument: React.FC<{ style?: React.CSSProperties }> = ({ style = {} }) => {
return (
<Icon

View File

@ -1,6 +1,7 @@
import { Icon } from '@douyinfe/semi-ui';
import React from 'react';
import { Icon } from '@douyinfe/semi-ui';
export const IconDocumentFill: React.FC<{ style?: React.CSSProperties }> = ({ style = {} }) => {
return (
<Icon

View File

@ -0,0 +1,17 @@
import { Icon } from '@douyinfe/semi-ui';
export const IconLineHeight: React.FC<{ style?: React.CSSProperties }> = ({ style = {} }) => {
return (
<Icon
style={style}
svg={
<svg width="16" height="16" viewBox="0 0 24 24" role="presentation">
<path
d="M11 4H21V6H11V4ZM6 7V11H4V7H1L5 3L9 7H6ZM6 17H9L5 21L1 17H4V13H6V17ZM11 18H21V20H11V18ZM9 11H21V13H9V11Z"
fill="currentColor"
></path>
</svg>
}
/>
);
};

View File

@ -1,6 +1,7 @@
import { Icon } from '@douyinfe/semi-ui';
import React from 'react';
import { Icon } from '@douyinfe/semi-ui';
export const IconMessage: React.FC<{ style?: React.CSSProperties }> = ({ style = {} }) => {
return (
<Icon

View File

@ -1,6 +1,7 @@
import { Icon } from '@douyinfe/semi-ui';
import React from 'react';
import { Icon } from '@douyinfe/semi-ui';
export const IconOverview: React.FC<{ style?: React.CSSProperties }> = ({ style = {} }) => {
return (
<Icon

View File

@ -35,3 +35,17 @@ export const IconPDF: React.FC<{ style?: React.CSSProperties }> = ({ style = {}
/>
);
};
export const IconFilePDF: React.FC<{ style?: React.CSSProperties }> = ({ style = {} }) => {
return (
<Icon
style={style}
svg={
<svg viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em">
<path fill="none" d="M0 0h24v24H0z" />
<path d="M12 16H8V8h4a4 4 0 1 1 0 8zm-2-6v4h2a2 2 0 1 0 0-4h-2zm5-6H5v16h14V8h-4V4zM3 2.992C3 2.444 3.447 2 3.999 2H16l5 5v13.993A1 1 0 0 1 20.007 22H3.993A1 1 0 0 1 3 21.008V2.992z" />
</svg>
}
/>
);
};

View File

@ -0,0 +1,15 @@
import { Icon } from '@douyinfe/semi-ui';
export const IconFilePPT: React.FC<{ style?: React.CSSProperties }> = ({ style = {} }) => {
return (
<Icon
style={style}
svg={
<svg viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em">
<path fill="none" d="M0 0h24v24H0z" />
<path d="M3 2.992C3 2.444 3.447 2 3.999 2H16l5 5v13.993A1 1 0 0 1 20.007 22H3.993A1 1 0 0 1 3 21.008V2.992zM5 4v16h14V8h-3v6h-6v2H8V8h7V4H5zm5 6v2h4v-2h-4z" />
</svg>
}
/>
);
};

View File

@ -1,6 +1,7 @@
import { Icon } from '@douyinfe/semi-ui';
import React from 'react';
import { Icon } from '@douyinfe/semi-ui';
export const IconSearch: React.FC<{ style?: React.CSSProperties }> = ({ style = {} }) => {
return (
<Icon

View File

@ -1,6 +1,7 @@
import { Icon } from '@douyinfe/semi-ui';
import React from 'react';
import { Icon } from '@douyinfe/semi-ui';
export const IconSetting: React.FC<{ style?: React.CSSProperties }> = ({ style = {} }) => {
return (
<Icon

View File

@ -1,6 +1,7 @@
import { Icon } from '@douyinfe/semi-ui';
import React from 'react';
import { Icon } from '@douyinfe/semi-ui';
export const IconShare: React.FC<{ style?: React.CSSProperties }> = ({ style = {} }) => {
return (
<Icon

View File

@ -0,0 +1,15 @@
import { Icon } from '@douyinfe/semi-ui';
export const IconFileSheet: React.FC<{ style?: React.CSSProperties }> = ({ style = {} }) => {
return (
<Icon
style={style}
svg={
<svg viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em">
<path fill="none" d="M0 0h24v24H0z" />
<path d="M13.2 12l2.8 4h-2.4L12 13.714 10.4 16H8l2.8-4L8 8h2.4l1.6 2.286L13.6 8H15V4H5v16h14V8h-3l-2.8 4zM3 2.992C3 2.444 3.447 2 3.999 2H16l5 5v13.993A1 1 0 0 1 20.007 22H3.993A1 1 0 0 1 3 21.008V2.992z" />
</svg>
}
/>
);
};

View File

@ -35,3 +35,17 @@ export const IconWord: React.FC<{ style?: React.CSSProperties }> = ({ style = {}
/>
);
};
export const IconFileWord: React.FC<{ style?: React.CSSProperties }> = ({ style = {} }) => {
return (
<Icon
style={style}
svg={
<svg viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em">
<path fill="none" d="M0 0h24v24H0z" />
<path d="M16 8v8h-2l-2-2-2 2H8V8h2v5l2-2 2 2V8h1V4H5v16h14V8h-3zM3 2.992C3 2.444 3.447 2 3.999 2H16l5 5v13.993A1 1 0 0 1 20.007 22H3.993A1 1 0 0 1 3 21.008V2.992z" />
</svg>
}
/>
);
};

View File

@ -7,6 +7,7 @@ export * from './IconCallout';
export * from './IconCenter';
export * from './IconClear';
export * from './IconCodeBlock';
export * from './IconColumns';
export * from './IconCountdown';
export * from './IconDeleteColumn';
export * from './IconDeleteRow';
@ -29,6 +30,7 @@ export * from './IconInfo';
export * from './IconJSON';
export * from './IconLayout';
export * from './IconLeft';
export * from './IconLineHeight';
export * from './IconLink';
export * from './IconList';
export * from './IconMarkdown';
@ -44,12 +46,14 @@ export * from './IconMindSide';
export * from './IconOrderedList';
export * from './IconOverview';
export * from './IconPDF';
export * from './IconPPT';
export * from './IconQuote';
export * from './IconRight';
export * from './IconSearch';
export * from './IconSearchReplace';
export * from './IconSetting';
export * from './IconShare';
export * from './IconSheet';
export * from './IconSplitCell';
export * from './IconStatus';
export * from './IconStructure';

View File

@ -1,10 +1,12 @@
import React, { useCallback, useMemo, useState } from 'react';
import { LazyLoadImage } from 'react-lazy-load-image-component';
import { Button, ButtonGroup, Col, Popover, Row, SideSheet, Skeleton, Space, TabPane, Tabs } from '@douyinfe/semi-ui';
import { Upload } from 'components/upload';
import { chunk } from 'helpers/chunk';
import { IsOnMobile } from 'hooks/use-on-mobile';
import { useToggle } from 'hooks/use-toggle';
import React, { useCallback, useMemo, useState } from 'react';
import { LazyLoadImage } from 'react-lazy-load-image-component';
import styles from './index.module.scss';

View File

@ -1,4 +1,5 @@
import { useEffect } from 'react';
import Viewer from 'viewerjs';
interface IProps {

View File

@ -1,4 +1,5 @@
import React, { useEffect, useMemo, useRef, useState } from 'react';
import * as timeagojs from 'timeago.js';
type Props = {

View File

@ -1,4 +1,5 @@
import { Typography } from '@douyinfe/semi-ui';
import Link from 'next/link';
import styles from './index.module.scss';

View File

@ -1,7 +1,9 @@
import { Banner, Input, Popconfirm, Select, Space } from '@douyinfe/semi-ui';
import { AuthEnum, AuthEnumArray } from '@think/domains';
import React, { useCallback, useState } from 'react';
import { Banner, Input, Popconfirm, Select, Space } from '@douyinfe/semi-ui';
import { AuthEnum, AuthEnumArray } from '@think/domains';
interface IProps {
onOk: (arg) => any;
}

View File

@ -1,7 +1,9 @@
import { Banner, Popconfirm, Select, Toast } from '@douyinfe/semi-ui';
import { AuthEnum, AuthEnumArray, IAuth, IUser } from '@think/domains';
import React, { useCallback, useState } from 'react';
import { Banner, Popconfirm, Select, Toast } from '@douyinfe/semi-ui';
import { AuthEnum, AuthEnumArray, IAuth, IUser } from '@think/domains';
interface IProps {
userWithAuth: { user: IUser; auth: IAuth };
updateUser: (arg) => any;

View File

@ -1,12 +1,16 @@
import React from 'react';
import { IconDelete, IconEdit } from '@douyinfe/semi-icons';
import { Banner, Button, Popconfirm, Table, Typography } from '@douyinfe/semi-ui';
import { AuthEnumTextMap } from '@think/domains';
import { DataRender } from 'components/data-render';
import { LocaleTime } from 'components/locale-time';
import React from 'react';
import { AddUser } from './add';
import { EditUser } from './edit';
import styles from './index.module.scss';
interface IProps {

View File

@ -1,4 +1,7 @@
import React, { useCallback, useMemo } from 'react';
import { Badge, Button, Dropdown, Modal, Pagination, TabPane, Tabs, Typography } from '@douyinfe/semi-ui';
import { DataRender } from 'components/data-render';
import { Empty } from 'components/empty';
import { IconMessage } from 'components/icons/IconMessage';
@ -8,81 +11,83 @@ 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 { Placeholder } from './placeholder';
import styles from './index.module.scss';
import { Placeholder } from './placeholder';
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) => {
onClick && onClick(messageId);
};
const handleRead = useCallback(
(messageId) => {
onClick && onClick(messageId);
},
[onClick]
);
const renderNormalContent = useCallback(() => {
return (
<div
className={styles.itemsWrap}
style={{ margin: '8px -16px', minHeight: 224 }}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
}}
>
{messages.length ? (
<>
{messages.map((msg) => {
return (
<div key={msg.id} className={styles.itemWrap} onClick={() => handleRead(msg.id)}>
<Link href={msg.url}>
<a className={styles.item}>
<div className={styles.leftWrap}>
<Text
ellipsis={{
showTooltip: {
opts: { content: msg.message },
},
}}
style={{ width: 240 }}
>
{msg.title}
</Text>
</div>
</a>
</Link>
</div>
);
})}
{total > PAGE_SIZE && (
<div className={styles.paginationWrap}>
<Pagination
size="small"
total={total}
currentPage={page}
pageSize={PAGE_SIZE}
style={{ textAlign: 'center' }}
onPageChange={onPageChange}
/>
</div>
)}
</>
) : (
<Empty illustration={<EmptyBoxIllustration />} message="暂无消息" />
)}
</div>
);
}, [handleRead, messages, onPageChange, page, total]);
return (
<DataRender
loading={loading}
loadingContent={<Placeholder />}
error={error}
normalContent={() => {
return (
<div
className={styles.itemsWrap}
style={{ margin: '8px -16px', minHeight: 224 }}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
}}
>
{messages.length ? (
<>
{messages.map((msg) => {
return (
<div key={msg.id} className={styles.itemWrap} onClick={() => handleRead(msg.id)}>
<Link href={msg.url}>
<a className={styles.item}>
<div className={styles.leftWrap}>
<Text
ellipsis={{
showTooltip: {
opts: { content: msg.message },
},
}}
style={{ width: 240 }}
>
{msg.title}
</Text>
</div>
</a>
</Link>
</div>
);
})}
{total > PAGE_SIZE && (
<div className={styles.paginationWrap}>
<Pagination
size="small"
total={total}
currentPage={page}
pageSize={PAGE_SIZE}
style={{ textAlign: 'center' }}
onPageChange={onPageChange}
/>
</div>
)}
</>
) : (
<Empty illustration={<EmptyBoxIllustration />} message="暂无消息" />
)}
</div>
);
}}
/>
<DataRender loading={loading} loadingContent={<Placeholder />} error={error} normalContent={renderNormalContent} />
);
};
@ -106,60 +111,83 @@ 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 = (
<Tabs
type="line"
size="small"
tabBarExtraContent={
unreadMsgs && unreadMsgs.total > 0 ? (
<Text type="quaternary" onClick={clearAll} style={{ cursor: 'pointer' }}>
</Text>
) : null
}
>
<TabPane tab="未读" itemKey="unread">
<MessagesRender
messageData={unreadMsgs}
loading={unreadLoading}
error={unreadError}
onClick={readMessage}
page={unreadPage}
onPageChange={unreadSetPage}
/>
</TabPane>
<TabPane tab="已读" itemKey="read">
<MessagesRender
messageData={readMsgs}
loading={readLoading}
error={readError}
page={readPage}
onPageChange={readSetPage}
/>
</TabPane>
<TabPane tab="全部" itemKey="all">
<MessagesRender
messageData={allMsgs}
loading={allLoading}
error={allError}
page={allPage}
onPageChange={allSetPage}
/>
</TabPane>
</Tabs>
const content = useMemo(
() =>
visible ? (
<Tabs
type="line"
size="small"
tabBarExtraContent={
unreadMsgs && unreadMsgs.total > 0 ? (
<Text type="quaternary" onClick={clearAll} style={{ cursor: 'pointer' }}>
</Text>
) : null
}
>
<TabPane tab="未读" itemKey="unread">
<MessagesRender
messageData={unreadMsgs}
loading={unreadLoading}
error={unreadError}
onClick={readMessage}
page={unreadPage}
onPageChange={unreadSetPage}
/>
</TabPane>
<TabPane tab="已读" itemKey="read">
<MessagesRender
messageData={readMsgs}
loading={readLoading}
error={readError}
page={readPage}
onPageChange={readSetPage}
/>
</TabPane>
<TabPane tab="全部" itemKey="all">
<MessagesRender
messageData={allMsgs}
loading={allLoading}
error={allError}
page={allPage}
onPageChange={allSetPage}
/>
</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 +225,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 +240,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} />;
};

View File

@ -1,10 +1,13 @@
import React, { useCallback } from 'react';
import { IconDelete } from '@douyinfe/semi-icons';
import { Modal, Space, Typography } from '@douyinfe/semi-ui';
import { IOrganization } from '@think/domains';
import { useOrganizationDetail } from 'data/organization';
import { useRouterQuery } from 'hooks/use-router-query';
import Router from 'next/router';
import React, { useCallback } from 'react';
interface IProps {
organizationId: IOrganization['id'];

View File

@ -1,5 +1,6 @@
import { Typography } from '@douyinfe/semi-ui';
import { Avatar } from '@douyinfe/semi-ui';
import { DataRender } from 'components/data-render';
import { useOrganizationDetail } from 'data/organization';
import { useRouterQuery } from 'hooks/use-router-query';

View File

@ -1,9 +1,11 @@
import { Space } from '@douyinfe/semi-ui';
import { LogoImage, LogoText } from 'components/logo';
import { useUser } from 'data/user';
import { useWindowSize } from 'hooks/use-window-size';
import { UserOrganizationsSwitcher } from '../switcher';
import styles from './index.module.scss';
export const OrganizationPublicSwitcher = () => {

View File

@ -1,14 +1,15 @@
import { Avatar, Button, Form, Toast, Typography } from '@douyinfe/semi-ui';
import { useCallback, useEffect, useRef, useState } from 'react';
import { Avatar, Button, Form, Toast } from '@douyinfe/semi-ui';
import { FormApi } from '@douyinfe/semi-ui/lib/es/form';
import { ORGANIZATION_LOGOS } from '@think/constants';
import { IOrganization } from '@think/domains';
import { DataRender } from 'components/data-render';
import { ImageUploader } from 'components/image-uploader';
import { useCreateOrganization, useOrganizationDetail } from 'data/organization';
import { useOrganizationDetail } from 'data/organization';
import { useToggle } from 'hooks/use-toggle';
import { SingleColumnLayout } from 'layouts/single-column';
import Router from 'next/router';
import { useCallback, useEffect, useRef, useState } from 'react';
import styles from './index.module.scss';

View File

@ -1,8 +1,11 @@
import { TabPane, Tabs } from '@douyinfe/semi-ui';
import { IOrganization } from '@think/domains';
import { Seo } from 'components/seo';
import React from 'react';
import { TabPane, Tabs } from '@douyinfe/semi-ui';
import { IOrganization } from '@think/domains';
import { Seo } from 'components/seo';
import { Base } from './base';
import { OrganizationMembers } from './members';
import { More } from './more';

View File

@ -1,7 +1,9 @@
import React from 'react';
import { IOrganization } from '@think/domains';
import { Members } from 'components/members';
import { useOrganizationMembers } from 'data/organization';
import React from 'react';
interface IProps {
organizationId: IOrganization['id'];

View File

@ -1,4 +1,5 @@
import { Banner, Button, Typography } from '@douyinfe/semi-ui';
import { OrganizationDeletor } from 'components/organization/delete';
const { Paragraph } = Typography;

View File

@ -1,11 +1,13 @@
import { useMemo } from 'react';
import { IconAppCenter, IconApps, IconSmallTriangleDown } from '@douyinfe/semi-icons';
import { Button, Dropdown, Space, Typography } from '@douyinfe/semi-ui';
import { Avatar } from '@douyinfe/semi-ui';
import { DataRender } from 'components/data-render';
import { useOrganizationDetail, useUserOrganizations } from 'data/organization';
import { useRouterQuery } from 'hooks/use-router-query';
import Link from 'next/link';
import { useMemo } from 'react';
import styles from './index.module.scss';

View File

@ -1,7 +1,8 @@
import React, { useEffect, useRef } from 'react';
import cls from 'classnames';
import { useClickOutside } from 'hooks/use-click-outside';
import interact from 'interactjs';
import React, { useEffect, useRef } from 'react';
import styles from './style.module.scss';

View File

@ -1,6 +1,10 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { IconSearch as SemiIconSearch } from '@douyinfe/semi-icons';
import { Button, Dropdown, Input, Modal, Spin, Typography } from '@douyinfe/semi-ui';
import { IDocument } from '@think/domains';
import { DataRender } from 'components/data-render';
import { DocumentStar } from 'components/document/star';
import { Empty } from 'components/empty';
@ -14,7 +18,6 @@ import { useRouterQuery } from 'hooks/use-router-query';
import { useToggle } from 'hooks/use-toggle';
import Link from 'next/link';
import Router from 'next/router';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { HttpClient } from 'services/http-client';
import styles from './index.module.scss';

View File

@ -1,6 +1,7 @@
import { useCallback, useRef } from 'react';
import { Button, Dropdown, Form } from '@douyinfe/semi-ui';
import { FormApi } from '@douyinfe/semi-ui/lib/es/form';
import { useCallback, useRef } from 'react';
type ISize = { width: number | string; height: number | string };
@ -11,6 +12,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 +31,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 +42,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>
);
};

View File

@ -1,13 +1,16 @@
import { useCallback } from 'react';
import { IconEdit, IconPlus, IconUser } from '@douyinfe/semi-icons';
import { Avatar, Button, Modal, Skeleton, Space, Tooltip, Typography } from '@douyinfe/semi-ui';
import type { ITemplate } from '@think/domains';
import cls from 'classnames';
import { IconDocument } from 'components/icons/IconDocument';
import { TemplateReader } from 'components/template/reader';
import { useUser } from 'data/user';
import { useToggle } from 'hooks/use-toggle';
import Router from 'next/router';
import { useCallback } from 'react';
import styles from './index.module.scss';
@ -21,6 +24,17 @@ export interface IProps {
onClosePreview?: () => void;
}
const bodyStyle = {
overflow: 'auto',
};
const titleContainerStyle = {
marginBottom: 12,
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
overflow: 'hidden',
} as React.CSSProperties;
const flexStyle = { display: 'flex' };
export const TemplateCard: React.FC<IProps> = ({
template,
onClick,
@ -35,26 +49,35 @@ export const TemplateCard: React.FC<IProps> = ({
Router.push(`/template/${template.id}/`);
}, [template]);
const cancel = useCallback(() => {
toggleVisible(false);
onClosePreview && onClosePreview();
}, [toggleVisible, onClosePreview]);
const preview = useCallback(() => {
toggleVisible(true);
onOpenPreview && onOpenPreview();
}, [toggleVisible, onOpenPreview]);
const useTemplate = useCallback(() => {
onClick && onClick(template.id);
}, [onClick, template.id]);
return (
<>
<Modal
title="模板预览"
width={'calc(100vh - 120px)'}
height={'calc(100vh - 120px)'}
bodyStyle={{
overflow: 'auto',
}}
bodyStyle={bodyStyle}
visible={visible}
onCancel={() => {
toggleVisible(false);
onClosePreview && onClosePreview();
}}
onCancel={cancel}
footer={null}
fullScreen
>
<TemplateReader key={template.id} templateId={template.id} />
</Modal>
<div className={cls(styles.cardWrap, getClassNames(template.id))}>
<div className={cls(styles.cardWrap, getClassNames(template.id))} onClick={useTemplate}>
<header>
<IconDocument />
<div className={styles.rightWrap}>
@ -68,14 +91,7 @@ export const TemplateCard: React.FC<IProps> = ({
</div>
</header>
<main>
<div
style={{
marginBottom: 12,
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
overflow: 'hidden',
}}
>
<div style={titleContainerStyle}>
<Text strong>{template.title}</Text>
</div>
<div>
@ -92,28 +108,16 @@ export const TemplateCard: React.FC<IProps> = ({
</main>
<footer>
<Text type="tertiary" size="small">
<div style={{ display: 'flex' }}>
<div style={flexStyle}>
使
{template.usageAmount}
</div>
</Text>
</footer>
<div className={styles.actions}>
<Button
theme="solid"
type="tertiary"
onClick={() => {
toggleVisible(true);
onOpenPreview && onOpenPreview();
}}
>
<Button theme="solid" type="tertiary" onClick={preview}>
</Button>
{onClick && (
<Button type="primary" theme="solid" onClick={() => onClick && onClick(template.id)}>
使
</Button>
)}
</div>
</div>
</>

View File

@ -1,5 +1,10 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { IconChevronLeft } from '@douyinfe/semi-icons';
import { Button, Nav, Popconfirm, Space, Switch, Tooltip, Typography } from '@douyinfe/semi-ui';
import { CollaborationEditor } from 'tiptap/editor';
import { DocumentStyle } from 'components/document/style';
import { Seo } from 'components/seo';
import { Theme } from 'components/theme';
@ -10,8 +15,6 @@ import { useDocumentStyle } from 'hooks/use-document-style';
import { useMount } from 'hooks/use-mount';
import { useWindowSize } from 'hooks/use-window-size';
import Router from 'next/router';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { CollaborationEditor } from 'tiptap/editor';
import styles from './index.module.scss';

View File

@ -1,8 +1,10 @@
import React, { useEffect } from 'react';
import { List, Pagination } from '@douyinfe/semi-ui';
import { DataRender } from 'components/data-render';
import { Empty } from 'components/empty';
import { IProps as ITemplateCardProps, TemplateCard, TemplateCardPlaceholder } from 'components/template/card';
import React, { useEffect, useMemo, useState } from 'react';
const grid = {
gutter: 16,
@ -29,15 +31,9 @@ export const TemplateList: React.FC<IProps> = ({
onClosePreview,
pageSize = 5,
}) => {
const { data, loading, error, refresh } = hook();
const [page, onPageChange] = useState(1);
const arr = useMemo(() => {
const arr = (data && data.data) || [];
const start = (page - 1) * pageSize;
const end = page * pageSize;
return arr.slice(start, end);
}, [data, page, pageSize]);
const { data, loading, error, page, setPage, refresh } = hook(pageSize);
const list = (data && data.data) || [];
const total = (data && data.total) || 0;
useEffect(() => {
refresh();
@ -62,7 +58,7 @@ export const TemplateList: React.FC<IProps> = ({
<>
<List
grid={grid}
dataSource={firstListItem ? [{}, ...arr] : arr}
dataSource={firstListItem ? [{}, ...list] : list}
renderItem={(template, idx) => {
if (idx === 0 && firstListItem) {
return <List.Item>{firstListItem}</List.Item>;
@ -82,7 +78,7 @@ export const TemplateList: React.FC<IProps> = ({
}}
emptyContent={<Empty message={'暂无模板'} />}
></List>
{data.data.length > pageSize ? (
{total > pageSize ? (
<Pagination
size="small"
style={{
@ -91,9 +87,9 @@ export const TemplateList: React.FC<IProps> = ({
justifyContent: 'center',
}}
pageSize={pageSize}
total={data.data.length}
total={total}
currentPage={page}
onChange={(cPage) => onPageChange(cPage)}
onChange={(cPage) => setPage(cPage)}
/>
) : null}
</>

View File

@ -1,9 +1,12 @@
import React from 'react';
import { Spin } from '@douyinfe/semi-ui';
import { ReaderEditor } from 'tiptap/editor';
import { DataRender } from 'components/data-render';
import { Seo } from 'components/seo';
import { useTemplate } from 'data/template';
import React from 'react';
import { ReaderEditor } from 'tiptap/editor';
interface IProps {
templateId: string;

View File

@ -1,7 +1,9 @@
import React, { useCallback } from 'react';
import { IconDesktop, IconMoon, IconSun } from '@douyinfe/semi-icons';
import { Button, Dropdown } from '@douyinfe/semi-ui';
import { Theme as ThemeState, ThemeEnum } from 'hooks/use-theme';
import React, { useCallback } from 'react';
export const Theme = () => {
const { userPrefer, theme, toggle } = ThemeState.useHook();

View File

@ -1,7 +1,9 @@
import React from 'react';
import { Tooltip as SemiTooltip } from '@douyinfe/semi-ui';
import { Position } from '@douyinfe/semi-ui/tooltip';
import { useToggle } from 'hooks/use-toggle';
import React from 'react';
interface IProps {
content: React.ReactNode;

View File

@ -1,7 +1,9 @@
import React from 'react';
import { IconUpload } from '@douyinfe/semi-icons';
import { Button, Toast, Upload as SemiUpload } from '@douyinfe/semi-ui';
import { useAsyncLoading } from 'hooks/use-async-loading';
import React from 'react';
import { uploadFile } from 'services/file';
interface IProps {

View File

@ -1,9 +1,11 @@
import React, { useCallback } from 'react';
import { IconSpin } from '@douyinfe/semi-icons';
import { Avatar, Button, Dropdown, Modal, Toast, Typography } from '@douyinfe/semi-ui';
import { useUser } from 'data/user';
import { useToggle } from 'hooks/use-toggle';
import Router from 'next/router';
import React, { useCallback } from 'react';
import { ResetPassword } from './reset-password';
import { UserSetting } from './setting';

View File

@ -1,8 +1,10 @@
import React, { useCallback, useState } from 'react';
import { Button, Col, Form, Row, Toast } from '@douyinfe/semi-ui';
import { useResetPassword, useSystemPublicConfig, useUser, useVerifyCode } from 'data/user';
import { useInterval } from 'hooks/use-interval';
import { useToggle } from 'hooks/use-toggle';
import React, { useCallback, useState } from 'react';
export const ResetPassword = ({ onSuccess }) => {
const [email, setEmail] = useState('');
@ -53,7 +55,7 @@ export const ResetPassword = ({ onSuccess }) => {
return (
<Form
initValues={{ email: user.email, password: '', confirmPassword: '' }}
initValues={{ email: user ? user.email : '', password: '', confirmPassword: '' }}
onChange={onFormChange}
onSubmit={onFinish}
>

View File

@ -1,10 +1,12 @@
import { Dispatch, SetStateAction, useCallback, useEffect, useRef, useState } from 'react';
import { Avatar, Button, Col, Form, Modal, Row, Space, Toast } from '@douyinfe/semi-ui';
import { FormApi } from '@douyinfe/semi-ui/lib/es/form';
import { Upload } from 'components/upload';
import { useSystemPublicConfig, useUser, useVerifyCode } from 'data/user';
import { useInterval } from 'hooks/use-interval';
import { useToggle } from 'hooks/use-toggle';
import { Dispatch, SetStateAction, useCallback, useEffect, useRef, useState } from 'react';
interface IProps {
visible: boolean;

View File

@ -1,7 +1,9 @@
import React from 'react';
import { Button } from '@douyinfe/semi-ui';
import { WikiCreator as WikiCreatorForm } from 'components/wiki/create';
import { useToggle } from 'hooks/use-toggle';
import React from 'react';
export const WikiCreator: React.FC = ({ children }) => {
const [visible, toggleVisible] = useToggle(false);

View File

@ -1,10 +1,12 @@
import React from 'react';
import { IconPlus } from '@douyinfe/semi-icons';
import { Button, Dropdown } from '@douyinfe/semi-ui';
import { DocumentCreator } from 'components/document/create';
import { WikiCreator } from 'components/wiki/create';
import { useRouterQuery } from 'hooks/use-router-query';
import { useToggle } from 'hooks/use-toggle';
import React from 'react';
interface IProps {
onCreateDocument?: () => void;

View File

@ -1,5 +1,6 @@
import { IconUser } from '@douyinfe/semi-icons';
import { Avatar, Skeleton, Space, Typography } from '@douyinfe/semi-ui';
import { IconDocument } from 'components/icons/IconDocument';
import { LocaleTime } from 'components/locale-time';
import { WikiStar } from 'components/wiki/star';

View File

@ -1,10 +1,13 @@
import { Dispatch, SetStateAction, useRef } from 'react';
import { Form, Modal } from '@douyinfe/semi-ui';
import { FormApi } from '@douyinfe/semi-ui/lib/es/form';
import type { IWiki } from '@think/domains';
import { ICreateWiki, useOwnWikis } from 'data/wiki';
import { useRouterQuery } from 'hooks/use-router-query';
import Router from 'next/router';
import { Dispatch, SetStateAction, useRef } from 'react';
interface IProps {
visible: boolean;

View File

@ -1,9 +1,11 @@
import React, { useCallback } from 'react';
import { IconDelete } from '@douyinfe/semi-icons';
import { Modal, Space, Typography } from '@douyinfe/semi-ui';
import { useOwnWikis } from 'data/wiki';
import { useRouterQuery } from 'hooks/use-router-query';
import Router from 'next/router';
import React, { useCallback } from 'react';
interface IProps {
wikiId: string;

View File

@ -1,9 +1,12 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { IconClose } from '@douyinfe/semi-icons';
import { Banner, Button, Checkbox, Toast, Transfer, Typography } from '@douyinfe/semi-ui';
import { isPublicDocument } from '@think/domains';
import { flattenTree2Array } from 'components/wiki/tocs/utils';
import { useWikiDetail, useWikiTocs } from 'data/wiki';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import styles from './index.module.scss';

View File

@ -1,5 +1,7 @@
import { Avatar, Skeleton, Space, Typography } from '@douyinfe/semi-ui';
import { IWiki } from '@think/domains';
import { IconDocument } from 'components/icons/IconDocument';
import { LocaleTime } from 'components/locale-time';
import { WikiStar } from 'components/wiki/star';

View File

@ -1,6 +1,7 @@
import { Skeleton } from '@douyinfe/semi-ui';
import React from 'react';
import { Skeleton } from '@douyinfe/semi-ui';
export const WorkspacePlaceholder = () => {
const placeholder = (
<div

View File

@ -1,9 +1,13 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { Avatar, Button, Form, Toast } from '@douyinfe/semi-ui';
import { FormApi } from '@douyinfe/semi-ui/lib/es/form';
import { WIKI_AVATARS } from '@think/constants';
import type { IWiki } from '@think/domains';
import { ImageUploader } from 'components/image-uploader';
import { useEffect, useRef, useState } from 'react';
import { pick } from 'helpers/pick';
import styles from './index.module.scss';
@ -22,32 +26,36 @@ interface IProps {
update: (arg: IUpdateWIKI) => Promise<void>;
}
const getFormValueFromWiki = (wiki) => {
return pick(wiki, ['name', 'description', 'avatar']);
};
export const Base: React.FC<IProps> = ({ wiki, update }) => {
const $form = useRef<FormApi>();
const [currentCover, setCurrentCover] = useState('');
const onSubmit = () => {
const onSubmit = useCallback(() => {
$form.current.validate().then((values) => {
update(values).then(() => {
Toast.success('操作成功');
});
});
};
}, [update]);
const setCover = (url) => {
const setCover = useCallback((url) => {
$form.current.setValue('avatar', url);
setCurrentCover(url);
};
}, []);
useEffect(() => {
if (!wiki) return;
$form.current.setValues(wiki);
$form.current.setValues(getFormValueFromWiki(wiki));
setCurrentCover(wiki.avatar);
}, [wiki]);
return (
<Form
initValues={wiki}
initValues={getFormValueFromWiki(wiki)}
style={{ width: '100%' }}
getFormApi={(formApi) => ($form.current = formApi)}
onSubmit={onSubmit}

View File

@ -1,9 +1,12 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { Button, Toast, Typography, Upload } from '@douyinfe/semi-ui';
import type { IWiki } from '@think/domains';
import { useCreateDocument } from 'data/document';
import { useRouterQuery } from 'hooks/use-router-query';
import { useToggle } from 'hooks/use-toggle';
import { useCallback, useEffect, useRef, useState } from 'react';
import { createMarkdownParser, MarkdownParse } from './parser';
@ -94,7 +97,7 @@ export const Import: React.FC<IProps> = ({ wikiId }) => {
<div style={{ marginTop: 16 }}>
<Upload
action=""
accept="text/markdown"
accept=".md,.MD,.Md,.mD"
draggable
multiple
ref={$upload}

View File

@ -1,10 +1,12 @@
import { Toast } from '@douyinfe/semi-ui';
import { safeJSONStringify } from 'helpers/json';
import { createEditor } from 'tiptap/core';
import { AllExtensions } from 'tiptap/core/all-kit';
import { Collaboration } from 'tiptap/core/extensions/collaboration';
import { prosemirrorJSONToYDoc } from 'tiptap/core/thritypart/y-prosemirror/y-prosemirror';
import { markdownToProsemirror } from 'tiptap/markdown/markdown-to-prosemirror';
import { safeJSONStringify } from 'helpers/json';
import * as Y from 'yjs';
export interface MarkdownParse {
@ -23,6 +25,7 @@ export const createMarkdownParser = () => {
const parse = (filename: string, markdown: string) => {
try {
const prosemirrorNode = markdownToProsemirror({
editor,
schema: editor.schema,
content: markdown,
needTitle: true,

View File

@ -1,9 +1,12 @@
import React from 'react';
import { TabPane, Tabs } from '@douyinfe/semi-ui';
import { IWiki } from '@think/domains';
import { Seo } from 'components/seo';
import { WikiTocsManager } from 'components/wiki/tocs/manager';
import { useWikiDetail } from 'data/wiki';
import React from 'react';
import { Base } from './base';
import { Import } from './import';

View File

@ -1,4 +1,5 @@
import { Banner, Button, Typography } from '@douyinfe/semi-ui';
import { WikiDeletor } from 'components/wiki/delete';
interface IProps {

View File

@ -1,10 +1,13 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { IconClose } from '@douyinfe/semi-icons';
import { Banner, Button, Checkbox, Radio, RadioGroup, Toast, Transfer, Typography } from '@douyinfe/semi-ui';
import { isPublicDocument, isPublicWiki, WIKI_STATUS_LIST } from '@think/domains';
import { flattenTree2Array } from 'components/wiki/tocs/utils';
import { useWikiDetail, useWikiTocs } from 'data/wiki';
import { buildUrl } from 'helpers/url';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import styles from './index.module.scss';

View File

@ -1,6 +1,7 @@
import React from 'react';
import { Members } from 'components/members';
import { useWikiMembers } from 'data/wiki';
import React from 'react';
interface IProps {
wikiId: string;

View File

@ -1,8 +1,11 @@
import React from 'react';
import { IconStar } from '@douyinfe/semi-icons';
import { Button, Tooltip } from '@douyinfe/semi-ui';
import { IOrganization, IWiki } from '@think/domains';
import { useWikiStarToggle } from 'data/star';
import React from 'react';
interface IProps {
organizationId: IOrganization['id'];

View File

@ -48,6 +48,7 @@
font-size: 14px;
cursor: pointer;
align-items: center;
color: var(--semi-color-text-0);
> span {
margin-right: 6px;

View File

@ -1,34 +1,38 @@
import { useMemo } from 'react';
import { IconPlus, IconSmallTriangleDown } from '@douyinfe/semi-icons';
import { Avatar, Button, Dropdown, Skeleton, Typography } from '@douyinfe/semi-ui';
import { IDocument } from '@think/domains';
import cls from 'classnames';
import { DataRender } from 'components/data-render';
import { IconOverview, IconSetting } from 'components/icons';
import { findParents } from 'components/wiki/tocs/utils';
import { useStarDocumentsInWiki, useStarWikisInOrganization } from 'data/star';
import { useWikiDetail, useWikiTocs } from 'data/wiki';
import { triggerCreateDocument } from 'event';
import Link from 'next/link';
import { useRouter } from 'next/router';
import { useEffect, useMemo, useState } from 'react';
import { Tree } from './tree';
import styles from './index.module.scss';
import { Tree } from './tree';
interface IProps {
wikiId: string;
documentId?: string;
docAsLink?: string;
getDocLink?: (arg: IDocument) => string;
}
const { Text } = Typography;
const defaultGetDocLink = (document) =>
`/app/org/${document.organizationId}/wiki/${document.wikiId}/doc/${document.id}`;
export const WikiTocs: React.FC<IProps> = ({
wikiId,
documentId = null,
docAsLink = '/app/org/[organizationId]/wiki/[wikiId]/doc/[documentId]',
getDocLink = (document) => `/app/org/${document.organizationId}/wiki/${document.wikiId}/doc/${document.id}`,
getDocLink = defaultGetDocLink,
}) => {
const { pathname, query } = useRouter();
const { data: wiki, loading: wikiLoading, error: wikiError } = useWikiDetail(wikiId);
@ -39,15 +43,8 @@ export const WikiTocs: React.FC<IProps> = ({
loading: starDocumentsLoading,
error: starDocumentsError,
} = useStarDocumentsInWiki(query.organizationId, wikiId);
const [parentIds, setParentIds] = useState<Array<string>>([]);
const otherStarWikis = useMemo(() => (starWikis || []).filter((wiki) => wiki.id !== wikiId), [starWikis, wikiId]);
useEffect(() => {
if (!tocs || !tocs.length) return;
const parentIds = findParents(tocs, documentId);
setParentIds(parentIds);
}, [tocs, documentId]);
return (
<div className={styles.wrap}>
<header>
@ -139,7 +136,9 @@ export const WikiTocs: React.FC<IProps> = ({
</Avatar>
<Text strong>{wiki.name}</Text>
</span>
<IconSmallTriangleDown />
<Text>
<IconSmallTriangleDown />
</Text>
</div>
</Dropdown>
) : (
@ -276,15 +275,7 @@ export const WikiTocs: React.FC<IProps> = ({
<DataRender
loading={starDocumentsLoading}
error={starDocumentsError}
normalContent={() => (
<Tree
data={starDocuments || []}
docAsLink={docAsLink}
getDocLink={getDocLink}
parentIds={parentIds}
activeId={documentId}
/>
)}
normalContent={() => <Tree data={starDocuments || []} docAsLink={docAsLink} getDocLink={getDocLink} />}
/>
</div>
@ -315,14 +306,7 @@ export const WikiTocs: React.FC<IProps> = ({
loading={tocsLoading}
error={tocsError}
normalContent={() => (
<Tree
needAddDocument
data={tocs || []}
docAsLink={docAsLink}
getDocLink={getDocLink}
parentIds={parentIds}
activeId={documentId}
/>
<Tree needAddDocument data={tocs || []} docAsLink={docAsLink} getDocLink={getDocLink} />
)}
/>
</div>

Some files were not shown because too many files have changed in this diff Show More