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 # 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 是一款开源知识管理工具。通过独立的知识库空间,结构化地组织在线协作文档,实现知识的积累与沉淀,促进知识的复用与流通。同时支持多人协作文档。使用的技术如下: Think 是一款开源知识管理工具。通过独立的知识库空间,结构化地组织在线协作文档,实现知识的积累与沉淀,促进知识的复用与流通。同时支持多人协作文档。使用的技术如下:
@ -15,12 +31,6 @@ Think 是一款开源知识管理工具。通过独立的知识库空间,结
[云策文档](https://think.codingit.cn)已经部署上线,可前往注册使用。 [云策文档](https://think.codingit.cn)已经部署上线,可前往注册使用。
## 交流群
欢迎进群交流。
<img width="300" alt="image" src="https://user-images.githubusercontent.com/26452939/184578110-62b49297-da6f-4623-945a-3a03550d924f.PNG">
## 预览 ## 预览
<details> <details>

View File

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

View File

@ -54,7 +54,21 @@ module.exports = {
'react/react-in-jsx-scope': 'off', 'react/react-in-jsx-scope': 'off',
'react/prop-types': 'off', 'react/prop-types': 'off',
'@typescript-eslint/explicit-function-return-type': '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', 'simple-import-sort/exports': 'error',
}, },
ignorePatterns: ['dist/', 'node_modules', 'scripts', 'examples'], ignorePatterns: ['dist/', 'node_modules', 'scripts', 'examples'],

View File

@ -8,11 +8,14 @@
"pm2": "pm2 start npm --name @think/client -- start" "pm2": "pm2 start npm --name @think/client -- start"
}, },
"dependencies": { "dependencies": {
"@douyinfe/semi-icons": "^2.3.1", "@douyinfe/semi-icons": "^2.18.0",
"@douyinfe/semi-next": "^2.3.1", "@douyinfe/semi-next": "^2.18.0",
"@douyinfe/semi-ui": "^2.3.1", "@douyinfe/semi-ui": "^2.18.0",
"@excalidraw/excalidraw": "^0.12.0", "@excalidraw/excalidraw": "^0.12.0",
"@hocuspocus/provider": "^1.0.0-alpha.29", "@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/config": "workspace:^1.0.0",
"@think/constants": "workspace:^1.0.0", "@think/constants": "workspace:^1.0.0",
"@think/domains": "workspace:^1.0.0", "@think/domains": "workspace:^1.0.0",
@ -60,15 +63,14 @@
"clone": "^2.1.2", "clone": "^2.1.2",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"deep-equal": "^2.0.5", "deep-equal": "^2.0.5",
"docx": "^7.3.0",
"dompurify": "^2.3.5", "dompurify": "^2.3.5",
"downloadjs": "^1.4.7", "file-saver": "^2.0.5",
"html-to-docx": "^1.4.0",
"htmldiff-js": "^1.0.5", "htmldiff-js": "^1.0.5",
"interactjs": "^1.10.11", "interactjs": "^1.10.11",
"katex": "^0.15.2", "katex": "^0.15.2",
"kity": "^2.0.4", "kity": "^2.0.4",
"lib0": "^0.2.47", "lib0": "^0.2.47",
"lodash.pick": "^4.4.0",
"lowlight": "^2.5.0", "lowlight": "^2.5.0",
"markdown-it": "^12.3.2", "markdown-it": "^12.3.2",
"markdown-it-anchor": "^8.4.1", "markdown-it-anchor": "^8.4.1",
@ -78,6 +80,7 @@
"markdown-it-sup": "^1.0.0", "markdown-it-sup": "^1.0.0",
"next": "12.1.0", "next": "12.1.0",
"next-pwa": "^5.5.2", "next-pwa": "^5.5.2",
"pdfjs-dist": "3.1.81",
"prosemirror-codemark": "^0.3.6", "prosemirror-codemark": "^0.3.6",
"prosemirror-commands": "^1.3.0", "prosemirror-commands": "^1.3.0",
"prosemirror-markdown": "^1.7.0", "prosemirror-markdown": "^1.7.0",
@ -93,7 +96,6 @@
"react-full-screen": "^1.1.1", "react-full-screen": "^1.1.1",
"react-helmet": "^6.1.0", "react-helmet": "^6.1.0",
"react-lazy-load-image-component": "^1.5.4", "react-lazy-load-image-component": "^1.5.4",
"react-pdf": "^5.7.2",
"react-query": "^3.39.0", "react-query": "^3.39.0",
"react-split-pane": "^0.1.92", "react-split-pane": "^0.1.92",
"react-visibility-sensor": "^5.1.1", "react-visibility-sensor": "^5.1.1",
@ -110,6 +112,7 @@
"devDependencies": { "devDependencies": {
"@types/node": "17.0.13", "@types/node": "17.0.13",
"@types/react": "17.0.38", "@types/react": "17.0.38",
"@types/react-lazy-load-image-component": "^1.6.3",
"@typescript-eslint/eslint-plugin": "^5.21.0", "@typescript-eslint/eslint-plugin": "^5.21.0",
"@typescript-eslint/parser": "^5.21.0", "@typescript-eslint/parser": "^5.21.0",
"copy-webpack-plugin": "11.0.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 React from 'react';
import { TabPane, Tabs } from '@douyinfe/semi-ui';
import { Mail } from './mail'; import { Mail } from './mail';
import { System } from './system'; 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 { Banner, Button, Form, Toast } from '@douyinfe/semi-ui';
import { DataRender } from 'components/data-render'; import { DataRender } from 'components/data-render';
import { useSystemConfig } from 'data/user'; import { useSystemConfig } from 'data/user';
import { useToggle } from 'hooks/use-toggle'; import { useToggle } from 'hooks/use-toggle';
import React, { useCallback } from 'react';
export const Mail = () => { export const Mail = () => {
const { data, loading, error, sendTestEmail, updateSystemConfig } = useSystemConfig(); 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 { IconHelpCircle } from '@douyinfe/semi-icons';
import { Banner, Button, Form, Toast, Tooltip } from '@douyinfe/semi-ui'; import { Banner, Button, Form, Toast, Tooltip } from '@douyinfe/semi-ui';
import { DataRender } from 'components/data-render'; import { DataRender } from 'components/data-render';
import { useSystemConfig } from 'data/user'; import { useSystemConfig } from 'data/user';
import { useToggle } from 'hooks/use-toggle'; import { useToggle } from 'hooks/use-toggle';
import React, { useCallback } from 'react';
export const System = () => { export const System = () => {
const { data, loading, error, updateSystemConfig } = useSystemConfig(); 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 { IconClose } from '@douyinfe/semi-icons';
import { Banner as SemiBanner } from '@douyinfe/semi-ui'; import { Banner as SemiBanner } from '@douyinfe/semi-ui';
import { BannerProps } from '@douyinfe/semi-ui/banner'; import { BannerProps } from '@douyinfe/semi-ui/banner';
import { useToggle } from 'hooks/use-toggle'; import { useToggle } from 'hooks/use-toggle';
import React, { useEffect, useRef } from 'react';
interface IProps extends BannerProps { interface IProps extends BannerProps {
duration?: number; duration?: number;

View File

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

View File

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

View File

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

View File

@ -1,6 +1,7 @@
import { useToggle } from 'hooks/use-toggle';
import React, { useEffect, useRef } from 'react'; import React, { useEffect, useRef } from 'react';
import { useToggle } from 'hooks/use-toggle';
import { Render } from './constant'; import { Render } from './constant';
export const LoadingWrap = ({ loading, delay = 200, loadingContent, normalContent }) => { 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 { Button } from '@douyinfe/semi-ui';
import { DocumentCreator as DocumenCreatorForm } from 'components/document/create'; import { DocumentCreator as DocumenCreatorForm } from 'components/document/create';
import { useRouterQuery } from 'hooks/use-router-query'; import { useRouterQuery } from 'hooks/use-router-query';
import { useToggle } from 'hooks/use-toggle'; import { useToggle } from 'hooks/use-toggle';
import React from 'react';
interface IProps { interface IProps {
onCreateDocument?: () => void; 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 { IconArticle, IconBranch, IconExport, IconHistory, IconMore, IconPlus, IconStar } from '@douyinfe/semi-icons';
import { Button, Dropdown, Space, Typography } from '@douyinfe/semi-ui'; import { Button, Dropdown, Space, Typography } from '@douyinfe/semi-ui';
import { ButtonProps } from '@douyinfe/semi-ui/button/Button'; import { ButtonProps } from '@douyinfe/semi-ui/button/Button';
import { IDocument, IOrganization, IWiki } from '@think/domains'; import { IDocument, IOrganization, IWiki } from '@think/domains';
import cls from 'classnames'; import cls from 'classnames';
import { DocumentCreator } from 'components/document/create'; import { DocumentCreator } from 'components/document/create';
import { DocumentDeletor } from 'components/document/delete'; import { DocumentDeletor } from 'components/document/delete';
@ -12,7 +16,6 @@ import { DocumentStar } from 'components/document/star';
import { DocumentStyle } from 'components/document/style'; import { DocumentStyle } from 'components/document/style';
import { DocumentVersionTrigger } from 'components/document/version'; import { DocumentVersionTrigger } from 'components/document/version';
import { useToggle } from 'hooks/use-toggle'; import { useToggle } from 'hooks/use-toggle';
import React, { useCallback } from 'react';
import styles from './index.module.scss'; import styles from './index.module.scss';
@ -78,6 +81,7 @@ export const DocumentActions: React.FC<IProps> = ({
position="bottomLeft" position="bottomLeft"
visible={popoverVisible} visible={popoverVisible}
onVisibleChange={wrapOnVisibleChange} onVisibleChange={wrapOnVisibleChange}
stopPropagation={true}
content={ content={
<Dropdown.Menu style={{ minWidth: 112 }}> <Dropdown.Menu style={{ minWidth: 112 }}>
{showCreateDocument && ( {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 && ( {document && (
<DocumentExporter <DocumentExporter
document={document} document={document}

View File

@ -1,13 +1,16 @@
import { useCallback } from 'react';
import { IconEdit, IconUser } from '@douyinfe/semi-icons'; import { IconEdit, IconUser } from '@douyinfe/semi-icons';
import { Avatar, Button, Skeleton, Space, Tooltip, Typography } from '@douyinfe/semi-ui'; import { Avatar, Button, Skeleton, Space, Tooltip, Typography } from '@douyinfe/semi-ui';
import type { IDocument } from '@think/domains'; import type { IDocument } from '@think/domains';
import { DocumentShare } from 'components/document/share'; import { DocumentShare } from 'components/document/share';
import { DocumentStar } from 'components/document/star'; import { DocumentStar } from 'components/document/star';
import { IconDocument } from 'components/icons/IconDocument'; import { IconDocument } from 'components/icons/IconDocument';
import { LocaleTime } from 'components/locale-time'; import { LocaleTime } from 'components/locale-time';
import Link from 'next/link'; import Link from 'next/link';
import Router from 'next/router'; import Router from 'next/router';
import { useCallback } from 'react';
import styles from './index.module.scss'; 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 { 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 { Members } from 'components/members';
import { useDoumentMembers } from 'data/document'; import { useDoumentMembers } from 'data/document';
import { useUser } from 'data/user'; import { useUser } from 'data/user';
import { event, JOIN_USER } from 'event'; import { event, JOIN_USER } from 'event';
import { IsOnMobile } from 'hooks/use-on-mobile'; import { IsOnMobile } from 'hooks/use-on-mobile';
import { useToggle } from 'hooks/use-toggle'; import { useToggle } from 'hooks/use-toggle';
import React, { useEffect, useMemo, useRef, useState } from 'react';
interface IProps { interface IProps {
wikiId: string; wikiId: string;
@ -14,27 +16,42 @@ interface IProps {
disabled?: boolean; 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 }) => { export const DocumentCollaboration: React.FC<IProps> = ({ wikiId, documentId, disabled = false }) => {
const { isMobile } = IsOnMobile.useHook(); const { isMobile } = IsOnMobile.useHook();
const toastedUsersRef = useRef([]); const toastedUsersRef = useRef([]);
const { user: currentUser } = useUser(); const { user: currentUser } = useUser();
const [visible, toggleVisible] = useToggle(false); const [visible, toggleVisible] = useToggle(false);
const [collaborationUsers, setCollaborationUsers] = useState([]); const [collaborationUsers, setCollaborationUsers] = useState([]);
const content = useMemo( const content = useMemo(
() => ( () =>
<div style={{ padding: '24px 0' }}> !visible ? null : (
<Members <div style={{ padding: '24px 0' }}>
id={documentId} <Members
hook={useDoumentMembers} id={documentId}
descriptions={[ hook={useDoumentMembers}
'权限继承:默认继承知识库成员权限', descriptions={[
'超级管理员:组织超级管理员、知识库超级管理员和文档创建者', '权限继承:默认继承知识库成员权限',
]} '超级管理员:组织超级管理员、知识库超级管理员和文档创建者',
/> ]}
</div> />
), </div>
[documentId] ),
[visible, documentId]
); );
const btn = useMemo( const btn = useMemo(
() => ( () => (
<Button theme="borderless" type="tertiary" disabled={disabled} icon={<IconUserAdd />} onClick={toggleVisible} /> <Button theme="borderless" type="tertiary" disabled={disabled} icon={<IconUserAdd />} onClick={toggleVisible} />
@ -42,6 +59,27 @@ export const DocumentCollaboration: React.FC<IProps> = ({ wikiId, documentId, di
[disabled, toggleVisible] [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(() => { useEffect(() => {
const handler = (users) => { const handler = (users) => {
const joinUsers = users const joinUsers = users
@ -49,35 +87,40 @@ export const DocumentCollaboration: React.FC<IProps> = ({ wikiId, documentId, di
.filter((state) => state.user) .filter((state) => state.user)
.map((state) => ({ ...state.user, clientId: state.clientId })); .map((state) => ({ ...state.user, clientId: state.clientId }));
joinUsers const otherUsers = joinUsers
.filter(Boolean) .filter(Boolean)
.filter((joinUser) => { .filter((joinUser) => {
return joinUser.name !== currentUser.name; return joinUser.name !== currentUser.name;
}) })
.forEach((joinUser) => { .filter((joinUser) => {
if (!toastedUsersRef.current.includes(joinUser.clientId)) { return !toastedUsersRef.current.includes(joinUser.clientId);
Toast.info(`${joinUser.name}-${joinUser.clientId}加入文档`);
toastedUsersRef.current.push(joinUser.clientId);
}
}); });
if (otherUsers.length) {
Toast.info(`${otherUsers[0].name}${otherUsers.length}个用户加入文档`);
otherUsers.forEach((joinUser) => {
toastedUsersRef.current.push(joinUser.clientId);
});
}
setCollaborationUsers(joinUsers); setCollaborationUsers(joinUsers);
}; };
event.on(JOIN_USER, handler); event.on(JOIN_USER, handler);
return () => { return () => {
toastedUsersRef.current = [];
event.off(JOIN_USER, handler); event.off(JOIN_USER, handler);
toastedUsersRef.current = [];
}; };
}, [currentUser]); }, [currentUser]);
return ( return (
<> <>
<AvatarGroup maxCount={5} size="extra-small"> <AvatarGroup maxCount={2} renderMore={renderMore} size="extra-small">
{collaborationUsers.map((user) => { {collaborationUsers.map((user) => {
return ( 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"> <Avatar src={user.avatar} size="extra-small">
{user.name && user.name.charAt(0)} {user.name && user.name.charAt(0)}
</Avatar> </Avatar>
@ -85,6 +128,7 @@ export const DocumentCollaboration: React.FC<IProps> = ({ wikiId, documentId, di
); );
})} })}
</AvatarGroup> </AvatarGroup>
{isMobile ? ( {isMobile ? (
<> <>
<Modal <Modal
@ -93,7 +137,7 @@ export const DocumentCollaboration: React.FC<IProps> = ({ wikiId, documentId, di
visible={visible} visible={visible}
footer={null} footer={null}
onCancel={toggleVisible} onCancel={toggleVisible}
style={{ maxWidth: '96vw', maxHeight: '60vh', overflow: 'auto' }} style={mobileContainerStyle}
> >
{content} {content}
</Modal> </Modal>
@ -106,19 +150,7 @@ export const DocumentCollaboration: React.FC<IProps> = ({ wikiId, documentId, di
onVisibleChange={toggleVisible} onVisibleChange={toggleVisible}
trigger="click" trigger="click"
position="bottomRight" position="bottomRight"
content={ content={<div style={pcContainerStyle}>{content}</div>}
<div
style={{
width: 412,
maxWidth: '96vw',
padding: '0 24px',
maxHeight: '60vh',
overflow: 'auto',
}}
>
{content}
</div>
}
> >
{btn} {btn}
</Dropdown> </Dropdown>

View File

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

View File

@ -1,9 +1,12 @@
import React from 'react';
import { IconUser } from '@douyinfe/semi-icons'; import { IconUser } from '@douyinfe/semi-icons';
import { Avatar, Popconfirm, Skeleton, Space, Typography } from '@douyinfe/semi-ui'; import { Avatar, Popconfirm, Skeleton, Space, Typography } from '@douyinfe/semi-ui';
import type { IComment, IUser } from '@think/domains'; import type { IComment, IUser } from '@think/domains';
import { LocaleTime } from 'components/locale-time'; import { LocaleTime } from 'components/locale-time';
import { useUser } from 'data/user'; import { useUser } from 'data/user';
import React from 'react';
import styles from './index.module.scss'; 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 { 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 { DataRender } from 'components/data-render';
import { useComments } from 'data/comment'; import { useComments } from 'data/comment';
import { useUser } from 'data/user'; import { useUser } from 'data/user';
import { useToggle } from 'hooks/use-toggle'; 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 { Comments } from './comments';
import styles from './index.module.scss'; import styles from './index.module.scss';
interface IProps { 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 { Checkbox, Modal, TabPane, Tabs } from '@douyinfe/semi-ui';
import { IDocument, IWiki } from '@think/domains'; import { IDocument, IWiki } from '@think/domains';
import { TemplateCardEmpty } from 'components/template/card'; import { TemplateCardEmpty } from 'components/template/card';
import { TemplateList } from 'components/template/list'; import { TemplateList } from 'components/template/list';
import { useCreateDocument } from 'data/document'; import { useCreateDocument } from 'data/document';
import { useOwnTemplates, usePublicTemplates } from 'data/template'; import { useOwnTemplates, usePublicTemplates } from 'data/template';
import { useRouterQuery } from 'hooks/use-router-query'; import { useRouterQuery } from 'hooks/use-router-query';
import Router from 'next/router'; import Router from 'next/router';
import { Dispatch, SetStateAction, useCallback, useEffect, useState } from 'react';
import styles from './index.module.scss'; 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 { IconDelete } from '@douyinfe/semi-icons';
import { Popconfirm, Space, Typography } from '@douyinfe/semi-ui'; import { Popconfirm, Space, Typography } from '@douyinfe/semi-ui';
import { useDeleteDocument } from 'data/document'; import { useDeleteDocument } from 'data/document';
import { useRouterQuery } from 'hooks/use-router-query'; import { useRouterQuery } from 'hooks/use-router-query';
import Router from 'next/router'; import Router from 'next/router';
import React, { useCallback, useMemo } from 'react';
interface IProps { interface IProps {
wikiId: string; wikiId: string;

View File

@ -1,9 +1,12 @@
import React, { useEffect, useRef } from 'react';
import { IAuthority, ILoginUser } from '@think/domains'; import { IAuthority, ILoginUser } from '@think/domains';
import { CollaborationEditor, ICollaborationRefProps } from 'tiptap/editor';
import cls from 'classnames'; import cls from 'classnames';
import { event, triggerChangeDocumentTitle, triggerJoinUser, USE_DOCUMENT_VERSION } from 'event'; import { event, triggerChangeDocumentTitle, triggerJoinUser, USE_DOCUMENT_VERSION } from 'event';
import { useMount } from 'hooks/use-mount'; import { useMount } from 'hooks/use-mount';
import React, { useEffect, useRef } from 'react';
import { CollaborationEditor, ICollaborationRefProps } from 'tiptap/editor';
import styles from './index.module.scss'; import styles from './index.module.scss';
@ -23,6 +26,7 @@ export const Editor: React.FC<IProps> = ({ user: currentUser, documentId, author
if (!editor) return; if (!editor) return;
editor.commands.setContent(data); editor.commands.setContent(data);
}; };
event.on(USE_DOCUMENT_VERSION, handler); event.on(USE_DOCUMENT_VERSION, handler);
return () => { return () => {

View File

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

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 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 { createEditor } from 'tiptap/core';
import { AllExtensions } from 'tiptap/core/all-kit'; import { AllExtensions } from 'tiptap/core/all-kit';
import { prosemirrorToMarkdown } from 'tiptap/markdown/prosemirror-to-markdown'; 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 { printEditorContent } from './pdf';
import styles from './index.module.scss';
const { Text } = Typography; const { Text } = Typography;
interface IProps { interface IProps {
@ -40,18 +45,21 @@ export const DocumentExporter: React.FC<IProps> = ({ document, render }) => {
const exportMarkdown = useCallback(() => { const exportMarkdown = useCallback(() => {
const md = prosemirrorToMarkdown({ content: editor.state.doc.slice(0).content }); 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]); }, [document, editor]);
const exportJSON = useCallback(() => { 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]); }, [document, editor]);
const exportWord = useCallback(() => { const exportWord = useCallback(() => {
const editorContent = editor.view.dom.closest('.ProseMirror'); const editorContent = editor.view.dom.closest('.ProseMirror');
if (editorContent) { if (editorContent) {
exportDocx(editorContent.outerHTML).then((res) => { 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]); }, [editor, exportDocx, document]);
@ -70,7 +78,7 @@ export const DocumentExporter: React.FC<IProps> = ({ document, render }) => {
}} }}
> >
<Space> <Space>
<div className={styles.templateItem} onClick={exportMarkdown}> <div className={styles.templateItem} onMouseDown={exportMarkdown}>
<header> <header>
<IconMarkdown style={{ fontSize: 40 }} /> <IconMarkdown style={{ fontSize: 40 }} />
</header> </header>
@ -82,7 +90,7 @@ export const DocumentExporter: React.FC<IProps> = ({ document, render }) => {
</footer> </footer>
</div> </div>
<div className={styles.templateItem} onClick={exportJSON}> <div className={styles.templateItem} onMouseDown={exportJSON}>
<header> <header>
<IconJSON style={{ fontSize: 40 }} /> <IconJSON style={{ fontSize: 40 }} />
</header> </header>
@ -94,7 +102,7 @@ export const DocumentExporter: React.FC<IProps> = ({ document, render }) => {
</footer> </footer>
</div> </div>
<div className={styles.templateItem} onClick={exportWord}> <div className={styles.templateItem} onMouseDown={exportWord}>
<header> <header>
<Badge count="beta" type="danger"> <Badge count="beta" type="danger">
<IconWord style={{ fontSize: 40 }} /> <IconWord style={{ fontSize: 40 }} />
@ -108,7 +116,7 @@ export const DocumentExporter: React.FC<IProps> = ({ document, render }) => {
</footer> </footer>
</div> </div>
<div className={styles.templateItem} onClick={exportPDF}> <div className={styles.templateItem} onMouseDown={exportPDF}>
<header> <header>
<Badge count="beta" type="danger"> <Badge count="beta" type="danger">
<IconPDF style={{ fontSize: 40 }} /> <IconPDF style={{ fontSize: 40 }} />
@ -166,7 +174,7 @@ export const DocumentExporter: React.FC<IProps> = ({ document, render }) => {
<Dropdown <Dropdown
visible={visible} visible={visible}
onVisibleChange={toggleVisible} onVisibleChange={toggleVisible}
trigger="click" trigger="custom"
position="bottomRight" position="bottomRight"
content={<div style={{ padding: '0 16px' }}>{content}</div>} 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 content: string = style + dom.outerHTML;
const iframe: HTMLIFrameElement = document.createElement('iframe'); const iframe: HTMLIFrameElement = document.createElement('iframe');
iframe.id = 'el-tiptap-iframe'; iframe.setAttribute('style', 'position: absolute; width: 0; height: 0; top: 0; left: 0;');
iframe.setAttribute('style', 'position: absolute; width: 0; height: 0; top: -10px; left: -10px;');
document.body.appendChild(iframe); document.body.appendChild(iframe);
const frameWindow = iframe.contentWindow; 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 { IconShrinkScreenStroked } from '@douyinfe/semi-icons';
import { Button, Space, Tooltip, Typography } from '@douyinfe/semi-ui'; import { Button, Space, Tooltip, Typography } from '@douyinfe/semi-ui';
import { EditorContent, useEditor } from '@tiptap/react'; import { EditorContent, useEditor } from '@tiptap/react';
import { CollaborationKit, Document } from 'tiptap/editor';
import cls from 'classnames'; import cls from 'classnames';
import { IconFullscreen } from 'components/icons/IconFullscreen'; import { IconFullscreen } from 'components/icons/IconFullscreen';
import { IconPencil } from 'components/icons/IconPencil'; import { IconPencil } from 'components/icons/IconPencil';
import { safeJSONParse } from 'helpers/json'; import { safeJSONParse } from 'helpers/json';
import { useDrawingCursor } from 'hooks/use-cursor'; import { useDrawingCursor } from 'hooks/use-cursor';
import { useToggle } from 'hooks/use-toggle'; 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'; import styles from './index.module.scss';
@ -74,11 +77,14 @@ export const DocumentFullscreen: React.FC<IProps> = ({ data }) => {
const [isDrawing, toggleDrawing] = useToggle(false); const [isDrawing, toggleDrawing] = useToggle(false);
const [cover, setCover] = useState(''); const [cover, setCover] = useState('');
const editor = useEditor({ const editor = useEditor(
editable: false, {
extensions: CollaborationKit.filter((ext) => ['title', 'doc'].indexOf(ext.name) < 0).concat(Document), editable: false,
content: { type: 'doc', content: [] }, extensions: CollaborationKit.filter((ext) => ['title', 'doc'].indexOf(ext.name) < 0).concat(Document),
}); content: { type: 'doc', content: [] },
},
[]
);
const startPowerpoint = useCallback(() => { const startPowerpoint = useCallback(() => {
toggleVisible(true); toggleVisible(true);

View File

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

View File

@ -1,25 +1,28 @@
import React from 'react';
import { IconUser } from '@douyinfe/semi-icons'; import { IconUser } from '@douyinfe/semi-icons';
import { Avatar, Space } from '@douyinfe/semi-ui'; import { Avatar, Space } from '@douyinfe/semi-ui';
import { IDocument } from '@think/domains'; import { IDocument } from '@think/domains';
import { LocaleTime } from 'components/locale-time'; import { LocaleTime } from 'components/locale-time';
import React from 'react';
interface IProps { interface IProps {
document: IDocument; 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 }) => { export const Author: React.FC<IProps> = ({ document }) => {
return ( return (
<div <div style={style}>
style={{
borderTop: '1px solid var(--semi-color-border)',
marginTop: '0.75em',
padding: '16px 0',
fontSize: 13,
fontWeight: 'normal',
color: 'var(--semi-color-text-0)',
}}
>
<Space> <Space>
<Avatar size="small" src={document && document.createUser && document.createUser.avatar}> <Avatar size="small" src={document && document.createUser && document.createUser.avatar}>
<IconUser /> <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 { IconEdit } from '@douyinfe/semi-icons';
import { Button, Layout, Nav, Skeleton, Space, Spin, Tooltip, Typography } from '@douyinfe/semi-ui'; 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 { DataRender } from 'components/data-render';
import { DocumentCollaboration } from 'components/document/collaboration'; import { DocumentCollaboration } from 'components/document/collaboration';
import { DocumentShare } from 'components/document/share';
import { DocumentStar } from 'components/document/star'; import { DocumentStar } from 'components/document/star';
import { DocumentStyle } from 'components/document/style'; import { DocumentStyle } from 'components/document/style';
import { DocumentVersion } from 'components/document/version'; import { DocumentVersion } from 'components/document/version';
@ -14,13 +19,11 @@ import { useMount } from 'hooks/use-mount';
import { IsOnMobile } from 'hooks/use-on-mobile'; import { IsOnMobile } from 'hooks/use-on-mobile';
import { useWindowSize } from 'hooks/use-window-size'; import { useWindowSize } from 'hooks/use-window-size';
import Router from 'next/router'; 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 { DocumentActions } from '../actions';
import { DocumentFullscreen } from '../fullscreen'; import { DocumentFullscreen } from '../fullscreen';
import { Author } from './author'; import { Author } from './author';
import styles from './index.module.scss'; import styles from './index.module.scss';
const { Header } = Layout; const { Header } = Layout;
@ -30,6 +33,14 @@ interface IProps {
documentId: string; documentId: string;
} }
const loadingStyle = {
minHeight: 240,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
margin: 'auto',
};
export const DocumentReader: React.FC<IProps> = ({ documentId }) => { export const DocumentReader: React.FC<IProps> = ({ documentId }) => {
const { isMobile } = IsOnMobile.useHook(); const { isMobile } = IsOnMobile.useHook();
const mounted = useMount(); const mounted = useMount();
@ -37,6 +48,7 @@ export const DocumentReader: React.FC<IProps> = ({ documentId }) => {
const { user } = useUser(); const { user } = useUser();
const { data: documentAndAuth, loading: docAuthLoading, error: docAuthError } = useDocumentDetail(documentId); const { data: documentAndAuth, loading: docAuthLoading, error: docAuthError } = useDocumentDetail(documentId);
const { document, authority } = documentAndAuth || {}; const { document, authority } = documentAndAuth || {};
const [readable, editable] = useMemo(() => { const [readable, editable] = useMemo(() => {
if (!authority) return [false, false]; if (!authority) return [false, false];
return [authority.readable, authority.editable]; return [authority.readable, authority.editable];
@ -84,6 +96,7 @@ export const DocumentReader: React.FC<IProps> = ({ documentId }) => {
documentId={documentId} documentId={documentId}
/> />
)} )}
<DocumentStyle key="style" />
<Tooltip key="edit" content="编辑" position="bottom"> <Tooltip key="edit" content="编辑" position="bottom">
<Button disabled={!editable} icon={<IconEdit />} onMouseDown={gotoEdit} /> <Button disabled={!editable} icon={<IconEdit />} onMouseDown={gotoEdit} />
</Tooltip> </Tooltip>
@ -131,15 +144,7 @@ export const DocumentReader: React.FC<IProps> = ({ documentId }) => {
<DataRender <DataRender
loading={docAuthLoading} loading={docAuthLoading}
loadingContent={ loadingContent={
<div <div style={loadingStyle}>
style={{
minHeight: 240,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
margin: 'auto',
}}
>
<Spin /> <Spin />
</div> </div>
} }

View File

@ -9,7 +9,6 @@ import { Seo } from 'components/seo';
import { Theme } from 'components/theme'; import { Theme } from 'components/theme';
import { User } from 'components/user'; import { User } from 'components/user';
import { usePublicDocumentDetail } from 'data/document'; import { usePublicDocumentDetail } from 'data/document';
import { useDocumentStyle } from 'hooks/use-document-style';
import { useMount } from 'hooks/use-mount'; import { useMount } from 'hooks/use-mount';
import { IsOnMobile } from 'hooks/use-on-mobile'; import { IsOnMobile } from 'hooks/use-on-mobile';
import { SecureDocumentIllustration } from 'illustrations/secure-document'; import { SecureDocumentIllustration } from 'illustrations/secure-document';
@ -38,12 +37,8 @@ export const DocumentPublicReader: React.FC<IProps> = ({ documentId, hideLogo =
const mounted = useMount(); const mounted = useMount();
const { wikiId: currentWikiId } = useRouterQuery<{ wikiId: IWiki['id']; documentId: IDocument['id'] }>(); const { wikiId: currentWikiId } = useRouterQuery<{ wikiId: IWiki['id']; documentId: IDocument['id'] }>();
const { data, loading, error, query } = usePublicDocumentDetail(documentId); const { data, loading, error, query } = usePublicDocumentDetail(documentId);
const { width, fontSize } = useDocumentStyle();
const { isMobile } = IsOnMobile.useHook(); const { isMobile } = IsOnMobile.useHook();
const editorWrapClassNames = useMemo(() => {
return width === 'standardWidth' ? styles.isStandardWidth : styles.isFullWidth;
}, [width]);
const renderAuthor = useCallback( const renderAuthor = useCallback(
(element) => { (element) => {
if (!document) return null; 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 { IconLink } from '@douyinfe/semi-icons';
import { Button, Dropdown, Input, Modal, Space, Toast, Typography } from '@douyinfe/semi-ui'; import { Button, Dropdown, Input, Modal, Space, Toast, Typography } from '@douyinfe/semi-ui';
import { isPublicDocument } from '@think/domains'; import { isPublicDocument } from '@think/domains';
import { useDocumentDetail } from 'data/document'; import { useDocumentDetail } from 'data/document';
import { getDocumentShareURL } from 'helpers/url'; import { getDocumentShareURL } from 'helpers/url';
import { IsOnMobile } from 'hooks/use-on-mobile'; import { IsOnMobile } from 'hooks/use-on-mobile';
import { useToggle } from 'hooks/use-toggle'; import { useToggle } from 'hooks/use-toggle';
import { ShareIllustration } from 'illustrations/share'; import { ShareIllustration } from 'illustrations/share';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
interface IProps { interface IProps {
documentId: string; 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 React, { useCallback } from 'react';
import VisibilitySensor from 'react-visibility-sensor'; 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 { interface IProps {
organizationId: IOrganization['id']; organizationId: IOrganization['id'];
wikiId: IWiki['id']; wikiId: IWiki['id'];
@ -33,6 +36,15 @@ export const DocumentStar: React.FC<IProps> = ({ organizationId, wikiId, documen
[toggleVisible] [toggleVisible]
); );
const toggleStarAction = useCallback(
(e) => {
e.stopPropagation();
e.preventDefault();
toggleStar();
},
[toggleStar]
);
return ( return (
<VisibilitySensor onChange={onViewportChange}> <VisibilitySensor onChange={onViewportChange}>
{render ? ( {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)', color: data ? 'rgba(var(--semi-amber-4), 1)' : 'rgba(var(--semi-grey-3), 1)',
}} }}
disabled={disabled} disabled={disabled}
onClick={(e) => { onClick={toggleStarAction}
e.stopPropagation();
e.preventDefault();
toggleStar();
}}
/> />
</Tooltip> </Tooltip>
)} )}

View File

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

View File

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

View File

@ -1,6 +1,11 @@
import React, { useCallback, useEffect, useState } from 'react';
import { IconChevronLeft } from '@douyinfe/semi-icons'; import { IconChevronLeft } from '@douyinfe/semi-icons';
import { Button, Modal, Select, Space, Tag, Typography } from '@douyinfe/semi-ui'; import { Button, Modal, Select, Space, Tag, Typography } from '@douyinfe/semi-ui';
import { EditorContent, useEditor } from '@tiptap/react'; import { EditorContent, useEditor } from '@tiptap/react';
import { CollaborationKit } from 'tiptap/editor';
import cls from 'classnames'; import cls from 'classnames';
import { DataRender } from 'components/data-render'; import { DataRender } from 'components/data-render';
import { LocaleTime } from 'components/locale-time'; import { LocaleTime } from 'components/locale-time';
@ -9,8 +14,6 @@ import { generateDiffHtml } from 'helpers/generate-html';
import { safeJSONParse } from 'helpers/json'; import { safeJSONParse } from 'helpers/json';
import { DocumentVersionControl } from 'hooks/use-document-version'; import { DocumentVersionControl } from 'hooks/use-document-version';
import { IsOnMobile } from 'hooks/use-on-mobile'; 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'; 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 [selectedVersion, setSelectedVersion] = useState(null);
const [diffVersion, setDiffVersion] = useState(null); const [diffVersion, setDiffVersion] = useState(null);
const editor = useEditor({ const editor = useEditor(
editable: false, {
editorProps: { editable: false,
attributes: { editorProps: {
class: 'is-editable', attributes: {
class: 'is-editable',
},
}, },
extensions: CollaborationKit,
content: { type: 'doc', content: [] },
}, },
extensions: CollaborationKit, []
content: { type: 'doc', content: [] }, );
});
const close = useCallback(() => { const close = useCallback(() => {
toggleVisible(false); toggleVisible(false);
@ -123,8 +129,8 @@ export const DocumentVersion: React.FC<Partial<IProps>> = ({ documentId, onSelec
</Select> </Select>
<div style={{ paddingLeft: '8px' }}></div> <div style={{ paddingLeft: '8px' }}></div>
<Space style={{ marginLeft: 12 }}> <Space style={{ marginLeft: 12 }}>
<Tag style={{ backgroundColor: '#e9ffe9' }}></Tag> <Tag style={{ backgroundColor: '#e9ffe9', color: '#333' }}></Tag>
<Tag style={{ backgroundColor: '#ffeaea' }}></Tag> <Tag style={{ backgroundColor: '#ffeaea', color: '#333' }}></Tag>
</Space> </Space>
</div> </div>
)} )}
@ -153,8 +159,8 @@ export const DocumentVersion: React.FC<Partial<IProps>> = ({ documentId, onSelec
empty={!loading && !data.length} empty={!loading && !data.length}
normalContent={() => ( normalContent={() => (
<div className={styles.contentWrap}> <div className={styles.contentWrap}>
<main className={cls('container', isMobile && styles.isMobile)}> <main className={cls(isMobile && styles.isMobile)}>
<div> <div className="container">
{diffVersion ? ( {diffVersion ? (
<div id="diff-visual" className="ProseMirror"></div> <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 { Button, Popover, SideSheet, TabPane, Tabs } from '@douyinfe/semi-ui';
import { createKeysLocalStorageLRUCache } from 'helpers/lru-cache'; import { createKeysLocalStorageLRUCache } from 'helpers/lru-cache';
import { IsOnMobile } from 'hooks/use-on-mobile'; import { IsOnMobile } from 'hooks/use-on-mobile';
import { useToggle } from 'hooks/use-toggle'; import { useToggle } from 'hooks/use-toggle';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { ACTIVITIES, EXPRESSIONES, GESTURES, OBJECTS, SKY_WEATHER, SYMBOLS } from './constants'; import { ACTIVITIES, EXPRESSIONES, GESTURES, OBJECTS, SKY_WEATHER, SYMBOLS } from './constants';
import styles from './index.module.scss'; import styles from './index.module.scss';
const emojiLocalStorageLRUCache = createKeysLocalStorageLRUCache('EMOJI_PICKER', 20); const emojiLocalStorageLRUCache = createKeysLocalStorageLRUCache('EMOJI_PICKER', 20);
@ -64,38 +67,44 @@ export const EmojiPicker: React.FC<IProps> = ({ showClear = false, onSelectEmoji
}, [onSelectEmoji]); }, [onSelectEmoji]);
const content = useMemo( const content = useMemo(
() => ( () =>
<div className={styles.wrap}> !visible ? null : (
<Tabs <div className={styles.wrap}>
size="small" <Tabs
lazyRender size="small"
keepDOM lazyRender
tabBarExtraContent={ keepDOM
showClear ? ( tabBarExtraContent={
<Button size="small" onClick={clear}> showClear ? (
<Button size="small" onClick={clear}>
</Button>
) : null </Button>
} ) : null
collapsible }
> collapsible
{renderedList.map((list) => { >
return ( {renderedList.map((list) => {
<TabPane key={list.title} tab={list.title} itemKey={list.title} style={{ height: 250, overflow: 'auto' }}> return (
<div className={styles.listWrap}> <TabPane
{(list.data || []).map((ex) => ( key={list.title}
<div key={ex} onClick={() => selectEmoji(ex)}> tab={list.title}
{ex} itemKey={list.title}
</div> style={{ height: 250, overflow: 'auto' }}
))} >
</div> <div className={styles.listWrap}>
</TabPane> {(list.data || []).map((ex) => (
); <div key={ex} onClick={() => selectEmoji(ex)}>
})} {ex}
</Tabs> </div>
</div> ))}
), </div>
[showClear, renderedList, selectEmoji, clear] </TabPane>
);
})}
</Tabs>
</div>
),
[visible, showClear, renderedList, selectEmoji, clear]
); );
useEffect(() => { useEffect(() => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,7 @@
import { Icon } from '@douyinfe/semi-ui';
import React from 'react'; import React from 'react';
import { Icon } from '@douyinfe/semi-ui';
export const IconShare: React.FC<{ style?: React.CSSProperties }> = ({ style = {} }) => { export const IconShare: React.FC<{ style?: React.CSSProperties }> = ({ style = {} }) => {
return ( return (
<Icon <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 './IconCenter';
export * from './IconClear'; export * from './IconClear';
export * from './IconCodeBlock'; export * from './IconCodeBlock';
export * from './IconColumns';
export * from './IconCountdown'; export * from './IconCountdown';
export * from './IconDeleteColumn'; export * from './IconDeleteColumn';
export * from './IconDeleteRow'; export * from './IconDeleteRow';
@ -29,6 +30,7 @@ export * from './IconInfo';
export * from './IconJSON'; export * from './IconJSON';
export * from './IconLayout'; export * from './IconLayout';
export * from './IconLeft'; export * from './IconLeft';
export * from './IconLineHeight';
export * from './IconLink'; export * from './IconLink';
export * from './IconList'; export * from './IconList';
export * from './IconMarkdown'; export * from './IconMarkdown';
@ -44,12 +46,14 @@ export * from './IconMindSide';
export * from './IconOrderedList'; export * from './IconOrderedList';
export * from './IconOverview'; export * from './IconOverview';
export * from './IconPDF'; export * from './IconPDF';
export * from './IconPPT';
export * from './IconQuote'; export * from './IconQuote';
export * from './IconRight'; export * from './IconRight';
export * from './IconSearch'; export * from './IconSearch';
export * from './IconSearchReplace'; export * from './IconSearchReplace';
export * from './IconSetting'; export * from './IconSetting';
export * from './IconShare'; export * from './IconShare';
export * from './IconSheet';
export * from './IconSplitCell'; export * from './IconSplitCell';
export * from './IconStatus'; export * from './IconStatus';
export * from './IconStructure'; 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 { Button, ButtonGroup, Col, Popover, Row, SideSheet, Skeleton, Space, TabPane, Tabs } from '@douyinfe/semi-ui';
import { Upload } from 'components/upload'; import { Upload } from 'components/upload';
import { chunk } from 'helpers/chunk'; import { chunk } from 'helpers/chunk';
import { IsOnMobile } from 'hooks/use-on-mobile'; import { IsOnMobile } from 'hooks/use-on-mobile';
import { useToggle } from 'hooks/use-toggle'; import { useToggle } from 'hooks/use-toggle';
import React, { useCallback, useMemo, useState } from 'react';
import { LazyLoadImage } from 'react-lazy-load-image-component';
import styles from './index.module.scss'; import styles from './index.module.scss';

View File

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

View File

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

View File

@ -1,4 +1,5 @@
import { Typography } from '@douyinfe/semi-ui'; import { Typography } from '@douyinfe/semi-ui';
import Link from 'next/link'; import Link from 'next/link';
import styles from './index.module.scss'; 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 React, { useCallback, useState } from 'react';
import { Banner, Input, Popconfirm, Select, Space } from '@douyinfe/semi-ui';
import { AuthEnum, AuthEnumArray } from '@think/domains';
interface IProps { interface IProps {
onOk: (arg) => any; 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 React, { useCallback, useState } from 'react';
import { Banner, Popconfirm, Select, Toast } from '@douyinfe/semi-ui';
import { AuthEnum, AuthEnumArray, IAuth, IUser } from '@think/domains';
interface IProps { interface IProps {
userWithAuth: { user: IUser; auth: IAuth }; userWithAuth: { user: IUser; auth: IAuth };
updateUser: (arg) => any; updateUser: (arg) => any;

View File

@ -1,12 +1,16 @@
import React from 'react';
import { IconDelete, IconEdit } from '@douyinfe/semi-icons'; import { IconDelete, IconEdit } from '@douyinfe/semi-icons';
import { Banner, Button, Popconfirm, Table, Typography } from '@douyinfe/semi-ui'; import { Banner, Button, Popconfirm, Table, Typography } from '@douyinfe/semi-ui';
import { AuthEnumTextMap } from '@think/domains'; import { AuthEnumTextMap } from '@think/domains';
import { DataRender } from 'components/data-render'; import { DataRender } from 'components/data-render';
import { LocaleTime } from 'components/locale-time'; import { LocaleTime } from 'components/locale-time';
import React from 'react';
import { AddUser } from './add'; import { AddUser } from './add';
import { EditUser } from './edit'; import { EditUser } from './edit';
import styles from './index.module.scss'; import styles from './index.module.scss';
interface IProps { 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 { Badge, Button, Dropdown, Modal, Pagination, TabPane, Tabs, Typography } from '@douyinfe/semi-ui';
import { DataRender } from 'components/data-render'; import { DataRender } from 'components/data-render';
import { Empty } from 'components/empty'; import { Empty } from 'components/empty';
import { IconMessage } from 'components/icons/IconMessage'; import { IconMessage } from 'components/icons/IconMessage';
@ -8,81 +11,83 @@ import { IsOnMobile } from 'hooks/use-on-mobile';
import { useToggle } from 'hooks/use-toggle'; import { useToggle } from 'hooks/use-toggle';
import { EmptyBoxIllustration } from 'illustrations/empty-box'; import { EmptyBoxIllustration } from 'illustrations/empty-box';
import Link from 'next/link'; import Link from 'next/link';
import React, { useCallback } from 'react';
import { Placeholder } from './placeholder';
import styles from './index.module.scss'; import styles from './index.module.scss';
import { Placeholder } from './placeholder';
const { Text } = Typography; const { Text } = Typography;
const PAGE_SIZE = 6; const PAGE_SIZE = 6;
const MessagesRender = ({ messageData, loading, error, onClick = null, page = 1, onPageChange = null }) => { const MessagesRender = ({ messageData, loading, error, onClick = null, page = 1, onPageChange = null }) => {
const total = (messageData && messageData.total) || 0; const [messages, total] = useMemo(
const messages = (messageData && messageData.data) || []; () => [(messageData && messageData.data) || [], (messageData && messageData.total) || 0],
[messageData]
);
const handleRead = (messageId) => { const handleRead = useCallback(
onClick && onClick(messageId); (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 ( return (
<DataRender <DataRender loading={loading} loadingContent={<Placeholder />} error={error} normalContent={renderNormalContent} />
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>
);
}}
/>
); );
}; };
@ -106,60 +111,83 @@ const MessageBox = () => {
setPage: unreadSetPage, setPage: unreadSetPage,
} = useUnreadMessages(); } = useUnreadMessages();
const clearAll = () => { const clearAll = useCallback(() => {
Promise.all( Promise.all(
(unreadMsgs.data || []).map((msg) => { (unreadMsgs.data || []).map((msg) => {
return readMessage(msg.id); return readMessage(msg.id);
}) })
); );
}; }, [readMessage, unreadMsgs]);
const openModalOnMobile = useCallback(() => { const openModalOnMobile = useCallback(() => {
if (!isMobile) return; if (!isMobile) return;
toggleVisible(true); toggleVisible(true);
}, [isMobile, toggleVisible]); }, [isMobile, toggleVisible]);
const content = ( const content = useMemo(
<Tabs () =>
type="line" visible ? (
size="small" <Tabs
tabBarExtraContent={ type="line"
unreadMsgs && unreadMsgs.total > 0 ? ( size="small"
<Text type="quaternary" onClick={clearAll} style={{ cursor: 'pointer' }}> tabBarExtraContent={
unreadMsgs && unreadMsgs.total > 0 ? (
</Text> <Text type="quaternary" onClick={clearAll} style={{ cursor: 'pointer' }}>
) : null
} </Text>
> ) : null
<TabPane tab="未读" itemKey="unread"> }
<MessagesRender >
messageData={unreadMsgs} <TabPane tab="未读" itemKey="unread">
loading={unreadLoading} <MessagesRender
error={unreadError} messageData={unreadMsgs}
onClick={readMessage} loading={unreadLoading}
page={unreadPage} error={unreadError}
onPageChange={unreadSetPage} onClick={readMessage}
/> page={unreadPage}
</TabPane> onPageChange={unreadSetPage}
<TabPane tab="已读" itemKey="read"> />
<MessagesRender </TabPane>
messageData={readMsgs} <TabPane tab="已读" itemKey="read">
loading={readLoading} <MessagesRender
error={readError} messageData={readMsgs}
page={readPage} loading={readLoading}
onPageChange={readSetPage} error={readError}
/> page={readPage}
</TabPane> onPageChange={readSetPage}
<TabPane tab="全部" itemKey="all"> />
<MessagesRender </TabPane>
messageData={allMsgs} <TabPane tab="全部" itemKey="all">
loading={allLoading} <MessagesRender
error={allError} messageData={allMsgs}
page={allPage} loading={allLoading}
onPageChange={allSetPage} error={allError}
/> page={allPage}
</TabPane> onPageChange={allSetPage}
</Tabs> />
</TabPane>
</Tabs>
) : null,
[
allError,
allLoading,
allMsgs,
allPage,
allSetPage,
clearAll,
readError,
readLoading,
readMessage,
readMsgs,
readPage,
readSetPage,
unreadError,
unreadLoading,
unreadMsgs,
unreadPage,
unreadSetPage,
visible,
]
); );
const btn = ( const btn = (
@ -197,6 +225,8 @@ const MessageBox = () => {
</> </>
) : ( ) : (
<Dropdown <Dropdown
visible={visible}
onVisibleChange={toggleVisible}
position="bottomRight" position="bottomRight"
trigger="click" trigger="click"
content={<div style={{ width: 300, padding: '16px 16px 0' }}>{content}</div>} content={<div style={{ width: 300, padding: '16px 16px 0' }}>{content}</div>}
@ -210,5 +240,8 @@ const MessageBox = () => {
export const Message = () => { export const Message = () => {
const { loading, error } = useUser(); const { loading, error } = useUser();
return <DataRender loading={loading} error={error} normalContent={() => <MessageBox />} />;
const renderNormalContent = useCallback(() => <MessageBox />, []);
return <DataRender loading={loading} error={error} normalContent={renderNormalContent} />;
}; };

View File

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

View File

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

View File

@ -1,9 +1,11 @@
import { Space } from '@douyinfe/semi-ui'; import { Space } from '@douyinfe/semi-ui';
import { LogoImage, LogoText } from 'components/logo'; import { LogoImage, LogoText } from 'components/logo';
import { useUser } from 'data/user'; import { useUser } from 'data/user';
import { useWindowSize } from 'hooks/use-window-size'; import { useWindowSize } from 'hooks/use-window-size';
import { UserOrganizationsSwitcher } from '../switcher'; import { UserOrganizationsSwitcher } from '../switcher';
import styles from './index.module.scss'; import styles from './index.module.scss';
export const OrganizationPublicSwitcher = () => { 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 { FormApi } from '@douyinfe/semi-ui/lib/es/form';
import { ORGANIZATION_LOGOS } from '@think/constants'; import { ORGANIZATION_LOGOS } from '@think/constants';
import { IOrganization } from '@think/domains'; import { IOrganization } from '@think/domains';
import { DataRender } from 'components/data-render'; import { DataRender } from 'components/data-render';
import { ImageUploader } from 'components/image-uploader'; import { ImageUploader } from 'components/image-uploader';
import { useCreateOrganization, useOrganizationDetail } from 'data/organization'; import { useOrganizationDetail } from 'data/organization';
import { useToggle } from 'hooks/use-toggle'; 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'; 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 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 { Base } from './base';
import { OrganizationMembers } from './members'; import { OrganizationMembers } from './members';
import { More } from './more'; import { More } from './more';

View File

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

View File

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

View File

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

View File

@ -1,7 +1,8 @@
import React, { useEffect, useRef } from 'react';
import cls from 'classnames'; import cls from 'classnames';
import { useClickOutside } from 'hooks/use-click-outside'; import { useClickOutside } from 'hooks/use-click-outside';
import interact from 'interactjs'; import interact from 'interactjs';
import React, { useEffect, useRef } from 'react';
import styles from './style.module.scss'; 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 { IconSearch as SemiIconSearch } from '@douyinfe/semi-icons';
import { Button, Dropdown, Input, Modal, Spin, Typography } from '@douyinfe/semi-ui'; import { Button, Dropdown, Input, Modal, Spin, Typography } from '@douyinfe/semi-ui';
import { IDocument } from '@think/domains'; import { IDocument } from '@think/domains';
import { DataRender } from 'components/data-render'; import { DataRender } from 'components/data-render';
import { DocumentStar } from 'components/document/star'; import { DocumentStar } from 'components/document/star';
import { Empty } from 'components/empty'; import { Empty } from 'components/empty';
@ -14,7 +18,6 @@ import { useRouterQuery } from 'hooks/use-router-query';
import { useToggle } from 'hooks/use-toggle'; import { useToggle } from 'hooks/use-toggle';
import Link from 'next/link'; import Link from 'next/link';
import Router from 'next/router'; import Router from 'next/router';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { HttpClient } from 'services/http-client'; import { HttpClient } from 'services/http-client';
import styles from './index.module.scss'; 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 { Button, Dropdown, Form } from '@douyinfe/semi-ui';
import { FormApi } from '@douyinfe/semi-ui/lib/es/form'; import { FormApi } from '@douyinfe/semi-ui/lib/es/form';
import { useCallback, useRef } from 'react';
type ISize = { width: number | string; height: number | string }; type ISize = { width: number | string; height: number | string };
@ -11,6 +12,9 @@ interface IProps {
onOk: (arg: ISize) => void; onOk: (arg: ISize) => void;
} }
const containerStyle = { padding: '0 12px 12px' };
const inlineBlockStyle = { display: 'inline-block' };
export const SizeSetter: React.FC<IProps> = ({ width, maxWidth, height, onOk, children }) => { export const SizeSetter: React.FC<IProps> = ({ width, maxWidth, height, onOk, children }) => {
const $form = useRef<FormApi>(); const $form = useRef<FormApi>();
@ -27,7 +31,7 @@ export const SizeSetter: React.FC<IProps> = ({ width, maxWidth, height, onOk, ch
position={'bottomLeft'} position={'bottomLeft'}
spacing={10} spacing={10}
render={ render={
<div style={{ padding: '0 12px 12px' }}> <div style={containerStyle}>
<Form initValues={{ width, height }} getFormApi={(formApi) => ($form.current = formApi)} labelPosition="left"> <Form initValues={{ width, height }} getFormApi={(formApi) => ($form.current = formApi)} labelPosition="left">
<Form.Input autofocus label="宽" field="width" {...(maxWidth ? { max: maxWidth } : {})} /> <Form.Input autofocus label="宽" field="width" {...(maxWidth ? { max: maxWidth } : {})} />
<Form.Input label="高" field="height" /> <Form.Input label="高" field="height" />
@ -38,7 +42,7 @@ export const SizeSetter: React.FC<IProps> = ({ width, maxWidth, height, onOk, ch
</div> </div>
} }
> >
<span style={{ display: 'inline-block' }}>{children}</span> <span style={inlineBlockStyle}>{children}</span>
</Dropdown> </Dropdown>
); );
}; };

View File

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

View File

@ -1,5 +1,10 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { IconChevronLeft } from '@douyinfe/semi-icons'; import { IconChevronLeft } from '@douyinfe/semi-icons';
import { Button, Nav, Popconfirm, Space, Switch, Tooltip, Typography } from '@douyinfe/semi-ui'; import { Button, Nav, Popconfirm, Space, Switch, Tooltip, Typography } from '@douyinfe/semi-ui';
import { CollaborationEditor } from 'tiptap/editor';
import { DocumentStyle } from 'components/document/style'; import { DocumentStyle } from 'components/document/style';
import { Seo } from 'components/seo'; import { Seo } from 'components/seo';
import { Theme } from 'components/theme'; import { Theme } from 'components/theme';
@ -10,8 +15,6 @@ import { useDocumentStyle } from 'hooks/use-document-style';
import { useMount } from 'hooks/use-mount'; import { useMount } from 'hooks/use-mount';
import { useWindowSize } from 'hooks/use-window-size'; import { useWindowSize } from 'hooks/use-window-size';
import Router from 'next/router'; import Router from 'next/router';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { CollaborationEditor } from 'tiptap/editor';
import styles from './index.module.scss'; 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 { List, Pagination } from '@douyinfe/semi-ui';
import { DataRender } from 'components/data-render'; import { DataRender } from 'components/data-render';
import { Empty } from 'components/empty'; import { Empty } from 'components/empty';
import { IProps as ITemplateCardProps, TemplateCard, TemplateCardPlaceholder } from 'components/template/card'; import { IProps as ITemplateCardProps, TemplateCard, TemplateCardPlaceholder } from 'components/template/card';
import React, { useEffect, useMemo, useState } from 'react';
const grid = { const grid = {
gutter: 16, gutter: 16,
@ -29,15 +31,9 @@ export const TemplateList: React.FC<IProps> = ({
onClosePreview, onClosePreview,
pageSize = 5, pageSize = 5,
}) => { }) => {
const { data, loading, error, refresh } = hook(); const { data, loading, error, page, setPage, refresh } = hook(pageSize);
const [page, onPageChange] = useState(1); const list = (data && data.data) || [];
const total = (data && data.total) || 0;
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]);
useEffect(() => { useEffect(() => {
refresh(); refresh();
@ -62,7 +58,7 @@ export const TemplateList: React.FC<IProps> = ({
<> <>
<List <List
grid={grid} grid={grid}
dataSource={firstListItem ? [{}, ...arr] : arr} dataSource={firstListItem ? [{}, ...list] : list}
renderItem={(template, idx) => { renderItem={(template, idx) => {
if (idx === 0 && firstListItem) { if (idx === 0 && firstListItem) {
return <List.Item>{firstListItem}</List.Item>; return <List.Item>{firstListItem}</List.Item>;
@ -82,7 +78,7 @@ export const TemplateList: React.FC<IProps> = ({
}} }}
emptyContent={<Empty message={'暂无模板'} />} emptyContent={<Empty message={'暂无模板'} />}
></List> ></List>
{data.data.length > pageSize ? ( {total > pageSize ? (
<Pagination <Pagination
size="small" size="small"
style={{ style={{
@ -91,9 +87,9 @@ export const TemplateList: React.FC<IProps> = ({
justifyContent: 'center', justifyContent: 'center',
}} }}
pageSize={pageSize} pageSize={pageSize}
total={data.data.length} total={total}
currentPage={page} currentPage={page}
onChange={(cPage) => onPageChange(cPage)} onChange={(cPage) => setPage(cPage)}
/> />
) : null} ) : null}
</> </>

View File

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

View File

@ -1,7 +1,9 @@
import React, { useCallback } from 'react';
import { IconDesktop, IconMoon, IconSun } from '@douyinfe/semi-icons'; import { IconDesktop, IconMoon, IconSun } from '@douyinfe/semi-icons';
import { Button, Dropdown } from '@douyinfe/semi-ui'; import { Button, Dropdown } from '@douyinfe/semi-ui';
import { Theme as ThemeState, ThemeEnum } from 'hooks/use-theme'; import { Theme as ThemeState, ThemeEnum } from 'hooks/use-theme';
import React, { useCallback } from 'react';
export const Theme = () => { export const Theme = () => {
const { userPrefer, theme, toggle } = ThemeState.useHook(); 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 { Tooltip as SemiTooltip } from '@douyinfe/semi-ui';
import { Position } from '@douyinfe/semi-ui/tooltip'; import { Position } from '@douyinfe/semi-ui/tooltip';
import { useToggle } from 'hooks/use-toggle'; import { useToggle } from 'hooks/use-toggle';
import React from 'react';
interface IProps { interface IProps {
content: React.ReactNode; content: React.ReactNode;

View File

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

View File

@ -1,9 +1,11 @@
import React, { useCallback } from 'react';
import { IconSpin } from '@douyinfe/semi-icons'; import { IconSpin } from '@douyinfe/semi-icons';
import { Avatar, Button, Dropdown, Modal, Toast, Typography } from '@douyinfe/semi-ui'; import { Avatar, Button, Dropdown, Modal, Toast, Typography } from '@douyinfe/semi-ui';
import { useUser } from 'data/user'; import { useUser } from 'data/user';
import { useToggle } from 'hooks/use-toggle'; import { useToggle } from 'hooks/use-toggle';
import Router from 'next/router'; import Router from 'next/router';
import React, { useCallback } from 'react';
import { ResetPassword } from './reset-password'; import { ResetPassword } from './reset-password';
import { UserSetting } from './setting'; 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 { Button, Col, Form, Row, Toast } from '@douyinfe/semi-ui';
import { useResetPassword, useSystemPublicConfig, useUser, useVerifyCode } from 'data/user'; import { useResetPassword, useSystemPublicConfig, useUser, useVerifyCode } from 'data/user';
import { useInterval } from 'hooks/use-interval'; import { useInterval } from 'hooks/use-interval';
import { useToggle } from 'hooks/use-toggle'; import { useToggle } from 'hooks/use-toggle';
import React, { useCallback, useState } from 'react';
export const ResetPassword = ({ onSuccess }) => { export const ResetPassword = ({ onSuccess }) => {
const [email, setEmail] = useState(''); const [email, setEmail] = useState('');
@ -53,7 +55,7 @@ export const ResetPassword = ({ onSuccess }) => {
return ( return (
<Form <Form
initValues={{ email: user.email, password: '', confirmPassword: '' }} initValues={{ email: user ? user.email : '', password: '', confirmPassword: '' }}
onChange={onFormChange} onChange={onFormChange}
onSubmit={onFinish} 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 { Avatar, Button, Col, Form, Modal, Row, Space, Toast } from '@douyinfe/semi-ui';
import { FormApi } from '@douyinfe/semi-ui/lib/es/form'; import { FormApi } from '@douyinfe/semi-ui/lib/es/form';
import { Upload } from 'components/upload'; import { Upload } from 'components/upload';
import { useSystemPublicConfig, useUser, useVerifyCode } from 'data/user'; import { useSystemPublicConfig, useUser, useVerifyCode } from 'data/user';
import { useInterval } from 'hooks/use-interval'; import { useInterval } from 'hooks/use-interval';
import { useToggle } from 'hooks/use-toggle'; import { useToggle } from 'hooks/use-toggle';
import { Dispatch, SetStateAction, useCallback, useEffect, useRef, useState } from 'react';
interface IProps { interface IProps {
visible: boolean; visible: boolean;

View File

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

View File

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

View File

@ -1,5 +1,6 @@
import { IconUser } from '@douyinfe/semi-icons'; import { IconUser } from '@douyinfe/semi-icons';
import { Avatar, Skeleton, Space, Typography } from '@douyinfe/semi-ui'; import { Avatar, Skeleton, Space, Typography } from '@douyinfe/semi-ui';
import { IconDocument } from 'components/icons/IconDocument'; import { IconDocument } from 'components/icons/IconDocument';
import { LocaleTime } from 'components/locale-time'; import { LocaleTime } from 'components/locale-time';
import { WikiStar } from 'components/wiki/star'; 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 { Form, Modal } from '@douyinfe/semi-ui';
import { FormApi } from '@douyinfe/semi-ui/lib/es/form'; import { FormApi } from '@douyinfe/semi-ui/lib/es/form';
import type { IWiki } from '@think/domains'; import type { IWiki } from '@think/domains';
import { ICreateWiki, useOwnWikis } from 'data/wiki'; import { ICreateWiki, useOwnWikis } from 'data/wiki';
import { useRouterQuery } from 'hooks/use-router-query'; import { useRouterQuery } from 'hooks/use-router-query';
import Router from 'next/router'; import Router from 'next/router';
import { Dispatch, SetStateAction, useRef } from 'react';
interface IProps { interface IProps {
visible: boolean; visible: boolean;

View File

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

View File

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

View File

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

View File

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

View File

@ -1,9 +1,13 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { Avatar, Button, Form, Toast } from '@douyinfe/semi-ui'; import { Avatar, Button, Form, Toast } from '@douyinfe/semi-ui';
import { FormApi } from '@douyinfe/semi-ui/lib/es/form'; import { FormApi } from '@douyinfe/semi-ui/lib/es/form';
import { WIKI_AVATARS } from '@think/constants'; import { WIKI_AVATARS } from '@think/constants';
import type { IWiki } from '@think/domains'; import type { IWiki } from '@think/domains';
import { ImageUploader } from 'components/image-uploader'; import { ImageUploader } from 'components/image-uploader';
import { useEffect, useRef, useState } from 'react'; import { pick } from 'helpers/pick';
import styles from './index.module.scss'; import styles from './index.module.scss';
@ -22,32 +26,36 @@ interface IProps {
update: (arg: IUpdateWIKI) => Promise<void>; update: (arg: IUpdateWIKI) => Promise<void>;
} }
const getFormValueFromWiki = (wiki) => {
return pick(wiki, ['name', 'description', 'avatar']);
};
export const Base: React.FC<IProps> = ({ wiki, update }) => { export const Base: React.FC<IProps> = ({ wiki, update }) => {
const $form = useRef<FormApi>(); const $form = useRef<FormApi>();
const [currentCover, setCurrentCover] = useState(''); const [currentCover, setCurrentCover] = useState('');
const onSubmit = () => { const onSubmit = useCallback(() => {
$form.current.validate().then((values) => { $form.current.validate().then((values) => {
update(values).then(() => { update(values).then(() => {
Toast.success('操作成功'); Toast.success('操作成功');
}); });
}); });
}; }, [update]);
const setCover = (url) => { const setCover = useCallback((url) => {
$form.current.setValue('avatar', url); $form.current.setValue('avatar', url);
setCurrentCover(url); setCurrentCover(url);
}; }, []);
useEffect(() => { useEffect(() => {
if (!wiki) return; if (!wiki) return;
$form.current.setValues(wiki); $form.current.setValues(getFormValueFromWiki(wiki));
setCurrentCover(wiki.avatar); setCurrentCover(wiki.avatar);
}, [wiki]); }, [wiki]);
return ( return (
<Form <Form
initValues={wiki} initValues={getFormValueFromWiki(wiki)}
style={{ width: '100%' }} style={{ width: '100%' }}
getFormApi={(formApi) => ($form.current = formApi)} getFormApi={(formApi) => ($form.current = formApi)}
onSubmit={onSubmit} 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 { Button, Toast, Typography, Upload } from '@douyinfe/semi-ui';
import type { IWiki } from '@think/domains'; import type { IWiki } from '@think/domains';
import { useCreateDocument } from 'data/document'; import { useCreateDocument } from 'data/document';
import { useRouterQuery } from 'hooks/use-router-query'; import { useRouterQuery } from 'hooks/use-router-query';
import { useToggle } from 'hooks/use-toggle'; import { useToggle } from 'hooks/use-toggle';
import { useCallback, useEffect, useRef, useState } from 'react';
import { createMarkdownParser, MarkdownParse } from './parser'; import { createMarkdownParser, MarkdownParse } from './parser';
@ -94,7 +97,7 @@ export const Import: React.FC<IProps> = ({ wikiId }) => {
<div style={{ marginTop: 16 }}> <div style={{ marginTop: 16 }}>
<Upload <Upload
action="" action=""
accept="text/markdown" accept=".md,.MD,.Md,.mD"
draggable draggable
multiple multiple
ref={$upload} ref={$upload}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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