mirror of https://github.com/fantasticit/think.git
commit
48b446747b
22
README.md
22
README.md
|
@ -1,5 +1,21 @@
|
|||
# think
|
||||
|
||||
## 声明
|
||||
|
||||
1. 请先阅读[提问的智慧](https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way/blob/main/README-zh_CN.md)
|
||||
2. 为什么停止开发了?
|
||||
1. 对于文档类产品,无法做出独立的 library 或 framework 给不同需求的团队(或个人),这使得我不确定这件事的意义
|
||||
2. 对于独立编辑器开发,无论最终以何种形态存在,其表现还是为应用,而非框架(或依赖),能做到的也许只是一种示范
|
||||
3. 作者本身专攻前端,对高性能、扩展性良好的后端架构心有余而力不足,同时也缺乏专业的运维知识(欢迎赐教)
|
||||
4. 对于 ProseMirror 和 yjs 本身还有许多玩法,但是精力不足
|
||||
1. 类似金山文档的表格体验
|
||||
2. 类似飞书文档的拖拽到节点前后生成分栏
|
||||
3. markdown 、txt、office 文件的导入导出(office 方面可能需要后端协助,java poi 是一个可行的选择)
|
||||
4. 从 office 套件粘贴到编辑器,保留格式和图片(前端可独立完成,思路可参考 TinyCME 的 PowerPaste 和 RTF)
|
||||
5. 基于 yjs 的版本备份和恢复(部分同学提出增量保存 diff,个人还是建议全量 snapshot)
|
||||
6. 基于 yjs 的协同开发(比如结合 luckysheet)
|
||||
3. 如果有好的工作和想法,可以和作者联系(发送邮件)
|
||||
|
||||
## 简介
|
||||
|
||||
Think 是一款开源知识管理工具。通过独立的知识库空间,结构化地组织在线协作文档,实现知识的积累与沉淀,促进知识的复用与流通。同时支持多人协作文档。使用的技术如下:
|
||||
|
@ -15,12 +31,6 @@ Think 是一款开源知识管理工具。通过独立的知识库空间,结
|
|||
|
||||
[云策文档](https://think.codingit.cn)已经部署上线,可前往注册使用。
|
||||
|
||||
## 交流群
|
||||
|
||||
欢迎进群交流。
|
||||
|
||||
<img width="300" alt="image" src="https://user-images.githubusercontent.com/26452939/184578110-62b49297-da6f-4623-945a-3a03550d924f.PNG">
|
||||
|
||||
## 预览
|
||||
|
||||
<details>
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
"private": true,
|
||||
"author": "fantasticit",
|
||||
"scripts": {
|
||||
"clean": "npx rimraf ./node_modules ./packages/**/node_modules",
|
||||
"clean": "npx rimraf ./node_modules ./packages/**/node_modules ./packages/**/.next",
|
||||
"dev": "concurrently 'pnpm:dev:*'",
|
||||
"dev:server": "pnpm run --dir packages/server dev",
|
||||
"dev:client": "pnpm run --dir packages/client dev",
|
||||
|
|
|
@ -54,7 +54,21 @@ module.exports = {
|
|||
'react/react-in-jsx-scope': 'off',
|
||||
'react/prop-types': 'off',
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'simple-import-sort/imports': 'error',
|
||||
'simple-import-sort/imports': [
|
||||
'error',
|
||||
{
|
||||
groups: [
|
||||
['react'],
|
||||
['@douyinfe(.*)$'],
|
||||
['(@)?think(.*)$'],
|
||||
['(@)?tiptap(.*)$'],
|
||||
['^@?\\w'],
|
||||
['@/(.*)'],
|
||||
['^[./]'],
|
||||
['(.*).module.scss'],
|
||||
],
|
||||
},
|
||||
],
|
||||
'simple-import-sort/exports': 'error',
|
||||
},
|
||||
ignorePatterns: ['dist/', 'node_modules', 'scripts', 'examples'],
|
||||
|
|
|
@ -8,11 +8,14 @@
|
|||
"pm2": "pm2 start npm --name @think/client -- start"
|
||||
},
|
||||
"dependencies": {
|
||||
"@douyinfe/semi-icons": "^2.3.1",
|
||||
"@douyinfe/semi-next": "^2.3.1",
|
||||
"@douyinfe/semi-ui": "^2.3.1",
|
||||
"@douyinfe/semi-icons": "^2.18.0",
|
||||
"@douyinfe/semi-next": "^2.18.0",
|
||||
"@douyinfe/semi-ui": "^2.18.0",
|
||||
"@excalidraw/excalidraw": "^0.12.0",
|
||||
"@hocuspocus/provider": "^1.0.0-alpha.29",
|
||||
"@react-pdf-viewer/core": "3.9.0",
|
||||
"@react-pdf-viewer/default-layout": "3.9.0",
|
||||
"@react-pdf-viewer/locales": "^1.0.0",
|
||||
"@think/config": "workspace:^1.0.0",
|
||||
"@think/constants": "workspace:^1.0.0",
|
||||
"@think/domains": "workspace:^1.0.0",
|
||||
|
@ -60,15 +63,14 @@
|
|||
"clone": "^2.1.2",
|
||||
"cross-env": "^7.0.3",
|
||||
"deep-equal": "^2.0.5",
|
||||
"docx": "^7.3.0",
|
||||
"dompurify": "^2.3.5",
|
||||
"downloadjs": "^1.4.7",
|
||||
"html-to-docx": "^1.4.0",
|
||||
"file-saver": "^2.0.5",
|
||||
"htmldiff-js": "^1.0.5",
|
||||
"interactjs": "^1.10.11",
|
||||
"katex": "^0.15.2",
|
||||
"kity": "^2.0.4",
|
||||
"lib0": "^0.2.47",
|
||||
"lodash.pick": "^4.4.0",
|
||||
"lowlight": "^2.5.0",
|
||||
"markdown-it": "^12.3.2",
|
||||
"markdown-it-anchor": "^8.4.1",
|
||||
|
@ -78,6 +80,7 @@
|
|||
"markdown-it-sup": "^1.0.0",
|
||||
"next": "12.1.0",
|
||||
"next-pwa": "^5.5.2",
|
||||
"pdfjs-dist": "3.1.81",
|
||||
"prosemirror-codemark": "^0.3.6",
|
||||
"prosemirror-commands": "^1.3.0",
|
||||
"prosemirror-markdown": "^1.7.0",
|
||||
|
@ -93,7 +96,6 @@
|
|||
"react-full-screen": "^1.1.1",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-lazy-load-image-component": "^1.5.4",
|
||||
"react-pdf": "^5.7.2",
|
||||
"react-query": "^3.39.0",
|
||||
"react-split-pane": "^0.1.92",
|
||||
"react-visibility-sensor": "^5.1.1",
|
||||
|
@ -110,6 +112,7 @@
|
|||
"devDependencies": {
|
||||
"@types/node": "17.0.13",
|
||||
"@types/react": "17.0.38",
|
||||
"@types/react-lazy-load-image-component": "^1.6.3",
|
||||
"@typescript-eslint/eslint-plugin": "^5.21.0",
|
||||
"@typescript-eslint/parser": "^5.21.0",
|
||||
"copy-webpack-plugin": "11.0.0",
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { TabPane, Tabs } from '@douyinfe/semi-ui';
|
||||
import React from 'react';
|
||||
|
||||
import { TabPane, Tabs } from '@douyinfe/semi-ui';
|
||||
|
||||
import { Mail } from './mail';
|
||||
import { System } from './system';
|
||||
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import React, { useCallback } from 'react';
|
||||
|
||||
import { Banner, Button, Form, Toast } from '@douyinfe/semi-ui';
|
||||
|
||||
import { DataRender } from 'components/data-render';
|
||||
import { useSystemConfig } from 'data/user';
|
||||
import { useToggle } from 'hooks/use-toggle';
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
export const Mail = () => {
|
||||
const { data, loading, error, sendTestEmail, updateSystemConfig } = useSystemConfig();
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import React, { useCallback } from 'react';
|
||||
|
||||
import { IconHelpCircle } from '@douyinfe/semi-icons';
|
||||
import { Banner, Button, Form, Toast, Tooltip } from '@douyinfe/semi-ui';
|
||||
|
||||
import { DataRender } from 'components/data-render';
|
||||
import { useSystemConfig } from 'data/user';
|
||||
import { useToggle } from 'hooks/use-toggle';
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
export const System = () => {
|
||||
const { data, loading, error, updateSystemConfig } = useSystemConfig();
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import React, { useEffect, useRef } from 'react';
|
||||
|
||||
import { IconClose } from '@douyinfe/semi-icons';
|
||||
import { Banner as SemiBanner } from '@douyinfe/semi-ui';
|
||||
import { BannerProps } from '@douyinfe/semi-ui/banner';
|
||||
|
||||
import { useToggle } from 'hooks/use-toggle';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
|
||||
interface IProps extends BannerProps {
|
||||
duration?: number;
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import React, { useMemo } from 'react';
|
||||
|
||||
import { Dropdown, SideSheet, Typography } from '@douyinfe/semi-ui';
|
||||
|
||||
import { IsOnMobile } from 'hooks/use-on-mobile';
|
||||
import { useToggle } from 'hooks/use-toggle';
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import styles from './style.module.scss';
|
||||
|
||||
|
@ -89,25 +91,26 @@ export const ColorPicker: React.FC<{
|
|||
const [visible, toggleVisible] = useToggle(false);
|
||||
|
||||
const content = useMemo(
|
||||
() => (
|
||||
<div style={{ padding: isMobile ? '24px 0 24px' : '12px 16px', width: isMobile ? 'auto' : 272 }}>
|
||||
<div className={styles.emptyWrap} onClick={() => onSetColor(null)}>
|
||||
<span></span>
|
||||
<Text>无颜色</Text>
|
||||
</div>
|
||||
() =>
|
||||
!visible ? null : (
|
||||
<div style={{ padding: isMobile ? '24px 0 24px' : '12px 16px', width: isMobile ? 'auto' : 272 }}>
|
||||
<div className={styles.emptyWrap} onClick={() => onSetColor(null)}>
|
||||
<span></span>
|
||||
<Text>无颜色</Text>
|
||||
</div>
|
||||
|
||||
<div className={styles.colorWrap}>
|
||||
{colors.map((color) => {
|
||||
return (
|
||||
<div key={color} className={styles.colorItem} onClick={() => onSetColor(color)}>
|
||||
<span style={{ backgroundColor: color }}></span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div className={styles.colorWrap}>
|
||||
{colors.map((color) => {
|
||||
return (
|
||||
<div key={color} className={styles.colorItem} onClick={() => onSetColor(color)}>
|
||||
<span style={{ backgroundColor: color }}></span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
[onSetColor, isMobile]
|
||||
),
|
||||
[onSetColor, isMobile, visible]
|
||||
);
|
||||
|
||||
if (disabled) return <span style={{ display: 'inline-block' }}>{children}</span>;
|
||||
|
@ -132,7 +135,14 @@ export const ColorPicker: React.FC<{
|
|||
</span>
|
||||
</>
|
||||
) : (
|
||||
<Dropdown zIndex={10000} trigger="click" position={'bottomLeft'} render={content}>
|
||||
<Dropdown
|
||||
visible={visible}
|
||||
onVisibleChange={toggleVisible}
|
||||
zIndex={10000}
|
||||
trigger="click"
|
||||
position={'bottomLeft'}
|
||||
render={content}
|
||||
>
|
||||
<span style={{ display: 'inline-block' }}>{children}</span>
|
||||
</Dropdown>
|
||||
)}
|
||||
|
|
|
@ -1,28 +1,34 @@
|
|||
import { Spin, Typography } from '@douyinfe/semi-ui';
|
||||
import { Empty } from 'illustrations/empty';
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { Spin, Typography } from '@douyinfe/semi-ui';
|
||||
|
||||
import { Empty } from 'illustrations/empty';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
export const defaultLoading = <Spin />;
|
||||
export const defaultLoading = (
|
||||
<div style={{ margin: 'auto' }}>
|
||||
<Spin />
|
||||
</div>
|
||||
);
|
||||
|
||||
export const defaultRenderError = (error) => {
|
||||
return <Text>{(error && error.message) || '未知错误'}</Text>;
|
||||
};
|
||||
|
||||
const emptyStyle: React.CSSProperties = {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexDirection: 'column',
|
||||
position: 'relative',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
};
|
||||
|
||||
export const defaultEmpty = (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexDirection: 'column',
|
||||
position: 'relative',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
}}
|
||||
>
|
||||
<div style={emptyStyle}>
|
||||
<div>
|
||||
<Empty />
|
||||
</div>
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import React from 'react';
|
||||
|
||||
import deepEqual from 'deep-equal';
|
||||
|
||||
import { defaultEmpty, defaultLoading, defaultRenderError, Render } from './constant';
|
||||
import { LoadingWrap } from './loading';
|
||||
|
||||
|
@ -15,7 +17,7 @@ interface IProps {
|
|||
normalContent: RenderProps;
|
||||
}
|
||||
|
||||
export const DataRender: React.FC<IProps> = ({
|
||||
export const _DataRender: React.FC<IProps> = ({
|
||||
loading,
|
||||
error,
|
||||
empty,
|
||||
|
@ -36,3 +38,7 @@ export const DataRender: React.FC<IProps> = ({
|
|||
<LoadingWrap loading={loading} loadingContent={loadingContent} normalContent={loading ? null : normalContent} />
|
||||
);
|
||||
};
|
||||
|
||||
export const DataRender = React.memo(_DataRender, (prevProps, nextProps) => {
|
||||
return deepEqual(prevProps, nextProps);
|
||||
});
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { useToggle } from 'hooks/use-toggle';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
|
||||
import { useToggle } from 'hooks/use-toggle';
|
||||
|
||||
import { Render } from './constant';
|
||||
|
||||
export const LoadingWrap = ({ loading, delay = 200, loadingContent, normalContent }) => {
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import React from 'react';
|
||||
|
||||
import { Button } from '@douyinfe/semi-ui';
|
||||
|
||||
import { DocumentCreator as DocumenCreatorForm } from 'components/document/create';
|
||||
import { useRouterQuery } from 'hooks/use-router-query';
|
||||
import { useToggle } from 'hooks/use-toggle';
|
||||
import React from 'react';
|
||||
|
||||
interface IProps {
|
||||
onCreateDocument?: () => void;
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
import React, { useCallback } from 'react';
|
||||
|
||||
import { IconArticle, IconBranch, IconExport, IconHistory, IconMore, IconPlus, IconStar } from '@douyinfe/semi-icons';
|
||||
import { Button, Dropdown, Space, Typography } from '@douyinfe/semi-ui';
|
||||
import { ButtonProps } from '@douyinfe/semi-ui/button/Button';
|
||||
|
||||
import { IDocument, IOrganization, IWiki } from '@think/domains';
|
||||
|
||||
import cls from 'classnames';
|
||||
import { DocumentCreator } from 'components/document/create';
|
||||
import { DocumentDeletor } from 'components/document/delete';
|
||||
|
@ -12,7 +16,6 @@ import { DocumentStar } from 'components/document/star';
|
|||
import { DocumentStyle } from 'components/document/style';
|
||||
import { DocumentVersionTrigger } from 'components/document/version';
|
||||
import { useToggle } from 'hooks/use-toggle';
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import styles from './index.module.scss';
|
||||
|
||||
|
@ -78,6 +81,7 @@ export const DocumentActions: React.FC<IProps> = ({
|
|||
position="bottomLeft"
|
||||
visible={popoverVisible}
|
||||
onVisibleChange={wrapOnVisibleChange}
|
||||
stopPropagation={true}
|
||||
content={
|
||||
<Dropdown.Menu style={{ minWidth: 112 }}>
|
||||
{showCreateDocument && (
|
||||
|
@ -169,24 +173,6 @@ export const DocumentActions: React.FC<IProps> = ({
|
|||
/>
|
||||
)}
|
||||
|
||||
{!hideDocumentVersion && (
|
||||
<DocumentStyle
|
||||
key="style"
|
||||
render={({ onClick }) => {
|
||||
return (
|
||||
<Dropdown.Item onClick={onClick}>
|
||||
<Text>
|
||||
<Space>
|
||||
<IconArticle />
|
||||
文档排版
|
||||
</Space>
|
||||
</Text>
|
||||
</Dropdown.Item>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{document && (
|
||||
<DocumentExporter
|
||||
document={document}
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
import { useCallback } from 'react';
|
||||
|
||||
import { IconEdit, IconUser } from '@douyinfe/semi-icons';
|
||||
import { Avatar, Button, Skeleton, Space, Tooltip, Typography } from '@douyinfe/semi-ui';
|
||||
|
||||
import type { IDocument } from '@think/domains';
|
||||
|
||||
import { DocumentShare } from 'components/document/share';
|
||||
import { DocumentStar } from 'components/document/star';
|
||||
import { IconDocument } from 'components/icons/IconDocument';
|
||||
import { LocaleTime } from 'components/locale-time';
|
||||
import Link from 'next/link';
|
||||
import Router from 'next/router';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import styles from './index.module.scss';
|
||||
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { IconUserAdd } from '@douyinfe/semi-icons';
|
||||
import { Avatar, AvatarGroup, Button, Dropdown, Modal, Toast, Tooltip } from '@douyinfe/semi-ui';
|
||||
import { Avatar, AvatarGroup, Button, Dropdown, Modal, Popover, Toast, Tooltip, Typography } from '@douyinfe/semi-ui';
|
||||
|
||||
import { Members } from 'components/members';
|
||||
import { useDoumentMembers } from 'data/document';
|
||||
import { useUser } from 'data/user';
|
||||
import { event, JOIN_USER } from 'event';
|
||||
import { IsOnMobile } from 'hooks/use-on-mobile';
|
||||
import { useToggle } from 'hooks/use-toggle';
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
interface IProps {
|
||||
wikiId: string;
|
||||
|
@ -14,27 +16,42 @@ interface IProps {
|
|||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
const mobileContainerStyle: React.CSSProperties = { maxWidth: '96vw', maxHeight: '60vh', overflow: 'auto' };
|
||||
|
||||
const pcContainerStyle: React.CSSProperties = {
|
||||
width: 412,
|
||||
maxWidth: '96vw',
|
||||
padding: '0 24px',
|
||||
maxHeight: '60vh',
|
||||
overflow: 'auto',
|
||||
};
|
||||
|
||||
export const DocumentCollaboration: React.FC<IProps> = ({ wikiId, documentId, disabled = false }) => {
|
||||
const { isMobile } = IsOnMobile.useHook();
|
||||
const toastedUsersRef = useRef([]);
|
||||
const { user: currentUser } = useUser();
|
||||
const [visible, toggleVisible] = useToggle(false);
|
||||
const [collaborationUsers, setCollaborationUsers] = useState([]);
|
||||
|
||||
const content = useMemo(
|
||||
() => (
|
||||
<div style={{ padding: '24px 0' }}>
|
||||
<Members
|
||||
id={documentId}
|
||||
hook={useDoumentMembers}
|
||||
descriptions={[
|
||||
'权限继承:默认继承知识库成员权限',
|
||||
'超级管理员:组织超级管理员、知识库超级管理员和文档创建者',
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
[documentId]
|
||||
() =>
|
||||
!visible ? null : (
|
||||
<div style={{ padding: '24px 0' }}>
|
||||
<Members
|
||||
id={documentId}
|
||||
hook={useDoumentMembers}
|
||||
descriptions={[
|
||||
'权限继承:默认继承知识库成员权限',
|
||||
'超级管理员:组织超级管理员、知识库超级管理员和文档创建者',
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
[visible, documentId]
|
||||
);
|
||||
|
||||
const btn = useMemo(
|
||||
() => (
|
||||
<Button theme="borderless" type="tertiary" disabled={disabled} icon={<IconUserAdd />} onClick={toggleVisible} />
|
||||
|
@ -42,6 +59,27 @@ export const DocumentCollaboration: React.FC<IProps> = ({ wikiId, documentId, di
|
|||
[disabled, toggleVisible]
|
||||
);
|
||||
|
||||
const renderMore = useCallback((restNumber, restAvatars) => {
|
||||
const content = restAvatars.map((avatar, index) => {
|
||||
return (
|
||||
<div style={{ paddingBottom: 12 }} key={index}>
|
||||
{React.cloneElement(avatar, { size: 'extra-small' })}
|
||||
<Text style={{ marginLeft: 8, fontSize: 14 }}>{avatar.props.content}</Text>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
return (
|
||||
<Popover
|
||||
content={<div style={{ maxHeight: '50vh', overflow: 'auto' }}>{content}</div>}
|
||||
autoAdjustOverflow={false}
|
||||
position={'bottomRight'}
|
||||
style={{ padding: '12px 8px', paddingBottom: 0 }}
|
||||
>
|
||||
<Avatar size="extra-small">{`+${restNumber}`}</Avatar>
|
||||
</Popover>
|
||||
);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (users) => {
|
||||
const joinUsers = users
|
||||
|
@ -49,35 +87,40 @@ export const DocumentCollaboration: React.FC<IProps> = ({ wikiId, documentId, di
|
|||
.filter((state) => state.user)
|
||||
.map((state) => ({ ...state.user, clientId: state.clientId }));
|
||||
|
||||
joinUsers
|
||||
const otherUsers = joinUsers
|
||||
.filter(Boolean)
|
||||
.filter((joinUser) => {
|
||||
return joinUser.name !== currentUser.name;
|
||||
})
|
||||
.forEach((joinUser) => {
|
||||
if (!toastedUsersRef.current.includes(joinUser.clientId)) {
|
||||
Toast.info(`${joinUser.name}-${joinUser.clientId}加入文档`);
|
||||
toastedUsersRef.current.push(joinUser.clientId);
|
||||
}
|
||||
.filter((joinUser) => {
|
||||
return !toastedUsersRef.current.includes(joinUser.clientId);
|
||||
});
|
||||
|
||||
if (otherUsers.length) {
|
||||
Toast.info(`${otherUsers[0].name}等${otherUsers.length}个用户加入文档`);
|
||||
|
||||
otherUsers.forEach((joinUser) => {
|
||||
toastedUsersRef.current.push(joinUser.clientId);
|
||||
});
|
||||
}
|
||||
|
||||
setCollaborationUsers(joinUsers);
|
||||
};
|
||||
|
||||
event.on(JOIN_USER, handler);
|
||||
|
||||
return () => {
|
||||
toastedUsersRef.current = [];
|
||||
event.off(JOIN_USER, handler);
|
||||
toastedUsersRef.current = [];
|
||||
};
|
||||
}, [currentUser]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<AvatarGroup maxCount={5} size="extra-small">
|
||||
<AvatarGroup maxCount={2} renderMore={renderMore} size="extra-small">
|
||||
{collaborationUsers.map((user) => {
|
||||
return (
|
||||
<Tooltip key={user.id} content={`${user.name}-${user.clientId}`} position="bottom">
|
||||
<Tooltip key={user.clientId} content={`${user.name}-${user.clientId}`} position="bottom">
|
||||
<Avatar src={user.avatar} size="extra-small">
|
||||
{user.name && user.name.charAt(0)}
|
||||
</Avatar>
|
||||
|
@ -85,6 +128,7 @@ export const DocumentCollaboration: React.FC<IProps> = ({ wikiId, documentId, di
|
|||
);
|
||||
})}
|
||||
</AvatarGroup>
|
||||
|
||||
{isMobile ? (
|
||||
<>
|
||||
<Modal
|
||||
|
@ -93,7 +137,7 @@ export const DocumentCollaboration: React.FC<IProps> = ({ wikiId, documentId, di
|
|||
visible={visible}
|
||||
footer={null}
|
||||
onCancel={toggleVisible}
|
||||
style={{ maxWidth: '96vw', maxHeight: '60vh', overflow: 'auto' }}
|
||||
style={mobileContainerStyle}
|
||||
>
|
||||
{content}
|
||||
</Modal>
|
||||
|
@ -106,19 +150,7 @@ export const DocumentCollaboration: React.FC<IProps> = ({ wikiId, documentId, di
|
|||
onVisibleChange={toggleVisible}
|
||||
trigger="click"
|
||||
position="bottomRight"
|
||||
content={
|
||||
<div
|
||||
style={{
|
||||
width: 412,
|
||||
maxWidth: '96vw',
|
||||
padding: '0 24px',
|
||||
maxHeight: '60vh',
|
||||
overflow: 'auto',
|
||||
}}
|
||||
>
|
||||
{content}
|
||||
</div>
|
||||
}
|
||||
content={<div style={pcContainerStyle}>{content}</div>}
|
||||
>
|
||||
{btn}
|
||||
</Dropdown>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import type { IComment } from '@think/domains';
|
||||
import React from 'react';
|
||||
|
||||
import type { IComment } from '@think/domains';
|
||||
|
||||
import { CommentItem } from './item';
|
||||
|
||||
interface IProps {
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
import React from 'react';
|
||||
|
||||
import { IconUser } from '@douyinfe/semi-icons';
|
||||
import { Avatar, Popconfirm, Skeleton, Space, Typography } from '@douyinfe/semi-ui';
|
||||
|
||||
import type { IComment, IUser } from '@think/domains';
|
||||
|
||||
import { LocaleTime } from 'components/locale-time';
|
||||
import { useUser } from 'data/user';
|
||||
import React from 'react';
|
||||
|
||||
import styles from './index.module.scss';
|
||||
|
||||
|
|
|
@ -1,13 +1,17 @@
|
|||
import React, { useCallback, useRef, useState } from 'react';
|
||||
|
||||
import { Avatar, Banner, Button, Pagination, Space, Spin, Typography } from '@douyinfe/semi-ui';
|
||||
|
||||
import { EditorContent, useEditor } from 'tiptap/core';
|
||||
import { CommentKit, CommentMenuBar } from 'tiptap/editor';
|
||||
|
||||
import { DataRender } from 'components/data-render';
|
||||
import { useComments } from 'data/comment';
|
||||
import { useUser } from 'data/user';
|
||||
import { useToggle } from 'hooks/use-toggle';
|
||||
import React, { useCallback, useRef, useState } from 'react';
|
||||
import { EditorContent, useEditor } from 'tiptap/core';
|
||||
import { CommentKit, CommentMenuBar } from 'tiptap/editor';
|
||||
|
||||
import { Comments } from './comments';
|
||||
|
||||
import styles from './index.module.scss';
|
||||
|
||||
interface IProps {
|
||||
|
|
|
@ -1,12 +1,15 @@
|
|||
import { Dispatch, SetStateAction, useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { Checkbox, Modal, TabPane, Tabs } from '@douyinfe/semi-ui';
|
||||
|
||||
import { IDocument, IWiki } from '@think/domains';
|
||||
|
||||
import { TemplateCardEmpty } from 'components/template/card';
|
||||
import { TemplateList } from 'components/template/list';
|
||||
import { useCreateDocument } from 'data/document';
|
||||
import { useOwnTemplates, usePublicTemplates } from 'data/template';
|
||||
import { useRouterQuery } from 'hooks/use-router-query';
|
||||
import Router from 'next/router';
|
||||
import { Dispatch, SetStateAction, useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import styles from './index.module.scss';
|
||||
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import React, { useCallback, useMemo } from 'react';
|
||||
|
||||
import { IconDelete } from '@douyinfe/semi-icons';
|
||||
import { Popconfirm, Space, Typography } from '@douyinfe/semi-ui';
|
||||
|
||||
import { useDeleteDocument } from 'data/document';
|
||||
import { useRouterQuery } from 'hooks/use-router-query';
|
||||
import Router from 'next/router';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
|
||||
interface IProps {
|
||||
wikiId: string;
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
import React, { useEffect, useRef } from 'react';
|
||||
|
||||
import { IAuthority, ILoginUser } from '@think/domains';
|
||||
|
||||
import { CollaborationEditor, ICollaborationRefProps } from 'tiptap/editor';
|
||||
|
||||
import cls from 'classnames';
|
||||
import { event, triggerChangeDocumentTitle, triggerJoinUser, USE_DOCUMENT_VERSION } from 'event';
|
||||
import { useMount } from 'hooks/use-mount';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { CollaborationEditor, ICollaborationRefProps } from 'tiptap/editor';
|
||||
|
||||
import styles from './index.module.scss';
|
||||
|
||||
|
@ -23,6 +26,7 @@ export const Editor: React.FC<IProps> = ({ user: currentUser, documentId, author
|
|||
if (!editor) return;
|
||||
editor.commands.setContent(data);
|
||||
};
|
||||
|
||||
event.on(USE_DOCUMENT_VERSION, handler);
|
||||
|
||||
return () => {
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { IconChevronLeft } from '@douyinfe/semi-icons';
|
||||
import { Button, Nav, Skeleton, Space, Tooltip, Typography } from '@douyinfe/semi-ui';
|
||||
|
||||
import { DataRender } from 'components/data-render';
|
||||
import { Divider } from 'components/divider';
|
||||
import { DocumentCollaboration } from 'components/document/collaboration';
|
||||
|
@ -16,10 +19,10 @@ import { IsOnMobile } from 'hooks/use-on-mobile';
|
|||
import { useWindowSize } from 'hooks/use-window-size';
|
||||
import { SecureDocumentIllustration } from 'illustrations/secure-document';
|
||||
import Router from 'next/router';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { DocumentActions } from '../actions';
|
||||
import { Editor } from './editor';
|
||||
|
||||
import styles from './index.module.scss';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
@ -28,6 +31,14 @@ interface IProps {
|
|||
documentId: string;
|
||||
}
|
||||
|
||||
const ErrorContent = () => {
|
||||
return (
|
||||
<div style={{ margin: '10vh', textAlign: 'center' }}>
|
||||
<SecureDocumentIllustration />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const DocumentEditor: React.FC<IProps> = ({ documentId }) => {
|
||||
const { isMobile } = IsOnMobile.useHook();
|
||||
const { width: windowWith } = useWindowSize();
|
||||
|
@ -62,7 +73,7 @@ export const DocumentEditor: React.FC<IProps> = ({ documentId }) => {
|
|||
{document && (
|
||||
<DocumentActions organizationId={document.organizationId} wikiId={document.wikiId} documentId={documentId} />
|
||||
)}
|
||||
<DocumentVersion documentId={documentId} onSelect={triggerUseDocumentVersion} />
|
||||
<DocumentVersion key={'edit'} documentId={documentId} onSelect={triggerUseDocumentVersion} />
|
||||
</Space>
|
||||
),
|
||||
[documentId, document, authority]
|
||||
|
@ -84,9 +95,7 @@ export const DocumentEditor: React.FC<IProps> = ({ documentId }) => {
|
|||
mode="horizontal"
|
||||
header={
|
||||
<>
|
||||
<Tooltip content="返回" position="bottom">
|
||||
<Button onMouseDown={goback} icon={<IconChevronLeft />} style={{ marginRight: 16 }} />
|
||||
</Tooltip>
|
||||
<Button onMouseDown={goback} icon={<IconChevronLeft />} style={{ marginRight: 16 }} />
|
||||
<DataRender
|
||||
loading={docAuthLoading}
|
||||
error={docAuthError}
|
||||
|
@ -125,13 +134,7 @@ export const DocumentEditor: React.FC<IProps> = ({ documentId }) => {
|
|||
<DataRender
|
||||
loading={docAuthLoading}
|
||||
error={docAuthError}
|
||||
errorContent={() => {
|
||||
return (
|
||||
<div style={{ margin: '10vh', textAlign: 'center' }}>
|
||||
<SecureDocumentIllustration />
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
errorContent={<ErrorContent />}
|
||||
normalContent={() => {
|
||||
return (
|
||||
<>
|
||||
|
|
|
@ -1,19 +1,24 @@
|
|||
import { Badge, Button, Dropdown, Modal, Space, Typography } from '@douyinfe/semi-ui';
|
||||
import { IDocument } from '@think/domains';
|
||||
import { IconJSON, IconMarkdown, IconPDF, IconWord } from 'components/icons';
|
||||
import { useDocumentDetail } from 'data/document';
|
||||
import download from 'downloadjs';
|
||||
import { safeJSONParse, safeJSONStringify } from 'helpers/json';
|
||||
import { IsOnMobile } from 'hooks/use-on-mobile';
|
||||
import { useToggle } from 'hooks/use-toggle';
|
||||
import React, { useCallback, useEffect, useMemo } from 'react';
|
||||
|
||||
import { Badge, Button, Dropdown, Modal, Space, Typography } from '@douyinfe/semi-ui';
|
||||
|
||||
import { IDocument } from '@think/domains';
|
||||
|
||||
import { createEditor } from 'tiptap/core';
|
||||
import { AllExtensions } from 'tiptap/core/all-kit';
|
||||
import { prosemirrorToMarkdown } from 'tiptap/markdown/prosemirror-to-markdown';
|
||||
|
||||
import styles from './index.module.scss';
|
||||
import { IconJSON, IconMarkdown, IconPDF, IconWord } from 'components/icons';
|
||||
import { useDocumentDetail } from 'data/document';
|
||||
import FileSaver from 'file-saver';
|
||||
import { safeJSONParse, safeJSONStringify } from 'helpers/json';
|
||||
import { IsOnMobile } from 'hooks/use-on-mobile';
|
||||
import { useToggle } from 'hooks/use-toggle';
|
||||
|
||||
import { printEditorContent } from './pdf';
|
||||
|
||||
import styles from './index.module.scss';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
interface IProps {
|
||||
|
@ -40,18 +45,21 @@ export const DocumentExporter: React.FC<IProps> = ({ document, render }) => {
|
|||
|
||||
const exportMarkdown = useCallback(() => {
|
||||
const md = prosemirrorToMarkdown({ content: editor.state.doc.slice(0).content });
|
||||
download(md, `${document.title}.md`, 'text/plain');
|
||||
const blob = new Blob([md], { type: 'text/plain;charset=utf-8' });
|
||||
FileSaver.saveAs(blob, `${document.title}.md`);
|
||||
}, [document, editor]);
|
||||
|
||||
const exportJSON = useCallback(() => {
|
||||
download(safeJSONStringify(editor.getJSON()), `${document.title}.json`, 'text/plain');
|
||||
const blob = new Blob([safeJSONStringify(editor.getJSON())], { type: 'text/plain;charset=utf-8' });
|
||||
FileSaver.saveAs(blob, `${document.title}.json`);
|
||||
}, [document, editor]);
|
||||
|
||||
const exportWord = useCallback(() => {
|
||||
const editorContent = editor.view.dom.closest('.ProseMirror');
|
||||
if (editorContent) {
|
||||
exportDocx(editorContent.outerHTML).then((res) => {
|
||||
download(Buffer.from(res as Buffer), `${document.title}.docx`);
|
||||
const blob = new Blob([Buffer.from(res as Buffer)], { type: 'text/plain;charset=utf-8' });
|
||||
FileSaver.saveAs(blob, `${document.title}.docx`);
|
||||
});
|
||||
}
|
||||
}, [editor, exportDocx, document]);
|
||||
|
@ -70,7 +78,7 @@ export const DocumentExporter: React.FC<IProps> = ({ document, render }) => {
|
|||
}}
|
||||
>
|
||||
<Space>
|
||||
<div className={styles.templateItem} onClick={exportMarkdown}>
|
||||
<div className={styles.templateItem} onMouseDown={exportMarkdown}>
|
||||
<header>
|
||||
<IconMarkdown style={{ fontSize: 40 }} />
|
||||
</header>
|
||||
|
@ -82,7 +90,7 @@ export const DocumentExporter: React.FC<IProps> = ({ document, render }) => {
|
|||
</footer>
|
||||
</div>
|
||||
|
||||
<div className={styles.templateItem} onClick={exportJSON}>
|
||||
<div className={styles.templateItem} onMouseDown={exportJSON}>
|
||||
<header>
|
||||
<IconJSON style={{ fontSize: 40 }} />
|
||||
</header>
|
||||
|
@ -94,7 +102,7 @@ export const DocumentExporter: React.FC<IProps> = ({ document, render }) => {
|
|||
</footer>
|
||||
</div>
|
||||
|
||||
<div className={styles.templateItem} onClick={exportWord}>
|
||||
<div className={styles.templateItem} onMouseDown={exportWord}>
|
||||
<header>
|
||||
<Badge count="beta" type="danger">
|
||||
<IconWord style={{ fontSize: 40 }} />
|
||||
|
@ -108,7 +116,7 @@ export const DocumentExporter: React.FC<IProps> = ({ document, render }) => {
|
|||
</footer>
|
||||
</div>
|
||||
|
||||
<div className={styles.templateItem} onClick={exportPDF}>
|
||||
<div className={styles.templateItem} onMouseDown={exportPDF}>
|
||||
<header>
|
||||
<Badge count="beta" type="danger">
|
||||
<IconPDF style={{ fontSize: 40 }} />
|
||||
|
@ -166,7 +174,7 @@ export const DocumentExporter: React.FC<IProps> = ({ document, render }) => {
|
|||
<Dropdown
|
||||
visible={visible}
|
||||
onVisibleChange={toggleVisible}
|
||||
trigger="click"
|
||||
trigger="custom"
|
||||
position="bottomRight"
|
||||
content={<div style={{ padding: '0 16px' }}>{content}</div>}
|
||||
>
|
||||
|
|
|
@ -9,8 +9,7 @@ function printHtml(dom: Element) {
|
|||
const content: string = style + dom.outerHTML;
|
||||
|
||||
const iframe: HTMLIFrameElement = document.createElement('iframe');
|
||||
iframe.id = 'el-tiptap-iframe';
|
||||
iframe.setAttribute('style', 'position: absolute; width: 0; height: 0; top: -10px; left: -10px;');
|
||||
iframe.setAttribute('style', 'position: absolute; width: 0; height: 0; top: 0; left: 0;');
|
||||
document.body.appendChild(iframe);
|
||||
|
||||
const frameWindow = iframe.contentWindow;
|
||||
|
|
|
@ -1,15 +1,18 @@
|
|||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { FullScreen, useFullScreenHandle } from 'react-full-screen';
|
||||
|
||||
import { IconShrinkScreenStroked } from '@douyinfe/semi-icons';
|
||||
import { Button, Space, Tooltip, Typography } from '@douyinfe/semi-ui';
|
||||
|
||||
import { EditorContent, useEditor } from '@tiptap/react';
|
||||
import { CollaborationKit, Document } from 'tiptap/editor';
|
||||
|
||||
import cls from 'classnames';
|
||||
import { IconFullscreen } from 'components/icons/IconFullscreen';
|
||||
import { IconPencil } from 'components/icons/IconPencil';
|
||||
import { safeJSONParse } from 'helpers/json';
|
||||
import { useDrawingCursor } from 'hooks/use-cursor';
|
||||
import { useToggle } from 'hooks/use-toggle';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { FullScreen, useFullScreenHandle } from 'react-full-screen';
|
||||
import { CollaborationKit, Document } from 'tiptap/editor';
|
||||
|
||||
import styles from './index.module.scss';
|
||||
|
||||
|
@ -74,11 +77,14 @@ export const DocumentFullscreen: React.FC<IProps> = ({ data }) => {
|
|||
const [isDrawing, toggleDrawing] = useToggle(false);
|
||||
const [cover, setCover] = useState('');
|
||||
|
||||
const editor = useEditor({
|
||||
editable: false,
|
||||
extensions: CollaborationKit.filter((ext) => ['title', 'doc'].indexOf(ext.name) < 0).concat(Document),
|
||||
content: { type: 'doc', content: [] },
|
||||
});
|
||||
const editor = useEditor(
|
||||
{
|
||||
editable: false,
|
||||
extensions: CollaborationKit.filter((ext) => ['title', 'doc'].indexOf(ext.name) < 0).concat(Document),
|
||||
content: { type: 'doc', content: [] },
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const startPowerpoint = useCallback(() => {
|
||||
toggleVisible(true);
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
import React, { useCallback } from 'react';
|
||||
|
||||
import { IconLink } from '@douyinfe/semi-icons';
|
||||
import { Space, Typography } from '@douyinfe/semi-ui';
|
||||
|
||||
import { IDocument, IOrganization, IWiki } from '@think/domains';
|
||||
|
||||
import { copy } from 'helpers/copy';
|
||||
import { buildUrl } from 'helpers/url';
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
interface IProps {
|
||||
organizationId: IOrganization['id'];
|
||||
|
@ -14,6 +17,8 @@ interface IProps {
|
|||
|
||||
const { Text } = Typography;
|
||||
|
||||
const style = { cursor: 'pointer' };
|
||||
|
||||
export const DocumentLinkCopyer: React.FC<IProps> = ({ organizationId, wikiId, documentId, render }) => {
|
||||
const handle = useCallback(() => {
|
||||
copy(buildUrl(`/app/org/${organizationId}/wiki/${wikiId}/doc/${documentId}`));
|
||||
|
@ -29,7 +34,7 @@ export const DocumentLinkCopyer: React.FC<IProps> = ({ organizationId, wikiId, d
|
|||
return render ? (
|
||||
<>{render({ copy: handle, children: content })}</>
|
||||
) : (
|
||||
<Text onClick={handle} style={{ cursor: 'pointer' }}>
|
||||
<Text onClick={handle} style={style}>
|
||||
{content}
|
||||
</Text>
|
||||
);
|
||||
|
|
|
@ -1,25 +1,28 @@
|
|||
import React from 'react';
|
||||
|
||||
import { IconUser } from '@douyinfe/semi-icons';
|
||||
import { Avatar, Space } from '@douyinfe/semi-ui';
|
||||
|
||||
import { IDocument } from '@think/domains';
|
||||
|
||||
import { LocaleTime } from 'components/locale-time';
|
||||
import React from 'react';
|
||||
|
||||
interface IProps {
|
||||
document: IDocument;
|
||||
}
|
||||
|
||||
const style = {
|
||||
borderTop: '1px solid var(--semi-color-border)',
|
||||
marginTop: '0.75em',
|
||||
padding: '16px 0',
|
||||
fontSize: 13,
|
||||
fontWeight: 'normal',
|
||||
color: 'var(--semi-color-text-0)',
|
||||
};
|
||||
|
||||
export const Author: React.FC<IProps> = ({ document }) => {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
borderTop: '1px solid var(--semi-color-border)',
|
||||
marginTop: '0.75em',
|
||||
padding: '16px 0',
|
||||
fontSize: 13,
|
||||
fontWeight: 'normal',
|
||||
color: 'var(--semi-color-text-0)',
|
||||
}}
|
||||
>
|
||||
<div style={style}>
|
||||
<Space>
|
||||
<Avatar size="small" src={document && document.createUser && document.createUser.avatar}>
|
||||
<IconUser />
|
||||
|
|
|
@ -1,8 +1,13 @@
|
|||
import React, { useCallback, useMemo } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
import { IconEdit } from '@douyinfe/semi-icons';
|
||||
import { Button, Layout, Nav, Skeleton, Space, Spin, Tooltip, Typography } from '@douyinfe/semi-ui';
|
||||
|
||||
import { CollaborationEditor } from 'tiptap/editor';
|
||||
|
||||
import { DataRender } from 'components/data-render';
|
||||
import { DocumentCollaboration } from 'components/document/collaboration';
|
||||
import { DocumentShare } from 'components/document/share';
|
||||
import { DocumentStar } from 'components/document/star';
|
||||
import { DocumentStyle } from 'components/document/style';
|
||||
import { DocumentVersion } from 'components/document/version';
|
||||
|
@ -14,13 +19,11 @@ import { useMount } from 'hooks/use-mount';
|
|||
import { IsOnMobile } from 'hooks/use-on-mobile';
|
||||
import { useWindowSize } from 'hooks/use-window-size';
|
||||
import Router from 'next/router';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { CollaborationEditor } from 'tiptap/editor';
|
||||
|
||||
import { DocumentActions } from '../actions';
|
||||
import { DocumentFullscreen } from '../fullscreen';
|
||||
import { Author } from './author';
|
||||
|
||||
import styles from './index.module.scss';
|
||||
|
||||
const { Header } = Layout;
|
||||
|
@ -30,6 +33,14 @@ interface IProps {
|
|||
documentId: string;
|
||||
}
|
||||
|
||||
const loadingStyle = {
|
||||
minHeight: 240,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
margin: 'auto',
|
||||
};
|
||||
|
||||
export const DocumentReader: React.FC<IProps> = ({ documentId }) => {
|
||||
const { isMobile } = IsOnMobile.useHook();
|
||||
const mounted = useMount();
|
||||
|
@ -37,6 +48,7 @@ export const DocumentReader: React.FC<IProps> = ({ documentId }) => {
|
|||
const { user } = useUser();
|
||||
const { data: documentAndAuth, loading: docAuthLoading, error: docAuthError } = useDocumentDetail(documentId);
|
||||
const { document, authority } = documentAndAuth || {};
|
||||
|
||||
const [readable, editable] = useMemo(() => {
|
||||
if (!authority) return [false, false];
|
||||
return [authority.readable, authority.editable];
|
||||
|
@ -84,6 +96,7 @@ export const DocumentReader: React.FC<IProps> = ({ documentId }) => {
|
|||
documentId={documentId}
|
||||
/>
|
||||
)}
|
||||
<DocumentStyle key="style" />
|
||||
<Tooltip key="edit" content="编辑" position="bottom">
|
||||
<Button disabled={!editable} icon={<IconEdit />} onMouseDown={gotoEdit} />
|
||||
</Tooltip>
|
||||
|
@ -131,15 +144,7 @@ export const DocumentReader: React.FC<IProps> = ({ documentId }) => {
|
|||
<DataRender
|
||||
loading={docAuthLoading}
|
||||
loadingContent={
|
||||
<div
|
||||
style={{
|
||||
minHeight: 240,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
margin: 'auto',
|
||||
}}
|
||||
>
|
||||
<div style={loadingStyle}>
|
||||
<Spin />
|
||||
</div>
|
||||
}
|
||||
|
|
|
@ -9,7 +9,6 @@ import { Seo } from 'components/seo';
|
|||
import { Theme } from 'components/theme';
|
||||
import { User } from 'components/user';
|
||||
import { usePublicDocumentDetail } from 'data/document';
|
||||
import { useDocumentStyle } from 'hooks/use-document-style';
|
||||
import { useMount } from 'hooks/use-mount';
|
||||
import { IsOnMobile } from 'hooks/use-on-mobile';
|
||||
import { SecureDocumentIllustration } from 'illustrations/secure-document';
|
||||
|
@ -38,12 +37,8 @@ export const DocumentPublicReader: React.FC<IProps> = ({ documentId, hideLogo =
|
|||
const mounted = useMount();
|
||||
const { wikiId: currentWikiId } = useRouterQuery<{ wikiId: IWiki['id']; documentId: IDocument['id'] }>();
|
||||
const { data, loading, error, query } = usePublicDocumentDetail(documentId);
|
||||
const { width, fontSize } = useDocumentStyle();
|
||||
const { isMobile } = IsOnMobile.useHook();
|
||||
const editorWrapClassNames = useMemo(() => {
|
||||
return width === 'standardWidth' ? styles.isStandardWidth : styles.isFullWidth;
|
||||
}, [width]);
|
||||
|
||||
|
||||
const renderAuthor = useCallback(
|
||||
(element) => {
|
||||
if (!document) return null;
|
||||
|
|
|
@ -1,12 +1,15 @@
|
|||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { IconLink } from '@douyinfe/semi-icons';
|
||||
import { Button, Dropdown, Input, Modal, Space, Toast, Typography } from '@douyinfe/semi-ui';
|
||||
|
||||
import { isPublicDocument } from '@think/domains';
|
||||
|
||||
import { useDocumentDetail } from 'data/document';
|
||||
import { getDocumentShareURL } from 'helpers/url';
|
||||
import { IsOnMobile } from 'hooks/use-on-mobile';
|
||||
import { useToggle } from 'hooks/use-toggle';
|
||||
import { ShareIllustration } from 'illustrations/share';
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
interface IProps {
|
||||
documentId: string;
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
import { IconStar } from '@douyinfe/semi-icons';
|
||||
import { Button, Tooltip } from '@douyinfe/semi-ui';
|
||||
import { IDocument, IOrganization, IWiki } from '@think/domains';
|
||||
import { useDocumentStarToggle } from 'data/star';
|
||||
import { useToggle } from 'hooks/use-toggle';
|
||||
import React, { useCallback } from 'react';
|
||||
import VisibilitySensor from 'react-visibility-sensor';
|
||||
|
||||
import { IconStar } from '@douyinfe/semi-icons';
|
||||
import { Button, Tooltip } from '@douyinfe/semi-ui';
|
||||
|
||||
import { IDocument, IOrganization, IWiki } from '@think/domains';
|
||||
|
||||
import { useDocumentStarToggle } from 'data/star';
|
||||
import { useToggle } from 'hooks/use-toggle';
|
||||
|
||||
interface IProps {
|
||||
organizationId: IOrganization['id'];
|
||||
wikiId: IWiki['id'];
|
||||
|
@ -33,6 +36,15 @@ export const DocumentStar: React.FC<IProps> = ({ organizationId, wikiId, documen
|
|||
[toggleVisible]
|
||||
);
|
||||
|
||||
const toggleStarAction = useCallback(
|
||||
(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
toggleStar();
|
||||
},
|
||||
[toggleStar]
|
||||
);
|
||||
|
||||
return (
|
||||
<VisibilitySensor onChange={onViewportChange}>
|
||||
{render ? (
|
||||
|
@ -46,11 +58,7 @@ export const DocumentStar: React.FC<IProps> = ({ organizationId, wikiId, documen
|
|||
color: data ? 'rgba(var(--semi-amber-4), 1)' : 'rgba(var(--semi-grey-3), 1)',
|
||||
}}
|
||||
disabled={disabled}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
toggleStar();
|
||||
}}
|
||||
onClick={toggleStarAction}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
import React, { useMemo } from 'react';
|
||||
|
||||
import { IconArticle } from '@douyinfe/semi-icons';
|
||||
import { Button, Dropdown, Radio, RadioGroup, Slider, Typography } from '@douyinfe/semi-ui';
|
||||
|
||||
import { throttle } from 'helpers/throttle';
|
||||
import { useDocumentStyle } from 'hooks/use-document-style';
|
||||
import { IsOnMobile } from 'hooks/use-on-mobile';
|
||||
import { useToggle } from 'hooks/use-toggle';
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import styles from './index.module.scss';
|
||||
|
||||
|
@ -32,14 +34,8 @@ export const DocumentStyle: React.FC<IProps> = ({ render }) => {
|
|||
position={isMobile ? 'topRight' : 'bottomLeft'}
|
||||
visible={visible}
|
||||
onVisibleChange={toggleVisible}
|
||||
onClickOutSide={toggleVisible}
|
||||
content={
|
||||
<div
|
||||
className={styles.wrap}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<div className={styles.wrap}>
|
||||
<div className={styles.item}>
|
||||
<Text>正文大小</Text>
|
||||
<Text style={{ fontSize: '0.8em' }}> {fontSize}px</Text>
|
||||
|
|
|
@ -25,7 +25,6 @@
|
|||
main {
|
||||
padding: 24px;
|
||||
overflow: auto;
|
||||
background-color: var(--semi-color-bg-0);
|
||||
flex: 1;
|
||||
|
||||
&.isMobile {
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { IconChevronLeft } from '@douyinfe/semi-icons';
|
||||
import { Button, Modal, Select, Space, Tag, Typography } from '@douyinfe/semi-ui';
|
||||
|
||||
import { EditorContent, useEditor } from '@tiptap/react';
|
||||
import { CollaborationKit } from 'tiptap/editor';
|
||||
|
||||
import cls from 'classnames';
|
||||
import { DataRender } from 'components/data-render';
|
||||
import { LocaleTime } from 'components/locale-time';
|
||||
|
@ -9,8 +14,6 @@ import { generateDiffHtml } from 'helpers/generate-html';
|
|||
import { safeJSONParse } from 'helpers/json';
|
||||
import { DocumentVersionControl } from 'hooks/use-document-version';
|
||||
import { IsOnMobile } from 'hooks/use-on-mobile';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { CollaborationKit } from 'tiptap/editor';
|
||||
|
||||
import styles from './index.module.scss';
|
||||
|
||||
|
@ -31,16 +34,19 @@ export const DocumentVersion: React.FC<Partial<IProps>> = ({ documentId, onSelec
|
|||
const [selectedVersion, setSelectedVersion] = useState(null);
|
||||
const [diffVersion, setDiffVersion] = useState(null);
|
||||
|
||||
const editor = useEditor({
|
||||
editable: false,
|
||||
editorProps: {
|
||||
attributes: {
|
||||
class: 'is-editable',
|
||||
const editor = useEditor(
|
||||
{
|
||||
editable: false,
|
||||
editorProps: {
|
||||
attributes: {
|
||||
class: 'is-editable',
|
||||
},
|
||||
},
|
||||
extensions: CollaborationKit,
|
||||
content: { type: 'doc', content: [] },
|
||||
},
|
||||
extensions: CollaborationKit,
|
||||
content: { type: 'doc', content: [] },
|
||||
});
|
||||
[]
|
||||
);
|
||||
|
||||
const close = useCallback(() => {
|
||||
toggleVisible(false);
|
||||
|
@ -123,8 +129,8 @@ export const DocumentVersion: React.FC<Partial<IProps>> = ({ documentId, onSelec
|
|||
</Select>
|
||||
<div style={{ paddingLeft: '8px' }}>对比</div>
|
||||
<Space style={{ marginLeft: 12 }}>
|
||||
<Tag style={{ backgroundColor: '#e9ffe9' }}>增加的内容</Tag>
|
||||
<Tag style={{ backgroundColor: '#ffeaea' }}>删除的内容</Tag>
|
||||
<Tag style={{ backgroundColor: '#e9ffe9', color: '#333' }}>增加的内容</Tag>
|
||||
<Tag style={{ backgroundColor: '#ffeaea', color: '#333' }}>删除的内容</Tag>
|
||||
</Space>
|
||||
</div>
|
||||
)}
|
||||
|
@ -153,8 +159,8 @@ export const DocumentVersion: React.FC<Partial<IProps>> = ({ documentId, onSelec
|
|||
empty={!loading && !data.length}
|
||||
normalContent={() => (
|
||||
<div className={styles.contentWrap}>
|
||||
<main className={cls('container', isMobile && styles.isMobile)}>
|
||||
<div>
|
||||
<main className={cls(isMobile && styles.isMobile)}>
|
||||
<div className="container">
|
||||
{diffVersion ? (
|
||||
<div id="diff-visual" className="ProseMirror"></div>
|
||||
) : (
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { Button, Popover, SideSheet, TabPane, Tabs } from '@douyinfe/semi-ui';
|
||||
|
||||
import { createKeysLocalStorageLRUCache } from 'helpers/lru-cache';
|
||||
import { IsOnMobile } from 'hooks/use-on-mobile';
|
||||
import { useToggle } from 'hooks/use-toggle';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { ACTIVITIES, EXPRESSIONES, GESTURES, OBJECTS, SKY_WEATHER, SYMBOLS } from './constants';
|
||||
|
||||
import styles from './index.module.scss';
|
||||
|
||||
const emojiLocalStorageLRUCache = createKeysLocalStorageLRUCache('EMOJI_PICKER', 20);
|
||||
|
@ -64,38 +67,44 @@ export const EmojiPicker: React.FC<IProps> = ({ showClear = false, onSelectEmoji
|
|||
}, [onSelectEmoji]);
|
||||
|
||||
const content = useMemo(
|
||||
() => (
|
||||
<div className={styles.wrap}>
|
||||
<Tabs
|
||||
size="small"
|
||||
lazyRender
|
||||
keepDOM
|
||||
tabBarExtraContent={
|
||||
showClear ? (
|
||||
<Button size="small" onClick={clear}>
|
||||
清除
|
||||
</Button>
|
||||
) : null
|
||||
}
|
||||
collapsible
|
||||
>
|
||||
{renderedList.map((list) => {
|
||||
return (
|
||||
<TabPane key={list.title} tab={list.title} itemKey={list.title} style={{ height: 250, overflow: 'auto' }}>
|
||||
<div className={styles.listWrap}>
|
||||
{(list.data || []).map((ex) => (
|
||||
<div key={ex} onClick={() => selectEmoji(ex)}>
|
||||
{ex}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</TabPane>
|
||||
);
|
||||
})}
|
||||
</Tabs>
|
||||
</div>
|
||||
),
|
||||
[showClear, renderedList, selectEmoji, clear]
|
||||
() =>
|
||||
!visible ? null : (
|
||||
<div className={styles.wrap}>
|
||||
<Tabs
|
||||
size="small"
|
||||
lazyRender
|
||||
keepDOM
|
||||
tabBarExtraContent={
|
||||
showClear ? (
|
||||
<Button size="small" onClick={clear}>
|
||||
清除
|
||||
</Button>
|
||||
) : null
|
||||
}
|
||||
collapsible
|
||||
>
|
||||
{renderedList.map((list) => {
|
||||
return (
|
||||
<TabPane
|
||||
key={list.title}
|
||||
tab={list.title}
|
||||
itemKey={list.title}
|
||||
style={{ height: 250, overflow: 'auto' }}
|
||||
>
|
||||
<div className={styles.listWrap}>
|
||||
{(list.data || []).map((ex) => (
|
||||
<div key={ex} onClick={() => selectEmoji(ex)}>
|
||||
{ex}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</TabPane>
|
||||
);
|
||||
})}
|
||||
</Tabs>
|
||||
</div>
|
||||
),
|
||||
[visible, showClear, renderedList, selectEmoji, clear]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { Typography } from '@douyinfe/semi-ui';
|
||||
import React from 'react';
|
||||
|
||||
import { Typography } from '@douyinfe/semi-ui';
|
||||
|
||||
interface IProps {
|
||||
illustration?: React.ReactNode;
|
||||
message: React.ReactNode;
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
import React, { MouseEventHandler } from 'react';
|
||||
|
||||
type CellProperties = {
|
||||
active: boolean;
|
||||
hover: boolean;
|
||||
disabled: boolean;
|
||||
cellSize: number;
|
||||
onClick: MouseEventHandler<HTMLDivElement>;
|
||||
onMouseDown: MouseEventHandler<HTMLDivElement>;
|
||||
onMouseEnter: MouseEventHandler<HTMLDivElement>;
|
||||
styles: Record<string, React.CSSProperties>;
|
||||
id: string;
|
||||
|
@ -38,7 +37,7 @@ const getMergedStyle = (baseStyles, styles, styleClass) => ({
|
|||
...(styles && styles[styleClass] ? styles[styleClass] : {}),
|
||||
});
|
||||
|
||||
export const GridCell = ({ active, hover, disabled, onClick, onMouseEnter, cellSize, styles, id }: CellProperties) => {
|
||||
export const GridCell = ({ hover, disabled, onMouseDown, onMouseEnter, cellSize, styles, id }: CellProperties) => {
|
||||
const baseStyles = getBaseStyles(cellSize);
|
||||
const cellStyles = {
|
||||
cell: getMergedStyle(baseStyles, styles, 'cell'),
|
||||
|
@ -52,11 +51,10 @@ export const GridCell = ({ active, hover, disabled, onClick, onMouseEnter, cellS
|
|||
id={id}
|
||||
style={{
|
||||
...cellStyles.cell,
|
||||
...(active && cellStyles.active),
|
||||
...(hover && cellStyles.hover),
|
||||
...(!active && disabled && cellStyles.disabled),
|
||||
...(disabled && cellStyles.disabled),
|
||||
}}
|
||||
onClick={onClick}
|
||||
onMouseDown={onMouseDown}
|
||||
onMouseEnter={onMouseEnter}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { Typography } from '@douyinfe/semi-ui';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import { Typography } from '@douyinfe/semi-ui';
|
||||
|
||||
import { GridCell } from './grid-cell';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
@ -44,10 +45,6 @@ export const GridSelect = ({
|
|||
cellSize = 16,
|
||||
styles,
|
||||
}: RegionSelectionProps) => {
|
||||
const [activeCell, setActiveCell] = useState<CoordsType>({
|
||||
x: -1,
|
||||
y: -1,
|
||||
});
|
||||
const [hoverCell, setHoverCell] = useState<CoordsType>(null);
|
||||
|
||||
const onClick = useCallback(
|
||||
|
@ -60,6 +57,15 @@ export const GridSelect = ({
|
|||
[onSelect]
|
||||
);
|
||||
|
||||
const onClickPanel = useCallback(() => {
|
||||
if (hoverCell.x + 1 > 0 && hoverCell.y + 1 > 0) {
|
||||
onSelect({
|
||||
rows: hoverCell.y + 1,
|
||||
cols: hoverCell.x + 1,
|
||||
});
|
||||
}
|
||||
}, [hoverCell, onSelect]);
|
||||
|
||||
const onHover = useCallback(({ x, y, isCellDisabled }) => {
|
||||
if (isCellDisabled) {
|
||||
return setHoverCell(null);
|
||||
|
@ -71,16 +77,17 @@ export const GridSelect = ({
|
|||
const cells = [];
|
||||
for (let y = 0; y < rows; y++) {
|
||||
for (let x = 0; x < cols; x++) {
|
||||
const isActive = x <= activeCell.x && y <= activeCell.y;
|
||||
const isHover = hoverCell && x <= hoverCell.x && y <= hoverCell.y;
|
||||
const isCellDisabled = disabled;
|
||||
cells.push(
|
||||
<GridCell
|
||||
id={x + '-' + y}
|
||||
key={x + '-' + y}
|
||||
onClick={() => onClick({ x, y, isCellDisabled })}
|
||||
onMouseDown={(e) => {
|
||||
e.stopPropagation();
|
||||
onClick({ x, y, isCellDisabled });
|
||||
}}
|
||||
onMouseEnter={onHover.bind(null, { x, y, isCellDisabled })}
|
||||
active={isActive}
|
||||
hover={isHover}
|
||||
disabled={isCellDisabled}
|
||||
styles={styles}
|
||||
|
@ -90,12 +97,12 @@ export const GridSelect = ({
|
|||
}
|
||||
}
|
||||
return cells;
|
||||
}, [rows, cols, disabled, activeCell.x, activeCell.y, cellSize, hoverCell, styles, onClick, onHover]);
|
||||
}, [rows, cols, disabled, cellSize, hoverCell, styles, onClick, onHover]);
|
||||
|
||||
const baseStyles = useMemo(() => getBaseStyles(cols, cellSize), [cols, cellSize]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div onMouseDown={onClickPanel}>
|
||||
<div
|
||||
style={
|
||||
{
|
||||
|
|
|
@ -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>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -1,6 +1,7 @@
|
|||
import { Icon } from '@douyinfe/semi-ui';
|
||||
import React from 'react';
|
||||
|
||||
import { Icon } from '@douyinfe/semi-ui';
|
||||
|
||||
export const IconDocument: React.FC<{ style?: React.CSSProperties }> = ({ style = {} }) => {
|
||||
return (
|
||||
<Icon
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { Icon } from '@douyinfe/semi-ui';
|
||||
import React from 'react';
|
||||
|
||||
import { Icon } from '@douyinfe/semi-ui';
|
||||
|
||||
export const IconDocumentFill: React.FC<{ style?: React.CSSProperties }> = ({ style = {} }) => {
|
||||
return (
|
||||
<Icon
|
||||
|
|
|
@ -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>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -1,6 +1,7 @@
|
|||
import { Icon } from '@douyinfe/semi-ui';
|
||||
import React from 'react';
|
||||
|
||||
import { Icon } from '@douyinfe/semi-ui';
|
||||
|
||||
export const IconMessage: React.FC<{ style?: React.CSSProperties }> = ({ style = {} }) => {
|
||||
return (
|
||||
<Icon
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { Icon } from '@douyinfe/semi-ui';
|
||||
import React from 'react';
|
||||
|
||||
import { Icon } from '@douyinfe/semi-ui';
|
||||
|
||||
export const IconOverview: React.FC<{ style?: React.CSSProperties }> = ({ style = {} }) => {
|
||||
return (
|
||||
<Icon
|
||||
|
|
|
@ -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>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -1,6 +1,7 @@
|
|||
import { Icon } from '@douyinfe/semi-ui';
|
||||
import React from 'react';
|
||||
|
||||
import { Icon } from '@douyinfe/semi-ui';
|
||||
|
||||
export const IconSearch: React.FC<{ style?: React.CSSProperties }> = ({ style = {} }) => {
|
||||
return (
|
||||
<Icon
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { Icon } from '@douyinfe/semi-ui';
|
||||
import React from 'react';
|
||||
|
||||
import { Icon } from '@douyinfe/semi-ui';
|
||||
|
||||
export const IconSetting: React.FC<{ style?: React.CSSProperties }> = ({ style = {} }) => {
|
||||
return (
|
||||
<Icon
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { Icon } from '@douyinfe/semi-ui';
|
||||
import React from 'react';
|
||||
|
||||
import { Icon } from '@douyinfe/semi-ui';
|
||||
|
||||
export const IconShare: React.FC<{ style?: React.CSSProperties }> = ({ style = {} }) => {
|
||||
return (
|
||||
<Icon
|
||||
|
|
|
@ -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>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -7,6 +7,7 @@ export * from './IconCallout';
|
|||
export * from './IconCenter';
|
||||
export * from './IconClear';
|
||||
export * from './IconCodeBlock';
|
||||
export * from './IconColumns';
|
||||
export * from './IconCountdown';
|
||||
export * from './IconDeleteColumn';
|
||||
export * from './IconDeleteRow';
|
||||
|
@ -29,6 +30,7 @@ export * from './IconInfo';
|
|||
export * from './IconJSON';
|
||||
export * from './IconLayout';
|
||||
export * from './IconLeft';
|
||||
export * from './IconLineHeight';
|
||||
export * from './IconLink';
|
||||
export * from './IconList';
|
||||
export * from './IconMarkdown';
|
||||
|
@ -44,12 +46,14 @@ export * from './IconMindSide';
|
|||
export * from './IconOrderedList';
|
||||
export * from './IconOverview';
|
||||
export * from './IconPDF';
|
||||
export * from './IconPPT';
|
||||
export * from './IconQuote';
|
||||
export * from './IconRight';
|
||||
export * from './IconSearch';
|
||||
export * from './IconSearchReplace';
|
||||
export * from './IconSetting';
|
||||
export * from './IconShare';
|
||||
export * from './IconSheet';
|
||||
export * from './IconSplitCell';
|
||||
export * from './IconStatus';
|
||||
export * from './IconStructure';
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { LazyLoadImage } from 'react-lazy-load-image-component';
|
||||
|
||||
import { Button, ButtonGroup, Col, Popover, Row, SideSheet, Skeleton, Space, TabPane, Tabs } from '@douyinfe/semi-ui';
|
||||
|
||||
import { Upload } from 'components/upload';
|
||||
import { chunk } from 'helpers/chunk';
|
||||
import { IsOnMobile } from 'hooks/use-on-mobile';
|
||||
import { useToggle } from 'hooks/use-toggle';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { LazyLoadImage } from 'react-lazy-load-image-component';
|
||||
|
||||
import styles from './index.module.scss';
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { useEffect } from 'react';
|
||||
|
||||
import Viewer from 'viewerjs';
|
||||
|
||||
interface IProps {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import * as timeagojs from 'timeago.js';
|
||||
|
||||
type Props = {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { Typography } from '@douyinfe/semi-ui';
|
||||
|
||||
import Link from 'next/link';
|
||||
|
||||
import styles from './index.module.scss';
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import { Banner, Input, Popconfirm, Select, Space } from '@douyinfe/semi-ui';
|
||||
import { AuthEnum, AuthEnumArray } from '@think/domains';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
|
||||
import { Banner, Input, Popconfirm, Select, Space } from '@douyinfe/semi-ui';
|
||||
|
||||
import { AuthEnum, AuthEnumArray } from '@think/domains';
|
||||
|
||||
interface IProps {
|
||||
onOk: (arg) => any;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import { Banner, Popconfirm, Select, Toast } from '@douyinfe/semi-ui';
|
||||
import { AuthEnum, AuthEnumArray, IAuth, IUser } from '@think/domains';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
|
||||
import { Banner, Popconfirm, Select, Toast } from '@douyinfe/semi-ui';
|
||||
|
||||
import { AuthEnum, AuthEnumArray, IAuth, IUser } from '@think/domains';
|
||||
|
||||
interface IProps {
|
||||
userWithAuth: { user: IUser; auth: IAuth };
|
||||
updateUser: (arg) => any;
|
||||
|
|
|
@ -1,12 +1,16 @@
|
|||
import React from 'react';
|
||||
|
||||
import { IconDelete, IconEdit } from '@douyinfe/semi-icons';
|
||||
import { Banner, Button, Popconfirm, Table, Typography } from '@douyinfe/semi-ui';
|
||||
|
||||
import { AuthEnumTextMap } from '@think/domains';
|
||||
|
||||
import { DataRender } from 'components/data-render';
|
||||
import { LocaleTime } from 'components/locale-time';
|
||||
import React from 'react';
|
||||
|
||||
import { AddUser } from './add';
|
||||
import { EditUser } from './edit';
|
||||
|
||||
import styles from './index.module.scss';
|
||||
|
||||
interface IProps {
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
import React, { useCallback, useMemo } from 'react';
|
||||
|
||||
import { Badge, Button, Dropdown, Modal, Pagination, TabPane, Tabs, Typography } from '@douyinfe/semi-ui';
|
||||
|
||||
import { DataRender } from 'components/data-render';
|
||||
import { Empty } from 'components/empty';
|
||||
import { IconMessage } from 'components/icons/IconMessage';
|
||||
|
@ -8,81 +11,83 @@ import { IsOnMobile } from 'hooks/use-on-mobile';
|
|||
import { useToggle } from 'hooks/use-toggle';
|
||||
import { EmptyBoxIllustration } from 'illustrations/empty-box';
|
||||
import Link from 'next/link';
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import { Placeholder } from './placeholder';
|
||||
|
||||
import styles from './index.module.scss';
|
||||
import { Placeholder } from './placeholder';
|
||||
|
||||
const { Text } = Typography;
|
||||
const PAGE_SIZE = 6;
|
||||
|
||||
const MessagesRender = ({ messageData, loading, error, onClick = null, page = 1, onPageChange = null }) => {
|
||||
const total = (messageData && messageData.total) || 0;
|
||||
const messages = (messageData && messageData.data) || [];
|
||||
const [messages, total] = useMemo(
|
||||
() => [(messageData && messageData.data) || [], (messageData && messageData.total) || 0],
|
||||
[messageData]
|
||||
);
|
||||
|
||||
const handleRead = (messageId) => {
|
||||
onClick && onClick(messageId);
|
||||
};
|
||||
const handleRead = useCallback(
|
||||
(messageId) => {
|
||||
onClick && onClick(messageId);
|
||||
},
|
||||
[onClick]
|
||||
);
|
||||
|
||||
const renderNormalContent = useCallback(() => {
|
||||
return (
|
||||
<div
|
||||
className={styles.itemsWrap}
|
||||
style={{ margin: '8px -16px', minHeight: 224 }}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
{messages.length ? (
|
||||
<>
|
||||
{messages.map((msg) => {
|
||||
return (
|
||||
<div key={msg.id} className={styles.itemWrap} onClick={() => handleRead(msg.id)}>
|
||||
<Link href={msg.url}>
|
||||
<a className={styles.item}>
|
||||
<div className={styles.leftWrap}>
|
||||
<Text
|
||||
ellipsis={{
|
||||
showTooltip: {
|
||||
opts: { content: msg.message },
|
||||
},
|
||||
}}
|
||||
style={{ width: 240 }}
|
||||
>
|
||||
{msg.title}
|
||||
</Text>
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{total > PAGE_SIZE && (
|
||||
<div className={styles.paginationWrap}>
|
||||
<Pagination
|
||||
size="small"
|
||||
total={total}
|
||||
currentPage={page}
|
||||
pageSize={PAGE_SIZE}
|
||||
style={{ textAlign: 'center' }}
|
||||
onPageChange={onPageChange}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<Empty illustration={<EmptyBoxIllustration />} message="暂无消息" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}, [handleRead, messages, onPageChange, page, total]);
|
||||
|
||||
return (
|
||||
<DataRender
|
||||
loading={loading}
|
||||
loadingContent={<Placeholder />}
|
||||
error={error}
|
||||
normalContent={() => {
|
||||
return (
|
||||
<div
|
||||
className={styles.itemsWrap}
|
||||
style={{ margin: '8px -16px', minHeight: 224 }}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
{messages.length ? (
|
||||
<>
|
||||
{messages.map((msg) => {
|
||||
return (
|
||||
<div key={msg.id} className={styles.itemWrap} onClick={() => handleRead(msg.id)}>
|
||||
<Link href={msg.url}>
|
||||
<a className={styles.item}>
|
||||
<div className={styles.leftWrap}>
|
||||
<Text
|
||||
ellipsis={{
|
||||
showTooltip: {
|
||||
opts: { content: msg.message },
|
||||
},
|
||||
}}
|
||||
style={{ width: 240 }}
|
||||
>
|
||||
{msg.title}
|
||||
</Text>
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{total > PAGE_SIZE && (
|
||||
<div className={styles.paginationWrap}>
|
||||
<Pagination
|
||||
size="small"
|
||||
total={total}
|
||||
currentPage={page}
|
||||
pageSize={PAGE_SIZE}
|
||||
style={{ textAlign: 'center' }}
|
||||
onPageChange={onPageChange}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<Empty illustration={<EmptyBoxIllustration />} message="暂无消息" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<DataRender loading={loading} loadingContent={<Placeholder />} error={error} normalContent={renderNormalContent} />
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -106,60 +111,83 @@ const MessageBox = () => {
|
|||
setPage: unreadSetPage,
|
||||
} = useUnreadMessages();
|
||||
|
||||
const clearAll = () => {
|
||||
const clearAll = useCallback(() => {
|
||||
Promise.all(
|
||||
(unreadMsgs.data || []).map((msg) => {
|
||||
return readMessage(msg.id);
|
||||
})
|
||||
);
|
||||
};
|
||||
}, [readMessage, unreadMsgs]);
|
||||
|
||||
const openModalOnMobile = useCallback(() => {
|
||||
if (!isMobile) return;
|
||||
toggleVisible(true);
|
||||
}, [isMobile, toggleVisible]);
|
||||
|
||||
const content = (
|
||||
<Tabs
|
||||
type="line"
|
||||
size="small"
|
||||
tabBarExtraContent={
|
||||
unreadMsgs && unreadMsgs.total > 0 ? (
|
||||
<Text type="quaternary" onClick={clearAll} style={{ cursor: 'pointer' }}>
|
||||
全部已读
|
||||
</Text>
|
||||
) : null
|
||||
}
|
||||
>
|
||||
<TabPane tab="未读" itemKey="unread">
|
||||
<MessagesRender
|
||||
messageData={unreadMsgs}
|
||||
loading={unreadLoading}
|
||||
error={unreadError}
|
||||
onClick={readMessage}
|
||||
page={unreadPage}
|
||||
onPageChange={unreadSetPage}
|
||||
/>
|
||||
</TabPane>
|
||||
<TabPane tab="已读" itemKey="read">
|
||||
<MessagesRender
|
||||
messageData={readMsgs}
|
||||
loading={readLoading}
|
||||
error={readError}
|
||||
page={readPage}
|
||||
onPageChange={readSetPage}
|
||||
/>
|
||||
</TabPane>
|
||||
<TabPane tab="全部" itemKey="all">
|
||||
<MessagesRender
|
||||
messageData={allMsgs}
|
||||
loading={allLoading}
|
||||
error={allError}
|
||||
page={allPage}
|
||||
onPageChange={allSetPage}
|
||||
/>
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
const content = useMemo(
|
||||
() =>
|
||||
visible ? (
|
||||
<Tabs
|
||||
type="line"
|
||||
size="small"
|
||||
tabBarExtraContent={
|
||||
unreadMsgs && unreadMsgs.total > 0 ? (
|
||||
<Text type="quaternary" onClick={clearAll} style={{ cursor: 'pointer' }}>
|
||||
全部已读
|
||||
</Text>
|
||||
) : null
|
||||
}
|
||||
>
|
||||
<TabPane tab="未读" itemKey="unread">
|
||||
<MessagesRender
|
||||
messageData={unreadMsgs}
|
||||
loading={unreadLoading}
|
||||
error={unreadError}
|
||||
onClick={readMessage}
|
||||
page={unreadPage}
|
||||
onPageChange={unreadSetPage}
|
||||
/>
|
||||
</TabPane>
|
||||
<TabPane tab="已读" itemKey="read">
|
||||
<MessagesRender
|
||||
messageData={readMsgs}
|
||||
loading={readLoading}
|
||||
error={readError}
|
||||
page={readPage}
|
||||
onPageChange={readSetPage}
|
||||
/>
|
||||
</TabPane>
|
||||
<TabPane tab="全部" itemKey="all">
|
||||
<MessagesRender
|
||||
messageData={allMsgs}
|
||||
loading={allLoading}
|
||||
error={allError}
|
||||
page={allPage}
|
||||
onPageChange={allSetPage}
|
||||
/>
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
) : null,
|
||||
[
|
||||
allError,
|
||||
allLoading,
|
||||
allMsgs,
|
||||
allPage,
|
||||
allSetPage,
|
||||
clearAll,
|
||||
readError,
|
||||
readLoading,
|
||||
readMessage,
|
||||
readMsgs,
|
||||
readPage,
|
||||
readSetPage,
|
||||
unreadError,
|
||||
unreadLoading,
|
||||
unreadMsgs,
|
||||
unreadPage,
|
||||
unreadSetPage,
|
||||
visible,
|
||||
]
|
||||
);
|
||||
|
||||
const btn = (
|
||||
|
@ -197,6 +225,8 @@ const MessageBox = () => {
|
|||
</>
|
||||
) : (
|
||||
<Dropdown
|
||||
visible={visible}
|
||||
onVisibleChange={toggleVisible}
|
||||
position="bottomRight"
|
||||
trigger="click"
|
||||
content={<div style={{ width: 300, padding: '16px 16px 0' }}>{content}</div>}
|
||||
|
@ -210,5 +240,8 @@ const MessageBox = () => {
|
|||
|
||||
export const Message = () => {
|
||||
const { loading, error } = useUser();
|
||||
return <DataRender loading={loading} error={error} normalContent={() => <MessageBox />} />;
|
||||
|
||||
const renderNormalContent = useCallback(() => <MessageBox />, []);
|
||||
|
||||
return <DataRender loading={loading} error={error} normalContent={renderNormalContent} />;
|
||||
};
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
import React, { useCallback } from 'react';
|
||||
|
||||
import { IconDelete } from '@douyinfe/semi-icons';
|
||||
import { Modal, Space, Typography } from '@douyinfe/semi-ui';
|
||||
|
||||
import { IOrganization } from '@think/domains';
|
||||
|
||||
import { useOrganizationDetail } from 'data/organization';
|
||||
import { useRouterQuery } from 'hooks/use-router-query';
|
||||
import Router from 'next/router';
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
interface IProps {
|
||||
organizationId: IOrganization['id'];
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { Typography } from '@douyinfe/semi-ui';
|
||||
import { Avatar } from '@douyinfe/semi-ui';
|
||||
|
||||
import { DataRender } from 'components/data-render';
|
||||
import { useOrganizationDetail } from 'data/organization';
|
||||
import { useRouterQuery } from 'hooks/use-router-query';
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import { Space } from '@douyinfe/semi-ui';
|
||||
|
||||
import { LogoImage, LogoText } from 'components/logo';
|
||||
import { useUser } from 'data/user';
|
||||
import { useWindowSize } from 'hooks/use-window-size';
|
||||
|
||||
import { UserOrganizationsSwitcher } from '../switcher';
|
||||
|
||||
import styles from './index.module.scss';
|
||||
|
||||
export const OrganizationPublicSwitcher = () => {
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
import { Avatar, Button, Form, Toast, Typography } from '@douyinfe/semi-ui';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { Avatar, Button, Form, Toast } from '@douyinfe/semi-ui';
|
||||
import { FormApi } from '@douyinfe/semi-ui/lib/es/form';
|
||||
|
||||
import { ORGANIZATION_LOGOS } from '@think/constants';
|
||||
import { IOrganization } from '@think/domains';
|
||||
|
||||
import { DataRender } from 'components/data-render';
|
||||
import { ImageUploader } from 'components/image-uploader';
|
||||
import { useCreateOrganization, useOrganizationDetail } from 'data/organization';
|
||||
import { useOrganizationDetail } from 'data/organization';
|
||||
import { useToggle } from 'hooks/use-toggle';
|
||||
import { SingleColumnLayout } from 'layouts/single-column';
|
||||
import Router from 'next/router';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import styles from './index.module.scss';
|
||||
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
import { TabPane, Tabs } from '@douyinfe/semi-ui';
|
||||
import { IOrganization } from '@think/domains';
|
||||
import { Seo } from 'components/seo';
|
||||
import React from 'react';
|
||||
|
||||
import { TabPane, Tabs } from '@douyinfe/semi-ui';
|
||||
|
||||
import { IOrganization } from '@think/domains';
|
||||
|
||||
import { Seo } from 'components/seo';
|
||||
|
||||
import { Base } from './base';
|
||||
import { OrganizationMembers } from './members';
|
||||
import { More } from './more';
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import React from 'react';
|
||||
|
||||
import { IOrganization } from '@think/domains';
|
||||
|
||||
import { Members } from 'components/members';
|
||||
import { useOrganizationMembers } from 'data/organization';
|
||||
import React from 'react';
|
||||
|
||||
interface IProps {
|
||||
organizationId: IOrganization['id'];
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { Banner, Button, Typography } from '@douyinfe/semi-ui';
|
||||
|
||||
import { OrganizationDeletor } from 'components/organization/delete';
|
||||
|
||||
const { Paragraph } = Typography;
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
import { useMemo } from 'react';
|
||||
|
||||
import { IconAppCenter, IconApps, IconSmallTriangleDown } from '@douyinfe/semi-icons';
|
||||
import { Button, Dropdown, Space, Typography } from '@douyinfe/semi-ui';
|
||||
import { Avatar } from '@douyinfe/semi-ui';
|
||||
|
||||
import { DataRender } from 'components/data-render';
|
||||
import { useOrganizationDetail, useUserOrganizations } from 'data/organization';
|
||||
import { useRouterQuery } from 'hooks/use-router-query';
|
||||
import Link from 'next/link';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import styles from './index.module.scss';
|
||||
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import React, { useEffect, useRef } from 'react';
|
||||
|
||||
import cls from 'classnames';
|
||||
import { useClickOutside } from 'hooks/use-click-outside';
|
||||
import interact from 'interactjs';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
|
||||
import styles from './style.module.scss';
|
||||
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { IconSearch as SemiIconSearch } from '@douyinfe/semi-icons';
|
||||
import { Button, Dropdown, Input, Modal, Spin, Typography } from '@douyinfe/semi-ui';
|
||||
|
||||
import { IDocument } from '@think/domains';
|
||||
|
||||
import { DataRender } from 'components/data-render';
|
||||
import { DocumentStar } from 'components/document/star';
|
||||
import { Empty } from 'components/empty';
|
||||
|
@ -14,7 +18,6 @@ import { useRouterQuery } from 'hooks/use-router-query';
|
|||
import { useToggle } from 'hooks/use-toggle';
|
||||
import Link from 'next/link';
|
||||
import Router from 'next/router';
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { HttpClient } from 'services/http-client';
|
||||
|
||||
import styles from './index.module.scss';
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { useCallback, useRef } from 'react';
|
||||
|
||||
import { Button, Dropdown, Form } from '@douyinfe/semi-ui';
|
||||
import { FormApi } from '@douyinfe/semi-ui/lib/es/form';
|
||||
import { useCallback, useRef } from 'react';
|
||||
|
||||
type ISize = { width: number | string; height: number | string };
|
||||
|
||||
|
@ -11,6 +12,9 @@ interface IProps {
|
|||
onOk: (arg: ISize) => void;
|
||||
}
|
||||
|
||||
const containerStyle = { padding: '0 12px 12px' };
|
||||
const inlineBlockStyle = { display: 'inline-block' };
|
||||
|
||||
export const SizeSetter: React.FC<IProps> = ({ width, maxWidth, height, onOk, children }) => {
|
||||
const $form = useRef<FormApi>();
|
||||
|
||||
|
@ -27,7 +31,7 @@ export const SizeSetter: React.FC<IProps> = ({ width, maxWidth, height, onOk, ch
|
|||
position={'bottomLeft'}
|
||||
spacing={10}
|
||||
render={
|
||||
<div style={{ padding: '0 12px 12px' }}>
|
||||
<div style={containerStyle}>
|
||||
<Form initValues={{ width, height }} getFormApi={(formApi) => ($form.current = formApi)} labelPosition="left">
|
||||
<Form.Input autofocus label="宽" field="width" {...(maxWidth ? { max: maxWidth } : {})} />
|
||||
<Form.Input label="高" field="height" />
|
||||
|
@ -38,7 +42,7 @@ export const SizeSetter: React.FC<IProps> = ({ width, maxWidth, height, onOk, ch
|
|||
</div>
|
||||
}
|
||||
>
|
||||
<span style={{ display: 'inline-block' }}>{children}</span>
|
||||
<span style={inlineBlockStyle}>{children}</span>
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
import { useCallback } from 'react';
|
||||
|
||||
import { IconEdit, IconPlus, IconUser } from '@douyinfe/semi-icons';
|
||||
import { Avatar, Button, Modal, Skeleton, Space, Tooltip, Typography } from '@douyinfe/semi-ui';
|
||||
|
||||
import type { ITemplate } from '@think/domains';
|
||||
|
||||
import cls from 'classnames';
|
||||
import { IconDocument } from 'components/icons/IconDocument';
|
||||
import { TemplateReader } from 'components/template/reader';
|
||||
import { useUser } from 'data/user';
|
||||
import { useToggle } from 'hooks/use-toggle';
|
||||
import Router from 'next/router';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import styles from './index.module.scss';
|
||||
|
||||
|
@ -21,6 +24,17 @@ export interface IProps {
|
|||
onClosePreview?: () => void;
|
||||
}
|
||||
|
||||
const bodyStyle = {
|
||||
overflow: 'auto',
|
||||
};
|
||||
const titleContainerStyle = {
|
||||
marginBottom: 12,
|
||||
whiteSpace: 'nowrap',
|
||||
textOverflow: 'ellipsis',
|
||||
overflow: 'hidden',
|
||||
} as React.CSSProperties;
|
||||
const flexStyle = { display: 'flex' };
|
||||
|
||||
export const TemplateCard: React.FC<IProps> = ({
|
||||
template,
|
||||
onClick,
|
||||
|
@ -35,26 +49,35 @@ export const TemplateCard: React.FC<IProps> = ({
|
|||
Router.push(`/template/${template.id}/`);
|
||||
}, [template]);
|
||||
|
||||
const cancel = useCallback(() => {
|
||||
toggleVisible(false);
|
||||
onClosePreview && onClosePreview();
|
||||
}, [toggleVisible, onClosePreview]);
|
||||
|
||||
const preview = useCallback(() => {
|
||||
toggleVisible(true);
|
||||
onOpenPreview && onOpenPreview();
|
||||
}, [toggleVisible, onOpenPreview]);
|
||||
|
||||
const useTemplate = useCallback(() => {
|
||||
onClick && onClick(template.id);
|
||||
}, [onClick, template.id]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
title="模板预览"
|
||||
width={'calc(100vh - 120px)'}
|
||||
height={'calc(100vh - 120px)'}
|
||||
bodyStyle={{
|
||||
overflow: 'auto',
|
||||
}}
|
||||
bodyStyle={bodyStyle}
|
||||
visible={visible}
|
||||
onCancel={() => {
|
||||
toggleVisible(false);
|
||||
onClosePreview && onClosePreview();
|
||||
}}
|
||||
onCancel={cancel}
|
||||
footer={null}
|
||||
fullScreen
|
||||
>
|
||||
<TemplateReader key={template.id} templateId={template.id} />
|
||||
</Modal>
|
||||
<div className={cls(styles.cardWrap, getClassNames(template.id))}>
|
||||
<div className={cls(styles.cardWrap, getClassNames(template.id))} onClick={useTemplate}>
|
||||
<header>
|
||||
<IconDocument />
|
||||
<div className={styles.rightWrap}>
|
||||
|
@ -68,14 +91,7 @@ export const TemplateCard: React.FC<IProps> = ({
|
|||
</div>
|
||||
</header>
|
||||
<main>
|
||||
<div
|
||||
style={{
|
||||
marginBottom: 12,
|
||||
whiteSpace: 'nowrap',
|
||||
textOverflow: 'ellipsis',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<div style={titleContainerStyle}>
|
||||
<Text strong>{template.title}</Text>
|
||||
</div>
|
||||
<div>
|
||||
|
@ -92,28 +108,16 @@ export const TemplateCard: React.FC<IProps> = ({
|
|||
</main>
|
||||
<footer>
|
||||
<Text type="tertiary" size="small">
|
||||
<div style={{ display: 'flex' }}>
|
||||
<div style={flexStyle}>
|
||||
已使用
|
||||
{template.usageAmount}次
|
||||
</div>
|
||||
</Text>
|
||||
</footer>
|
||||
<div className={styles.actions}>
|
||||
<Button
|
||||
theme="solid"
|
||||
type="tertiary"
|
||||
onClick={() => {
|
||||
toggleVisible(true);
|
||||
onOpenPreview && onOpenPreview();
|
||||
}}
|
||||
>
|
||||
<Button theme="solid" type="tertiary" onClick={preview}>
|
||||
预览
|
||||
</Button>
|
||||
{onClick && (
|
||||
<Button type="primary" theme="solid" onClick={() => onClick && onClick(template.id)}>
|
||||
使用
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { IconChevronLeft } from '@douyinfe/semi-icons';
|
||||
import { Button, Nav, Popconfirm, Space, Switch, Tooltip, Typography } from '@douyinfe/semi-ui';
|
||||
|
||||
import { CollaborationEditor } from 'tiptap/editor';
|
||||
|
||||
import { DocumentStyle } from 'components/document/style';
|
||||
import { Seo } from 'components/seo';
|
||||
import { Theme } from 'components/theme';
|
||||
|
@ -10,8 +15,6 @@ import { useDocumentStyle } from 'hooks/use-document-style';
|
|||
import { useMount } from 'hooks/use-mount';
|
||||
import { useWindowSize } from 'hooks/use-window-size';
|
||||
import Router from 'next/router';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { CollaborationEditor } from 'tiptap/editor';
|
||||
|
||||
import styles from './index.module.scss';
|
||||
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import React, { useEffect } from 'react';
|
||||
|
||||
import { List, Pagination } from '@douyinfe/semi-ui';
|
||||
|
||||
import { DataRender } from 'components/data-render';
|
||||
import { Empty } from 'components/empty';
|
||||
import { IProps as ITemplateCardProps, TemplateCard, TemplateCardPlaceholder } from 'components/template/card';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
const grid = {
|
||||
gutter: 16,
|
||||
|
@ -29,15 +31,9 @@ export const TemplateList: React.FC<IProps> = ({
|
|||
onClosePreview,
|
||||
pageSize = 5,
|
||||
}) => {
|
||||
const { data, loading, error, refresh } = hook();
|
||||
const [page, onPageChange] = useState(1);
|
||||
|
||||
const arr = useMemo(() => {
|
||||
const arr = (data && data.data) || [];
|
||||
const start = (page - 1) * pageSize;
|
||||
const end = page * pageSize;
|
||||
return arr.slice(start, end);
|
||||
}, [data, page, pageSize]);
|
||||
const { data, loading, error, page, setPage, refresh } = hook(pageSize);
|
||||
const list = (data && data.data) || [];
|
||||
const total = (data && data.total) || 0;
|
||||
|
||||
useEffect(() => {
|
||||
refresh();
|
||||
|
@ -62,7 +58,7 @@ export const TemplateList: React.FC<IProps> = ({
|
|||
<>
|
||||
<List
|
||||
grid={grid}
|
||||
dataSource={firstListItem ? [{}, ...arr] : arr}
|
||||
dataSource={firstListItem ? [{}, ...list] : list}
|
||||
renderItem={(template, idx) => {
|
||||
if (idx === 0 && firstListItem) {
|
||||
return <List.Item>{firstListItem}</List.Item>;
|
||||
|
@ -82,7 +78,7 @@ export const TemplateList: React.FC<IProps> = ({
|
|||
}}
|
||||
emptyContent={<Empty message={'暂无模板'} />}
|
||||
></List>
|
||||
{data.data.length > pageSize ? (
|
||||
{total > pageSize ? (
|
||||
<Pagination
|
||||
size="small"
|
||||
style={{
|
||||
|
@ -91,9 +87,9 @@ export const TemplateList: React.FC<IProps> = ({
|
|||
justifyContent: 'center',
|
||||
}}
|
||||
pageSize={pageSize}
|
||||
total={data.data.length}
|
||||
total={total}
|
||||
currentPage={page}
|
||||
onChange={(cPage) => onPageChange(cPage)}
|
||||
onChange={(cPage) => setPage(cPage)}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
import React from 'react';
|
||||
|
||||
import { Spin } from '@douyinfe/semi-ui';
|
||||
|
||||
import { ReaderEditor } from 'tiptap/editor';
|
||||
|
||||
import { DataRender } from 'components/data-render';
|
||||
import { Seo } from 'components/seo';
|
||||
import { useTemplate } from 'data/template';
|
||||
import React from 'react';
|
||||
import { ReaderEditor } from 'tiptap/editor';
|
||||
|
||||
interface IProps {
|
||||
templateId: string;
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import React, { useCallback } from 'react';
|
||||
|
||||
import { IconDesktop, IconMoon, IconSun } from '@douyinfe/semi-icons';
|
||||
import { Button, Dropdown } from '@douyinfe/semi-ui';
|
||||
|
||||
import { Theme as ThemeState, ThemeEnum } from 'hooks/use-theme';
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
export const Theme = () => {
|
||||
const { userPrefer, theme, toggle } = ThemeState.useHook();
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import React from 'react';
|
||||
|
||||
import { Tooltip as SemiTooltip } from '@douyinfe/semi-ui';
|
||||
import { Position } from '@douyinfe/semi-ui/tooltip';
|
||||
|
||||
import { useToggle } from 'hooks/use-toggle';
|
||||
import React from 'react';
|
||||
|
||||
interface IProps {
|
||||
content: React.ReactNode;
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import React from 'react';
|
||||
|
||||
import { IconUpload } from '@douyinfe/semi-icons';
|
||||
import { Button, Toast, Upload as SemiUpload } from '@douyinfe/semi-ui';
|
||||
|
||||
import { useAsyncLoading } from 'hooks/use-async-loading';
|
||||
import React from 'react';
|
||||
import { uploadFile } from 'services/file';
|
||||
|
||||
interface IProps {
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import React, { useCallback } from 'react';
|
||||
|
||||
import { IconSpin } from '@douyinfe/semi-icons';
|
||||
import { Avatar, Button, Dropdown, Modal, Toast, Typography } from '@douyinfe/semi-ui';
|
||||
|
||||
import { useUser } from 'data/user';
|
||||
import { useToggle } from 'hooks/use-toggle';
|
||||
import Router from 'next/router';
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import { ResetPassword } from './reset-password';
|
||||
import { UserSetting } from './setting';
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import React, { useCallback, useState } from 'react';
|
||||
|
||||
import { Button, Col, Form, Row, Toast } from '@douyinfe/semi-ui';
|
||||
|
||||
import { useResetPassword, useSystemPublicConfig, useUser, useVerifyCode } from 'data/user';
|
||||
import { useInterval } from 'hooks/use-interval';
|
||||
import { useToggle } from 'hooks/use-toggle';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
|
||||
export const ResetPassword = ({ onSuccess }) => {
|
||||
const [email, setEmail] = useState('');
|
||||
|
@ -53,7 +55,7 @@ export const ResetPassword = ({ onSuccess }) => {
|
|||
|
||||
return (
|
||||
<Form
|
||||
initValues={{ email: user.email, password: '', confirmPassword: '' }}
|
||||
initValues={{ email: user ? user.email : '', password: '', confirmPassword: '' }}
|
||||
onChange={onFormChange}
|
||||
onSubmit={onFinish}
|
||||
>
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
import { Dispatch, SetStateAction, useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { Avatar, Button, Col, Form, Modal, Row, Space, Toast } from '@douyinfe/semi-ui';
|
||||
import { FormApi } from '@douyinfe/semi-ui/lib/es/form';
|
||||
|
||||
import { Upload } from 'components/upload';
|
||||
import { useSystemPublicConfig, useUser, useVerifyCode } from 'data/user';
|
||||
import { useInterval } from 'hooks/use-interval';
|
||||
import { useToggle } from 'hooks/use-toggle';
|
||||
import { Dispatch, SetStateAction, useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
interface IProps {
|
||||
visible: boolean;
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import React from 'react';
|
||||
|
||||
import { Button } from '@douyinfe/semi-ui';
|
||||
|
||||
import { WikiCreator as WikiCreatorForm } from 'components/wiki/create';
|
||||
import { useToggle } from 'hooks/use-toggle';
|
||||
import React from 'react';
|
||||
|
||||
export const WikiCreator: React.FC = ({ children }) => {
|
||||
const [visible, toggleVisible] = useToggle(false);
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
import React from 'react';
|
||||
|
||||
import { IconPlus } from '@douyinfe/semi-icons';
|
||||
import { Button, Dropdown } from '@douyinfe/semi-ui';
|
||||
|
||||
import { DocumentCreator } from 'components/document/create';
|
||||
import { WikiCreator } from 'components/wiki/create';
|
||||
import { useRouterQuery } from 'hooks/use-router-query';
|
||||
import { useToggle } from 'hooks/use-toggle';
|
||||
import React from 'react';
|
||||
|
||||
interface IProps {
|
||||
onCreateDocument?: () => void;
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { IconUser } from '@douyinfe/semi-icons';
|
||||
import { Avatar, Skeleton, Space, Typography } from '@douyinfe/semi-ui';
|
||||
|
||||
import { IconDocument } from 'components/icons/IconDocument';
|
||||
import { LocaleTime } from 'components/locale-time';
|
||||
import { WikiStar } from 'components/wiki/star';
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
import { Dispatch, SetStateAction, useRef } from 'react';
|
||||
|
||||
import { Form, Modal } from '@douyinfe/semi-ui';
|
||||
import { FormApi } from '@douyinfe/semi-ui/lib/es/form';
|
||||
|
||||
import type { IWiki } from '@think/domains';
|
||||
|
||||
import { ICreateWiki, useOwnWikis } from 'data/wiki';
|
||||
import { useRouterQuery } from 'hooks/use-router-query';
|
||||
import Router from 'next/router';
|
||||
import { Dispatch, SetStateAction, useRef } from 'react';
|
||||
|
||||
interface IProps {
|
||||
visible: boolean;
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import React, { useCallback } from 'react';
|
||||
|
||||
import { IconDelete } from '@douyinfe/semi-icons';
|
||||
import { Modal, Space, Typography } from '@douyinfe/semi-ui';
|
||||
|
||||
import { useOwnWikis } from 'data/wiki';
|
||||
import { useRouterQuery } from 'hooks/use-router-query';
|
||||
import Router from 'next/router';
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
interface IProps {
|
||||
wikiId: string;
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { IconClose } from '@douyinfe/semi-icons';
|
||||
import { Banner, Button, Checkbox, Toast, Transfer, Typography } from '@douyinfe/semi-ui';
|
||||
|
||||
import { isPublicDocument } from '@think/domains';
|
||||
|
||||
import { flattenTree2Array } from 'components/wiki/tocs/utils';
|
||||
import { useWikiDetail, useWikiTocs } from 'data/wiki';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import styles from './index.module.scss';
|
||||
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import { Avatar, Skeleton, Space, Typography } from '@douyinfe/semi-ui';
|
||||
|
||||
import { IWiki } from '@think/domains';
|
||||
|
||||
import { IconDocument } from 'components/icons/IconDocument';
|
||||
import { LocaleTime } from 'components/locale-time';
|
||||
import { WikiStar } from 'components/wiki/star';
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { Skeleton } from '@douyinfe/semi-ui';
|
||||
import React from 'react';
|
||||
|
||||
import { Skeleton } from '@douyinfe/semi-ui';
|
||||
|
||||
export const WorkspacePlaceholder = () => {
|
||||
const placeholder = (
|
||||
<div
|
||||
|
|
|
@ -1,9 +1,13 @@
|
|||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { Avatar, Button, Form, Toast } from '@douyinfe/semi-ui';
|
||||
import { FormApi } from '@douyinfe/semi-ui/lib/es/form';
|
||||
|
||||
import { WIKI_AVATARS } from '@think/constants';
|
||||
import type { IWiki } from '@think/domains';
|
||||
|
||||
import { ImageUploader } from 'components/image-uploader';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { pick } from 'helpers/pick';
|
||||
|
||||
import styles from './index.module.scss';
|
||||
|
||||
|
@ -22,32 +26,36 @@ interface IProps {
|
|||
update: (arg: IUpdateWIKI) => Promise<void>;
|
||||
}
|
||||
|
||||
const getFormValueFromWiki = (wiki) => {
|
||||
return pick(wiki, ['name', 'description', 'avatar']);
|
||||
};
|
||||
|
||||
export const Base: React.FC<IProps> = ({ wiki, update }) => {
|
||||
const $form = useRef<FormApi>();
|
||||
const [currentCover, setCurrentCover] = useState('');
|
||||
|
||||
const onSubmit = () => {
|
||||
const onSubmit = useCallback(() => {
|
||||
$form.current.validate().then((values) => {
|
||||
update(values).then(() => {
|
||||
Toast.success('操作成功');
|
||||
});
|
||||
});
|
||||
};
|
||||
}, [update]);
|
||||
|
||||
const setCover = (url) => {
|
||||
const setCover = useCallback((url) => {
|
||||
$form.current.setValue('avatar', url);
|
||||
setCurrentCover(url);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!wiki) return;
|
||||
$form.current.setValues(wiki);
|
||||
$form.current.setValues(getFormValueFromWiki(wiki));
|
||||
setCurrentCover(wiki.avatar);
|
||||
}, [wiki]);
|
||||
|
||||
return (
|
||||
<Form
|
||||
initValues={wiki}
|
||||
initValues={getFormValueFromWiki(wiki)}
|
||||
style={{ width: '100%' }}
|
||||
getFormApi={(formApi) => ($form.current = formApi)}
|
||||
onSubmit={onSubmit}
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { Button, Toast, Typography, Upload } from '@douyinfe/semi-ui';
|
||||
|
||||
import type { IWiki } from '@think/domains';
|
||||
|
||||
import { useCreateDocument } from 'data/document';
|
||||
import { useRouterQuery } from 'hooks/use-router-query';
|
||||
import { useToggle } from 'hooks/use-toggle';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { createMarkdownParser, MarkdownParse } from './parser';
|
||||
|
||||
|
@ -94,7 +97,7 @@ export const Import: React.FC<IProps> = ({ wikiId }) => {
|
|||
<div style={{ marginTop: 16 }}>
|
||||
<Upload
|
||||
action=""
|
||||
accept="text/markdown"
|
||||
accept=".md,.MD,.Md,.mD"
|
||||
draggable
|
||||
multiple
|
||||
ref={$upload}
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
import { Toast } from '@douyinfe/semi-ui';
|
||||
import { safeJSONStringify } from 'helpers/json';
|
||||
|
||||
import { createEditor } from 'tiptap/core';
|
||||
import { AllExtensions } from 'tiptap/core/all-kit';
|
||||
import { Collaboration } from 'tiptap/core/extensions/collaboration';
|
||||
import { prosemirrorJSONToYDoc } from 'tiptap/core/thritypart/y-prosemirror/y-prosemirror';
|
||||
import { markdownToProsemirror } from 'tiptap/markdown/markdown-to-prosemirror';
|
||||
|
||||
import { safeJSONStringify } from 'helpers/json';
|
||||
import * as Y from 'yjs';
|
||||
|
||||
export interface MarkdownParse {
|
||||
|
@ -23,6 +25,7 @@ export const createMarkdownParser = () => {
|
|||
const parse = (filename: string, markdown: string) => {
|
||||
try {
|
||||
const prosemirrorNode = markdownToProsemirror({
|
||||
editor,
|
||||
schema: editor.schema,
|
||||
content: markdown,
|
||||
needTitle: true,
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
import React from 'react';
|
||||
|
||||
import { TabPane, Tabs } from '@douyinfe/semi-ui';
|
||||
|
||||
import { IWiki } from '@think/domains';
|
||||
|
||||
import { Seo } from 'components/seo';
|
||||
import { WikiTocsManager } from 'components/wiki/tocs/manager';
|
||||
import { useWikiDetail } from 'data/wiki';
|
||||
import React from 'react';
|
||||
|
||||
import { Base } from './base';
|
||||
import { Import } from './import';
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { Banner, Button, Typography } from '@douyinfe/semi-ui';
|
||||
|
||||
import { WikiDeletor } from 'components/wiki/delete';
|
||||
|
||||
interface IProps {
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { IconClose } from '@douyinfe/semi-icons';
|
||||
import { Banner, Button, Checkbox, Radio, RadioGroup, Toast, Transfer, Typography } from '@douyinfe/semi-ui';
|
||||
|
||||
import { isPublicDocument, isPublicWiki, WIKI_STATUS_LIST } from '@think/domains';
|
||||
|
||||
import { flattenTree2Array } from 'components/wiki/tocs/utils';
|
||||
import { useWikiDetail, useWikiTocs } from 'data/wiki';
|
||||
import { buildUrl } from 'helpers/url';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import styles from './index.module.scss';
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import React from 'react';
|
||||
|
||||
import { Members } from 'components/members';
|
||||
import { useWikiMembers } from 'data/wiki';
|
||||
import React from 'react';
|
||||
|
||||
interface IProps {
|
||||
wikiId: string;
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
import React from 'react';
|
||||
|
||||
import { IconStar } from '@douyinfe/semi-icons';
|
||||
import { Button, Tooltip } from '@douyinfe/semi-ui';
|
||||
|
||||
import { IOrganization, IWiki } from '@think/domains';
|
||||
|
||||
import { useWikiStarToggle } from 'data/star';
|
||||
import React from 'react';
|
||||
|
||||
interface IProps {
|
||||
organizationId: IOrganization['id'];
|
||||
|
|
|
@ -48,6 +48,7 @@
|
|||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
color: var(--semi-color-text-0);
|
||||
|
||||
> span {
|
||||
margin-right: 6px;
|
||||
|
|
|
@ -1,34 +1,38 @@
|
|||
import { useMemo } from 'react';
|
||||
|
||||
import { IconPlus, IconSmallTriangleDown } from '@douyinfe/semi-icons';
|
||||
import { Avatar, Button, Dropdown, Skeleton, Typography } from '@douyinfe/semi-ui';
|
||||
|
||||
import { IDocument } from '@think/domains';
|
||||
|
||||
import cls from 'classnames';
|
||||
import { DataRender } from 'components/data-render';
|
||||
import { IconOverview, IconSetting } from 'components/icons';
|
||||
import { findParents } from 'components/wiki/tocs/utils';
|
||||
import { useStarDocumentsInWiki, useStarWikisInOrganization } from 'data/star';
|
||||
import { useWikiDetail, useWikiTocs } from 'data/wiki';
|
||||
import { triggerCreateDocument } from 'event';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { Tree } from './tree';
|
||||
|
||||
import styles from './index.module.scss';
|
||||
import { Tree } from './tree';
|
||||
|
||||
interface IProps {
|
||||
wikiId: string;
|
||||
documentId?: string;
|
||||
docAsLink?: string;
|
||||
getDocLink?: (arg: IDocument) => string;
|
||||
}
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
const defaultGetDocLink = (document) =>
|
||||
`/app/org/${document.organizationId}/wiki/${document.wikiId}/doc/${document.id}`;
|
||||
|
||||
export const WikiTocs: React.FC<IProps> = ({
|
||||
wikiId,
|
||||
documentId = null,
|
||||
docAsLink = '/app/org/[organizationId]/wiki/[wikiId]/doc/[documentId]',
|
||||
getDocLink = (document) => `/app/org/${document.organizationId}/wiki/${document.wikiId}/doc/${document.id}`,
|
||||
getDocLink = defaultGetDocLink,
|
||||
}) => {
|
||||
const { pathname, query } = useRouter();
|
||||
const { data: wiki, loading: wikiLoading, error: wikiError } = useWikiDetail(wikiId);
|
||||
|
@ -39,15 +43,8 @@ export const WikiTocs: React.FC<IProps> = ({
|
|||
loading: starDocumentsLoading,
|
||||
error: starDocumentsError,
|
||||
} = useStarDocumentsInWiki(query.organizationId, wikiId);
|
||||
const [parentIds, setParentIds] = useState<Array<string>>([]);
|
||||
const otherStarWikis = useMemo(() => (starWikis || []).filter((wiki) => wiki.id !== wikiId), [starWikis, wikiId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!tocs || !tocs.length) return;
|
||||
const parentIds = findParents(tocs, documentId);
|
||||
setParentIds(parentIds);
|
||||
}, [tocs, documentId]);
|
||||
|
||||
return (
|
||||
<div className={styles.wrap}>
|
||||
<header>
|
||||
|
@ -139,7 +136,9 @@ export const WikiTocs: React.FC<IProps> = ({
|
|||
</Avatar>
|
||||
<Text strong>{wiki.name}</Text>
|
||||
</span>
|
||||
<IconSmallTriangleDown />
|
||||
<Text>
|
||||
<IconSmallTriangleDown />
|
||||
</Text>
|
||||
</div>
|
||||
</Dropdown>
|
||||
) : (
|
||||
|
@ -276,15 +275,7 @@ export const WikiTocs: React.FC<IProps> = ({
|
|||
<DataRender
|
||||
loading={starDocumentsLoading}
|
||||
error={starDocumentsError}
|
||||
normalContent={() => (
|
||||
<Tree
|
||||
data={starDocuments || []}
|
||||
docAsLink={docAsLink}
|
||||
getDocLink={getDocLink}
|
||||
parentIds={parentIds}
|
||||
activeId={documentId}
|
||||
/>
|
||||
)}
|
||||
normalContent={() => <Tree data={starDocuments || []} docAsLink={docAsLink} getDocLink={getDocLink} />}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
@ -315,14 +306,7 @@ export const WikiTocs: React.FC<IProps> = ({
|
|||
loading={tocsLoading}
|
||||
error={tocsError}
|
||||
normalContent={() => (
|
||||
<Tree
|
||||
needAddDocument
|
||||
data={tocs || []}
|
||||
docAsLink={docAsLink}
|
||||
getDocLink={getDocLink}
|
||||
parentIds={parentIds}
|
||||
activeId={documentId}
|
||||
/>
|
||||
<Tree needAddDocument data={tocs || []} docAsLink={docAsLink} getDocLink={getDocLink} />
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue