Merge pull request #100 from fantasticit/feat/ui

improve ui
pull/114/head
fantasticit 2022-06-23 18:20:22 +08:00 committed by GitHub
commit 0fa5cca4bc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
66 changed files with 1428 additions and 961 deletions

View File

@ -0,0 +1,8 @@
.hoverVisible {
opacity: 0;
&:hover,
&.isActive {
opacity: 1;
}
}

View File

@ -1,20 +1,31 @@
import { IconMore, IconPlus, IconStar } from '@douyinfe/semi-icons';
import { Button, Dropdown, Popover, Space, Typography } from '@douyinfe/semi-ui';
import { IconArticle, IconBranch, 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 cls from 'classnames';
import { DocumentCreator } from 'components/document/create';
import { DocumentDeletor } from 'components/document/delete';
import { DocumentLinkCopyer } from 'components/document/link';
import { DocumentShare } from 'components/document/share';
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';
interface IProps {
wikiId: string;
documentId: string;
hoverVisible?: boolean;
onStar?: () => void;
onCreate?: () => void;
onDelete?: () => void;
onVisibleChange?: () => void;
showCreateDocument?: boolean;
size?: ButtonProps['size'];
hideDocumentVersion?: boolean;
hideDocumentStyle?: boolean;
}
const { Text } = Typography;
@ -22,12 +33,15 @@ const { Text } = Typography;
export const DocumentActions: React.FC<IProps> = ({
wikiId,
documentId,
hoverVisible,
onStar,
onCreate,
onDelete,
onVisibleChange,
showCreateDocument,
children,
size = 'default',
hideDocumentVersion = false,
hideDocumentStyle = false,
}) => {
const [popoverVisible, togglePopoverVisible] = useToggle(false);
const [createVisible, toggleCreateVisible] = useToggle(false);
@ -52,9 +66,10 @@ export const DocumentActions: React.FC<IProps> = ({
return (
<>
<Popover
showArrow
<Dropdown
style={{ padding: 0 }}
trigger="click"
position="bottomLeft"
visible={popoverVisible}
onVisibleChange={wrapOnVisibleChange}
content={
@ -70,7 +85,25 @@ export const DocumentActions: React.FC<IProps> = ({
</Dropdown.Item>
)}
<DocumentShare
key="share"
documentId={documentId}
render={({ isPublic, toggleVisible }) => {
return (
<Dropdown.Item onClick={toggleVisible}>
<Text>
<Space>
<IconBranch />
{isPublic ? '分享中' : '分享'}
</Space>
</Text>
</Dropdown.Item>
);
}}
/>
<DocumentStar
wikiId={wikiId}
documentId={documentId}
render={({ star, toggleStar, text }) => (
<Dropdown.Item
@ -96,10 +129,56 @@ export const DocumentActions: React.FC<IProps> = ({
wikiId={wikiId}
documentId={documentId}
render={({ copy, children }) => {
return <Dropdown.Item onClick={copy}>{children}</Dropdown.Item>;
return (
<Dropdown.Item onClick={copy}>
<Text>{children}</Text>
</Dropdown.Item>
);
}}
/>
{!hideDocumentVersion && (
<DocumentVersionTrigger
key="version"
documentId={documentId}
render={({ onClick }) => {
return (
<Dropdown.Item
onClick={() => {
togglePopoverVisible(false);
onClick();
}}
>
<Text>
<Space>
<IconHistory />
</Space>
</Text>
</Dropdown.Item>
);
}}
/>
)}
{!hideDocumentVersion && (
<DocumentStyle
key="style"
render={({ onClick }) => {
return (
<Dropdown.Item onClick={onClick}>
<Text>
<Space>
<IconArticle />
</Space>
</Text>
</Dropdown.Item>
);
}}
/>
)}
<Dropdown.Divider />
<DocumentDeletor
@ -113,9 +192,17 @@ export const DocumentActions: React.FC<IProps> = ({
</Dropdown.Menu>
}
>
{children || <Button icon={<IconMore />} theme="borderless" type="tertiary" />}
</Popover>
<Button
onClick={(e) => {
e.stopPropagation();
}}
type="tertiary"
size={size}
className={cls(hoverVisible && styles.hoverVisible, popoverVisible && styles.isActive)}
theme={popoverVisible ? 'solid' : 'borderless'}
icon={<IconMore />}
/>
</Dropdown>
{showCreateDocument && (
<DocumentCreator
wikiId={wikiId}

View File

@ -40,7 +40,7 @@ export const DocumentCard: React.FC<{ document: IDocument }> = ({ document }) =>
<Tooltip key="edit" content="编辑" position="bottom">
<Button type="tertiary" theme="borderless" icon={<IconEdit />} onClick={gotoEdit} />
</Tooltip>
<DocumentStar documentId={document.id} />
<DocumentStar wikiId={document.wikiId} documentId={document.id} />
</Space>
</div>
</header>

View File

@ -51,7 +51,6 @@ const renderChecked = (onChange, authKey: 'readable' | 'editable') => (checked,
export const DocumentCollaboration: React.FC<IProps> = ({ wikiId, documentId, disabled = false }) => {
const { isMobile } = IsOnMobile.useHook();
const toastedUsersRef = useRef<Array<IUser['id']>>([]);
const ref = useRef<HTMLInputElement>();
const { user: currentUser } = useUser();
const [visible, toggleVisible] = useToggle(false);
@ -86,7 +85,7 @@ export const DocumentCollaboration: React.FC<IProps> = ({ wikiId, documentId, di
<div style={{ marginTop: 16 }}>
<Input ref={ref} placeholder="输入对方用户名" value={inviteUser} onChange={setInviteUser}></Input>
<Paragraph style={{ marginTop: 16 }}>
<span style={{ verticalAlign: 'middle' }}>
<DocumentLinkCopyer wikiId={wikiId} documentId={documentId} />
</span>
@ -151,38 +150,30 @@ export const DocumentCollaboration: React.FC<IProps> = ({ wikiId, documentId, di
}, [visible]);
useEffect(() => {
const handler = (mentionUsers) => {
const newCollaborationUsers = mentionUsers
const handler = (users) => {
const joinUsers = users
.filter(Boolean)
.filter((state) => state.user)
.map((state) => ({ ...state.user, clientId: state.clientId }));
if (
collaborationUsers.length === newCollaborationUsers.length &&
newCollaborationUsers.every((newUser) => {
return collaborationUsers.find((existUser) => existUser.id === newUser.id);
joinUsers
.filter(Boolean)
.filter((joinUser) => {
return joinUser.name !== currentUser.name;
})
) {
return;
}
newCollaborationUsers.forEach((newUser) => {
if (currentUser && newUser.name !== currentUser.name && !toastedUsersRef.current.includes(newUser.id)) {
Toast.info(`${newUser.name}-${newUser.clientId}加入文档`);
toastedUsersRef.current.push(newUser.id);
}
.forEach((joinUser) => {
Toast.info(`${joinUser.name}-${joinUser.clientId}加入文档`);
});
setCollaborationUsers(newCollaborationUsers);
setCollaborationUsers(joinUsers);
};
event.on(JOIN_USER, handler);
return () => {
toastedUsersRef.current = [];
event.off(JOIN_USER, handler);
};
}, [collaborationUsers, currentUser]);
}, [currentUser]);
if (error)
return (

View File

@ -54,7 +54,6 @@ export const DocumentDeletor: React.FC<IProps> = ({ wikiId, documentId, render,
onConfirm={deleteAction}
okButtonProps={{ loading }}
zIndex={1070}
showArrow
>
{render ? render({ children: content }) : content}
</Popconfirm>

View File

@ -3,9 +3,7 @@ import { Button, Nav, Skeleton, Space, Tooltip, Typography } from '@douyinfe/sem
import { DataRender } from 'components/data-render';
import { Divider } from 'components/divider';
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';
import { Seo } from 'components/seo';
import { Theme } from 'components/theme';
@ -20,6 +18,7 @@ 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';
@ -49,12 +48,11 @@ export const DocumentEditor: React.FC<IProps> = ({ documentId }) => {
() => (
<Space>
{document && authority.readable && (
<DocumentCollaboration key="collaboration" wikiId={document.wikiId} documentId={documentId} />
<DocumentCollaboration key={documentId} wikiId={document.wikiId} documentId={documentId} />
)}
<DocumentShare key="share" documentId={documentId} />
<DocumentVersion key="version" documentId={documentId} onSelect={triggerUseDocumentVersion} />
<DocumentStar key="star" documentId={documentId} />
<DocumentStyle />
{document && <DocumentStar key="star" wikiId={document.wikiId} documentId={documentId} />}
{document && <DocumentActions wikiId={document.wikiId} documentId={documentId} />}
<DocumentVersion documentId={documentId} onSelect={triggerUseDocumentVersion} />
</Space>
),
[documentId, document, authority]

View File

@ -18,6 +18,7 @@ import React, { useCallback, useMemo } from 'react';
import { createPortal } from 'react-dom';
import { CollaborationEditor } from 'tiptap/editor';
import { DocumentActions } from '../actions';
import { Author } from './author';
import styles from './index.module.scss';
@ -64,18 +65,17 @@ export const DocumentReader: React.FC<IProps> = ({ documentId }) => {
{document && (
<DocumentCollaboration
disabled={!readable}
key="collaboration"
key={documentId}
wikiId={document.wikiId}
documentId={documentId}
/>
)}
{document && <DocumentStar disabled={!readable} key="star" wikiId={document.wikiId} documentId={documentId} />}
<Tooltip key="edit" content="编辑" position="bottom">
<Button disabled={!editable} icon={<IconEdit />} onMouseDown={gotoEdit} />
</Tooltip>
<DocumentShare disabled={!readable} key="share" documentId={documentId} />
<DocumentVersion disabled={!readable} key="version" documentId={documentId} />
<DocumentStar disabled={!readable} key="star" documentId={documentId} />
<DocumentStyle />
{document && <DocumentActions wikiId={document.wikiId} documentId={documentId} />}
<DocumentVersion documentId={documentId} />
</Space>
);
}, [document, documentId, readable, editable, gotoEdit]);

View File

@ -11,7 +11,7 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
interface IProps {
documentId: string;
disabled?: boolean;
render?: (arg: { isPublic: boolean; disabled: boolean; toggleVisible: (arg: boolean) => void }) => React.ReactNode;
render?: (arg: { isPublic: boolean; disabled: boolean; toggleVisible: () => void }) => React.ReactNode;
}
const { Text } = Typography;
@ -134,6 +134,7 @@ export const DocumentShare: React.FC<IProps> = ({ documentId, disabled = false,
footer={null}
onCancel={toggleVisible}
style={{ maxWidth: '96vw' }}
zIndex={1067}
>
{content}
</Modal>

View File

@ -1,12 +1,14 @@
import { IconStar } from '@douyinfe/semi-icons';
import { Button, Tooltip } from '@douyinfe/semi-ui';
import { useDocumentCollectToggle } from 'data/collector';
import { IDocument, 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';
interface IProps {
documentId: string;
wikiId: IWiki['id'];
documentId: IDocument['id'];
disabled?: boolean;
render?: (arg: {
star: boolean;
@ -16,9 +18,9 @@ interface IProps {
}) => React.ReactNode;
}
export const DocumentStar: React.FC<IProps> = ({ documentId, disabled = false, render }) => {
export const DocumentStar: React.FC<IProps> = ({ wikiId, documentId, disabled = false, render }) => {
const [visible, toggleVisible] = useToggle(false);
const { data, toggle: toggleStar } = useDocumentCollectToggle(documentId, { enabled: visible });
const { data, toggle: toggleStar } = useDocumentStarToggle(wikiId, documentId, { enabled: visible });
const text = data ? '取消收藏' : '收藏文档';
const onViewportChange = useCallback(

View File

@ -10,7 +10,11 @@ import styles from './index.module.scss';
const { Text } = Typography;
export const DocumentStyle = () => {
interface IProps {
render?: (arg: { onClick: () => void }) => React.ReactNode;
}
export const DocumentStyle: React.FC<IProps> = ({ render }) => {
const { isMobile } = IsOnMobile.useHook();
const { width, fontSize, setWidth, setFontSize } = useDocumentStyle();
const [visible, toggleVisible] = useToggle(false);
@ -30,7 +34,12 @@ export const DocumentStyle = () => {
onVisibleChange={toggleVisible}
onClickOutSide={toggleVisible}
content={
<div className={styles.wrap}>
<div
className={styles.wrap}
onClick={(e) => {
e.stopPropagation();
}}
>
<div className={styles.item}>
<Text></Text>
<Text style={{ fontSize: '0.8em' }}> {fontSize}px</Text>
@ -48,7 +57,11 @@ export const DocumentStyle = () => {
</div>
}
>
{render ? (
render({ onClick: toggleVisible })
) : (
<Button icon={<IconArticle />} theme="borderless" type="tertiary" onMouseDown={toggleVisible} />
)}
</Dropdown>
);
};

View File

@ -29,7 +29,7 @@
flex: 1;
&.isMobile {
padding: 0;
padding: 0 0 24px;
overflow: hidden;
> div {

View File

@ -6,24 +6,26 @@ import { DataRender } from 'components/data-render';
import { LocaleTime } from 'components/locale-time';
import { useDocumentVersion } from 'data/document';
import { safeJSONParse } from 'helpers/json';
import { DocumentVersionControl } from 'hooks/use-document-version';
import { IsOnMobile } from 'hooks/use-on-mobile';
import { useToggle } from 'hooks/use-toggle';
import React, { useCallback, useEffect, useState } from 'react';
import { CollaborationKit } from 'tiptap/editor';
import styles from './index.module.scss';
interface IProps {
export interface IProps {
documentId: string;
disabled?: boolean;
onSelect?: (data) => void;
render?: (arg: { onClick: (arg?: any) => void; disabled: boolean }) => React.ReactNode;
}
const { Title, Text } = Typography;
export const DocumentVersion: React.FC<IProps> = ({ documentId, disabled = false, onSelect }) => {
export const DocumentVersion: React.FC<Partial<IProps>> = ({ documentId, onSelect }) => {
const { isMobile } = IsOnMobile.useHook();
const [visible, toggleVisible] = useToggle(false);
const { visible, toggleVisible } = DocumentVersionControl.useHook();
const { data, loading, error, refresh } = useDocumentVersion(documentId, { enabled: visible });
const [selectedVersion, setSelectedVersion] = useState(null);
@ -61,9 +63,6 @@ export const DocumentVersion: React.FC<IProps> = ({ documentId, disabled = false
return (
<>
<Button type="primary" theme="light" disabled={disabled} onClick={toggleVisible}>
</Button>
<Modal
title="历史记录"
fullScreen
@ -139,3 +138,21 @@ export const DocumentVersion: React.FC<IProps> = ({ documentId, disabled = false
</>
);
};
export const DocumentVersionTrigger: React.FC<Partial<IProps>> = ({ render, disabled }) => {
const { toggleVisible } = DocumentVersionControl.useHook();
return (
<>
{render ? (
render({ onClick: toggleVisible, disabled })
) : (
<>
<Button type="primary" theme="light" disabled={disabled} onClick={toggleVisible}>
</Button>
</>
)}
</>
);
};

View File

@ -54,7 +54,7 @@ const List: React.FC<{ data: IDocument[] }> = ({ data }) => {
</div>
</div>
<div className={styles.rightWrap}>
<DocumentStar documentId={doc.id} />
<DocumentStar wikiId={doc.wikiId} documentId={doc.id} />
</div>
</a>
</Link>

View File

@ -3,7 +3,7 @@ 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';
import { IWikiWithIsMember } from 'data/collector';
import { IWikiWithIsMember } from 'data/star';
import Link from 'next/link';
import styles from './index.module.scss';

View File

@ -1,6 +1,7 @@
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';
@ -19,6 +20,9 @@ interface IProps {
const TitleMap = {
base: '基础信息',
privacy: '隐私管理',
tocs: '目录管理',
share: '隐私管理',
documents: '全部文档',
users: '成员管理',
import: '导入文档',
more: '更多',
@ -34,9 +38,15 @@ export const WikiSetting: React.FC<IProps> = ({ wikiId, tab, onNavigate }) => {
<TabPane tab={TitleMap['base']} itemKey="base">
<Base wiki={data} update={update as any} />
</TabPane>
<TabPane tab={TitleMap['users']} itemKey="users">
<Users wikiId={wikiId} />
</TabPane>
<TabPane tab={TitleMap['tocs']} itemKey="tocs">
<WikiTocsManager wikiId={wikiId} />
</TabPane>
<TabPane tab={TitleMap['privacy']} itemKey="privacy">
<Privacy wikiId={wikiId} />
</TabPane>

View File

@ -1,5 +1,4 @@
/* stylelint-disable */
.wrap {
.statusWrap {
padding: 10px 12px;
margin-top: 16px;
@ -10,4 +9,56 @@
margin-bottom: 16px;
}
}
.selectedItem {
:global {
.semi-icon-close {
color: var(--semi-color-tertiary);
visibility: hidden;
}
}
&:hover {
:global {
.semi-icon-close {
visibility: visible;
}
}
}
}
.selectedItem,
.sourceItem {
display: flex;
height: 36px;
padding: 10px 12px;
box-sizing: border-box;
align-items: center;
justify-content: space-between;
&:hover {
background-color: var(--semi-color-fill-0);
}
.info {
margin-left: 8px;
flex-grow: 1;
}
.name {
font-size: 14px;
line-height: 20px;
}
.email {
font-size: 12px;
line-height: 16px;
color: var(--semi-color-text-2);
}
}
.transferWrap {
width: 100%;
margin-top: 16px;
overflow: auto;
}

View File

@ -16,11 +16,65 @@ interface IProps {
export const Privacy: React.FC<IProps> = ({ wikiId }) => {
const { data: wiki, toggleStatus: toggleWorkspaceStatus } = useWikiDetail(wikiId);
const { data: tocs } = useWikiTocs(wikiId);
const [nextStatus, setNextStatus] = useState('');
const isPublic = useMemo(() => wiki && isPublicWiki(wiki.status), [wiki]);
const documents = useMemo(
() =>
flattenTree2Array(tocs)
.sort((a, b) => a.index - b.index)
.map((d) => {
d.label = d.title;
d.value = d.id;
return d;
}),
[tocs]
);
const [publicDocumentIds, setPublicDocumentIds] = useState([]); // 公开的
const privateDocumentIds = useMemo(() => {
return documents.filter((doc) => !publicDocumentIds.includes(doc.id)).map((doc) => doc.id);
}, [documents, publicDocumentIds]);
const renderSourceItem = useCallback((item) => {
return (
<div className={styles.sourceItem} key={item.id}>
<Checkbox
onChange={() => {
item.onChange();
}}
key={item.label}
checked={item.checked}
>
<Text>{item.title}</Text>
</Checkbox>
</div>
);
}, []);
const renderSelectedItem = useCallback((item) => {
return (
<div className={styles.selectedItem} key={item.id}>
<Text>{item.title}</Text>
<IconClose onClick={item.onRemove} />
</div>
);
}, []);
const customFilter = useCallback((sugInput, item) => {
return item.title.includes(sugInput);
}, []);
useEffect(() => {
if (!documents.length) return;
const activeIds = documents.filter((doc) => isPublicDocument(doc.status)).map((doc) => doc.id);
setPublicDocumentIds(activeIds);
}, [documents]);
const submit = () => {
const data = { nextStatus };
const data = { nextStatus, publicDocumentIds, privateDocumentIds };
toggleWorkspaceStatus(data).then((res) => {
const ret = res as unknown as any & {
documentOperateMessage?: string;
@ -64,6 +118,7 @@ export const Privacy: React.FC<IProps> = ({ wikiId }) => {
}
/>
)}
<div className={styles.statusWrap}>
<Title className={styles.title} heading={6}>
@ -78,6 +133,25 @@ export const Privacy: React.FC<IProps> = ({ wikiId }) => {
})}
</RadioGroup>
</div>
<div
className={styles.transferWrap}
style={{
height: `calc(100vh - ${isPublic ? 426 : 342}px)`,
}}
>
<Transfer
style={{ width: '100%', height: '100%' }}
dataSource={documents}
filter={customFilter}
value={publicDocumentIds}
renderSelectedItem={renderSelectedItem}
renderSourceItem={renderSourceItem}
inputProps={{ placeholder: '搜索文档' }}
onChange={(_, values) => setPublicDocumentIds(values.map((v) => v.id))}
/>
</div>
<Button style={{ marginTop: 16 }} type="primary" theme="solid" onClick={submit}>
</Button>

View File

@ -43,7 +43,9 @@ export const Users: React.FC<IProps> = ({ wikiId }) => {
normalContent={() => (
<div style={{ margin: '24px 0' }}>
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
<Button onClick={toggleVisible}></Button>
<Button onClick={toggleVisible} theme="solid">
</Button>
</div>
<Table style={{ margin: '16px 0' }} dataSource={users} size="small" pagination>
<Column title="用户名" dataIndex="userName" key="userName" />

View File

@ -1,6 +1,6 @@
import { IconStar } from '@douyinfe/semi-icons';
import { Button, Tooltip } from '@douyinfe/semi-ui';
import { useWikiCollectToggle } from 'data/collector';
import { useWikiStarToggle } from 'data/star';
import React from 'react';
interface IProps {
@ -10,7 +10,7 @@ interface IProps {
}
export const WikiStar: React.FC<IProps> = ({ wikiId, render, onChange }) => {
const { data, toggle } = useWikiCollectToggle(wikiId);
const { data, toggle } = useWikiStarToggle(wikiId);
const text = data ? '取消收藏' : '收藏知识库';
return (

View File

@ -1,12 +1,84 @@
/* stylelint-disable */
.wrap {
height: 100%;
padding-top: 12px;
overflow: auto;
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
> main {
flex: 1;
overflow: auto;
}
}
.titleWrap {
display: flex;
padding: 8px 12px;
overflow: hidden;
cursor: pointer;
justify-content: space-between;
align-items: center;
> span {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
&:hover {
background-color: var(--semi-color-fill-0);
}
&.isActive {
font-weight: 600;
color: var(--semi-color-primary);
background-color: var(--semi-color-primary-light-default);
}
}
.linkWrap {
> a {
display: flex;
padding: 8px 12px;
margin: 8px 0;
font-size: 14px;
cursor: pointer;
align-items: center;
> span {
margin-right: 6px;
}
}
&:hover {
background-color: var(--semi-color-fill-0);
}
&.isActive {
font-weight: 600;
color: var(--semi-color-primary);
background-color: var(--semi-color-primary-light-default);
}
}
.treeWrap {
flex: 1;
padding: 0 8px 32px 16px;
overflow: auto;
padding: 8px 12px;
.title {
display: flex;
justify-content: space-between;
align-items: center;
padding-left: 8px;
}
:global {
.semi-tree-option-list {
overflow-y: hidden;
}
}
}
@ -30,18 +102,17 @@
align-items: center;
width: calc(100% - 48px);
}
}
}
.right {
.hoverVisible {
opacity: 0;
}
&:hover {
.right {
&:hover,
&.isActive {
opacity: 1;
}
}
}
}
.navItemWrap {
display: flex;
@ -99,12 +170,4 @@
flex: 1;
}
}
.rightWrap {
padding-right: 4px;
}
}
.docListTitle {
margin: 12px 0.5rem;
}

View File

@ -1,16 +1,17 @@
import { IconPlus } from '@douyinfe/semi-icons';
import { Avatar, Button, Skeleton, Tooltip, Typography } from '@douyinfe/semi-ui';
import { isPublicWiki } from '@think/domains';
import { IconPlus, IconSmallTriangleDown } from '@douyinfe/semi-icons';
import { Avatar, Button, Dropdown, Skeleton, Typography } from '@douyinfe/semi-ui';
import cls from 'classnames';
import { DataRender } from 'components/data-render';
import { IconDocument, IconGlobe, IconOverview, IconSetting } from 'components/icons';
import { IconOverview, IconSetting } from 'components/icons';
import { findParents } from 'components/wiki/tocs/utils';
import { useStarWikis, useWikiStarDocuments } 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, useState } from 'react';
import styles from './index.module.scss';
import { NavItem } from './nav-item';
import { Tree } from './tree';
interface IProps {
@ -28,9 +29,15 @@ export const WikiTocs: React.FC<IProps> = ({
docAsLink = '/wiki/[wikiId]/document/[documentId]',
getDocLink = (documentId) => `/wiki/${wikiId}/document/${documentId}`,
}) => {
const { pathname, query } = useRouter();
const { pathname } = useRouter();
const { data: wiki, loading: wikiLoading, error: wikiError } = useWikiDetail(wikiId);
const { data: tocs, loading: tocsLoading, error: tocsError, refresh } = useWikiTocs(wikiId);
const { data: tocs, loading: tocsLoading, error: tocsError } = useWikiTocs(wikiId);
const { data: starWikis } = useStarWikis();
const {
data: starDocuments,
loading: starDocumentsLoading,
error: starDocumentsError,
} = useWikiStarDocuments(wikiId);
const [parentIds, setParentIds] = useState<Array<string>>([]);
useEffect(() => {
@ -41,11 +48,14 @@ export const WikiTocs: React.FC<IProps> = ({
return (
<div className={styles.wrap}>
<header>
<DataRender
loading={wikiLoading}
loadingContent={
<NavItem
icon={
<div className={styles.titleWrap}>
<Skeleton
placeholder={
<div style={{ display: 'flex' }}>
<Skeleton.Avatar
size="small"
style={{
@ -55,14 +65,38 @@ export const WikiTocs: React.FC<IProps> = ({
borderRadius: 4,
}}
></Skeleton.Avatar>
<Skeleton.Title style={{ width: 120 }} />
</div>
}
text={<Skeleton.Title style={{ width: 120 }} />}
loading={true}
/>
</div>
}
error={wikiError}
normalContent={() => (
<NavItem
icon={
<Dropdown
trigger={'click'}
position="bottomRight"
render={
<Dropdown.Menu style={{ width: 180 }}>
{(starWikis || [])
.filter((wiki) => wiki.id !== wikiId)
.map((wiki) => {
return (
<Dropdown.Item key={wiki.id}>
<Link
href={{
pathname: `/wiki/[wikiId]`,
query: { wikiId: wiki.id },
}}
>
<a
style={{
display: 'flex',
width: '100%',
}}
>
<span>
<Avatar
shape="square"
size="small"
@ -76,102 +110,164 @@ export const WikiTocs: React.FC<IProps> = ({
>
{wiki.name.charAt(0)}
</Avatar>
<Text strong ellipsis={{ rows: 1 }}>
{wiki.name}
</Text>
</span>
</a>
</Link>
</Dropdown.Item>
);
})}
</Dropdown.Menu>
}
text={<Text strong>{wiki.name}</Text>}
hoverable={false}
/>
>
<div className={styles.titleWrap}>
<span>
<Avatar
shape="square"
size="small"
src={wiki.avatar}
style={{
marginRight: 8,
width: 24,
height: 24,
borderRadius: 4,
}}
>
{wiki.name.charAt(0)}
</Avatar>
<Text strong>{wiki.name}</Text>
</span>
<IconSmallTriangleDown />
</div>
</Dropdown>
)}
/>
<NavItem
icon={<IconOverview />}
text={'概述'}
<DataRender
loading={wikiLoading}
loadingContent={
<div className={styles.titleWrap}>
<Skeleton
placeholder={
<div style={{ display: 'flex' }}>
<Skeleton.Avatar
size="small"
style={{
marginRight: 8,
width: 24,
height: 24,
borderRadius: 4,
}}
></Skeleton.Avatar>
<Skeleton.Title style={{ width: 120 }} />
</div>
}
loading={true}
/>
</div>
}
error={wikiError}
normalContent={() => (
<div className={cls(styles.linkWrap, pathname === '/wiki/[wikiId]' && styles.isActive)}>
<Link
href={{
pathname: `/wiki/[wikiId]`,
query: { wikiId },
}}
isActive={pathname === '/wiki/[wikiId]' || (query && wiki && query.documentId === wiki.homeDocumentId)}
>
<a>
<IconOverview style={{ fontSize: '1em' }} />
<span></span>
</a>
</Link>
</div>
)}
/>
<NavItem
icon={<IconSetting />}
text={'设置'}
<DataRender
loading={wikiLoading}
loadingContent={
<div className={styles.titleWrap}>
<Skeleton
placeholder={
<div style={{ display: 'flex' }}>
<Skeleton.Avatar
size="small"
style={{
marginRight: 8,
width: 24,
height: 24,
borderRadius: 4,
}}
></Skeleton.Avatar>
<Skeleton.Title style={{ width: 120 }} />
</div>
}
loading={true}
/>
</div>
}
error={wikiError}
normalContent={() => (
<div className={cls(styles.linkWrap, pathname === '/wiki/[wikiId]/setting' && styles.isActive)}>
<Link
href={{
pathname: `/wiki/[wikiId]/setting`,
query: { tab: 'base', wikiId },
}}
isActive={pathname === '/wiki/[wikiId]/setting'}
>
<a>
<IconSetting style={{ fontSize: '1em' }} />
<span></span>
</a>
</Link>
</div>
)}
/>
</header>
<main>
<div className={styles.treeWrap}>
<DataRender
loading={wikiLoading}
loadingContent={
<NavItem
icon={
<Skeleton.Avatar
size="small"
style={{
marginRight: 8,
width: 24,
height: 24,
borderRadius: 4,
}}
></Skeleton.Avatar>
}
text={<Skeleton.Title style={{ width: 120 }} />}
loading={starDocumentsLoading}
loadingContent={<Skeleton.Title style={{ width: '100%' }} />}
error={starDocumentsError}
normalContent={() => (
<div className={styles.title}>
<Text type="tertiary" size="small">
</Text>
</div>
)}
/>
}
error={wikiError}
normalContent={() =>
isPublicWiki(wiki.status) ? (
<NavItem
icon={<IconGlobe />}
text={
<Tooltip content="该知识库已公开,点我查看" position="right">
</Tooltip>
}
href={{
pathname: `/share/wiki/[wikiId]`,
query: { wikiId },
}}
isActive={pathname === '/share/wiki/[wikiId]'}
openNewTab
<DataRender
loading={starDocumentsLoading}
loadingContent={<div>1</div>}
error={starDocumentsError}
normalContent={() => (
<Tree
data={starDocuments || []}
docAsLink={docAsLink}
getDocLink={getDocLink}
parentIds={parentIds}
activeId={documentId}
/>
) : null
}
)}
/>
</div>
<div className={styles.treeWrap}>
<DataRender
loading={wikiLoading}
loadingContent={
<NavItem
icon={
<Skeleton.Avatar
size="small"
style={{
marginRight: 8,
width: 24,
height: 24,
borderRadius: 4,
}}
></Skeleton.Avatar>
}
text={<Skeleton.Title style={{ width: 120 }} />}
rightNode={<IconPlus />}
/>
}
loading={tocsLoading}
loadingContent={<Skeleton.Title style={{ width: '100%' }} />}
error={wikiError}
normalContent={() => (
<NavItem
icon={<IconDocument />}
text={'文档管理'}
href={{
pathname: `/wiki/[wikiId]/documents`,
query: { wikiId },
}}
isActive={pathname === '/wiki/[wikiId]/documents'}
rightNode={
<div className={styles.title}>
<Text type="tertiary" size="small">
</Text>
<Button
style={{ fontSize: '1em' }}
theme="borderless"
@ -182,18 +278,16 @@ export const WikiTocs: React.FC<IProps> = ({
triggerCreateDocument({ wikiId: wiki.id, documentId: null });
}}
/>
}
/>
</div>
)}
/>
<div className={styles.treeWrap}>
<DataRender
loading={tocsLoading}
loadingContent={<NavItem icon={null} text={<Skeleton.Title style={{ width: '100%' }} />} />}
loadingContent={<div>1</div>}
error={tocsError}
normalContent={() => (
<Tree
needAddDocument
data={tocs || []}
docAsLink={docAsLink}
getDocLink={getDocLink}
@ -203,6 +297,7 @@ export const WikiTocs: React.FC<IProps> = ({
)}
/>
</div>
</main>
</div>
);
};

View File

@ -1,8 +1,8 @@
.wrap {
margin-top: 5px;
margin-top: 16px;
.tocsWrap {
height: calc(100vh - 268px);
height: calc(100vh - 279px);
overflow: auto;
border: 1px solid var(--semi-color-border);
border-radius: var(--border-radius);

View File

@ -133,8 +133,8 @@ export const WikiTocsManager: React.FC<IProps> = ({ wikiId }) => {
/>
</div>
<div className={styles.btnWrap}>
<Button disabled={!changed} onClick={submit}>
<Button disabled={!changed} onClick={submit} theme="solid">
</Button>
</div>
</div>

View File

@ -43,6 +43,7 @@ export const WikiPublicTocs: React.FC<IProps> = ({
return (
<div className={styles.wrap}>
<header>
<div className={styles.navItemWrap}>
<div className={styles.navItem}>
<Space>
@ -105,8 +106,10 @@ export const WikiPublicTocs: React.FC<IProps> = ({
}}
isActive={pathname === '/share/wiki/[wikiId]'}
/>
</header>
<div className={styles.treeWrap} style={{ marginTop: 12 }}>
<main>
<div className={styles.treeWrap}>
<DataRender
loading={tocsLoading}
loadingContent={
@ -139,6 +142,7 @@ export const WikiPublicTocs: React.FC<IProps> = ({
)}
/>
</div>
</main>
</div>
);
};

View File

@ -1,4 +1,4 @@
import { IconMore, IconPlus } from '@douyinfe/semi-icons';
import { IconPlus } from '@douyinfe/semi-icons';
import { Button, Tree as SemiTree, Typography } from '@douyinfe/semi-ui';
import { DocumentActions } from 'components/document/actions';
import { DocumentCreator as DocumenCreatorForm } from 'components/document/create';
@ -13,18 +13,17 @@ import styles from './index.module.scss';
const Actions = ({ node }) => {
return (
<span className={styles.right}>
<DocumentActions wikiId={node.wikiId} documentId={node.id}>
<Button
onClick={(e) => {
e.stopPropagation();
}}
type="tertiary"
theme="borderless"
icon={<IconMore />}
<DocumentActions
key={node.id}
hoverVisible
wikiId={node.wikiId}
documentId={node.id}
size="small"
/>
</DocumentActions>
hideDocumentVersion
hideDocumentStyle
></DocumentActions>
<Button
className={styles.hoverVisible}
onClick={(e) => {
e.stopPropagation();
triggerCreateDocument({ wikiId: node.wikiId, documentId: node.id });
@ -67,7 +66,15 @@ const AddDocument = () => {
let scrollTimer;
export const Tree = ({ data, docAsLink, getDocLink, parentIds, activeId, isShareMode = false }) => {
export const Tree = ({
data,
docAsLink,
getDocLink,
parentIds,
activeId,
isShareMode = false,
needAddDocument = false,
}) => {
const $container = useRef<HTMLDivElement>(null);
const [expandedKeys, setExpandedKeys] = useState(parentIds);
@ -100,15 +107,13 @@ export const Tree = ({ data, docAsLink, getDocLink, parentIds, activeId, isShare
}, [parentIds]);
useEffect(() => {
clearTimeout(scrollTimer);
scrollTimer = setTimeout(() => {
const target = $container.current.querySelector(`#item-${activeId}`);
if (!target) return;
target.scrollIntoView();
clearTimeout(scrollTimer);
scrollTimer = setTimeout(() => {
scrollIntoView(target, {
behavior: 'smooth',
scrollMode: 'if-needed',
block: 'center',
});
}, 500);
@ -127,7 +132,7 @@ export const Tree = ({ data, docAsLink, getDocLink, parentIds, activeId, isShare
expandedKeys={expandedKeys}
onExpand={(expandedKeys) => setExpandedKeys(expandedKeys)}
/>
<AddDocument />
{needAddDocument && <AddDocument />}
</div>
);
};

View File

@ -1,185 +0,0 @@
import { CollectorApiDefinition, CollectType, IDocument, IWiki } from '@think/domains';
import {
event,
TOGGLE_COLLECT_DOUCMENT,
TOGGLE_COLLECT_WIKI,
triggerToggleCollectDocument,
triggerToggleCollectWiki,
} from 'event';
import { useCallback, useEffect } from 'react';
import { useQuery, UseQueryOptions } from 'react-query';
import { HttpClient } from 'services/http-client';
export type IWikiWithIsMember = IWiki & { isMember?: boolean };
/**
*
* @returns
*/
export const getCollectedWikis = (cookie = null): Promise<IWikiWithIsMember[]> => {
return HttpClient.request({
method: CollectorApiDefinition.wikis.method,
url: CollectorApiDefinition.wikis.client(),
cookie,
});
};
/**
*
* @returns
*/
export const useCollectedWikis = () => {
const { data, error, isLoading, refetch } = useQuery(CollectorApiDefinition.wikis.client(), getCollectedWikis, {
staleTime: 500,
});
useEffect(() => {
event.on(TOGGLE_COLLECT_WIKI, refetch);
return () => {
event.off(TOGGLE_COLLECT_WIKI, refetch);
};
}, [refetch]);
return { data, error, loading: isLoading, refresh: refetch };
};
/**
*
* @param wikiId
* @returns
*/
export const getWikiIsCollected = (wikiId, cookie = null): Promise<boolean> => {
return HttpClient.request({
method: CollectorApiDefinition.check.method,
url: CollectorApiDefinition.check.client(),
cookie,
data: {
type: CollectType.wiki,
targetId: wikiId,
},
});
};
/**
*
* @param wikiId
* @returns
*/
export const toggleCollectWiki = (wikiId, cookie = null): Promise<boolean> => {
return HttpClient.request({
method: CollectorApiDefinition.toggle.method,
url: CollectorApiDefinition.toggle.client(),
cookie,
data: {
type: CollectType.wiki,
targetId: wikiId,
},
});
};
/**
*
* @param wikiId
* @returns
*/
export const useWikiCollectToggle = (wikiId) => {
const { data, error, refetch } = useQuery([CollectorApiDefinition.check.client(), wikiId], () =>
getWikiIsCollected(wikiId)
);
const toggle = useCallback(async () => {
await toggleCollectWiki(wikiId);
refetch();
triggerToggleCollectWiki();
}, [refetch, wikiId]);
return { data, error, toggle };
};
/**
*
* @returns
*/
export const getCollectedDocuments = (cookie = null): Promise<IDocument[]> => {
return HttpClient.request({
method: CollectorApiDefinition.documents.method,
url: CollectorApiDefinition.documents.client(),
cookie,
});
};
/**
*
* @returns
*/
export const useCollectedDocuments = () => {
const { data, error, isLoading, refetch } = useQuery(
CollectorApiDefinition.documents.client(),
getCollectedDocuments,
{ staleTime: 500 }
);
useEffect(() => {
event.on(TOGGLE_COLLECT_DOUCMENT, refetch);
return () => {
event.off(TOGGLE_COLLECT_DOUCMENT, refetch);
};
}, [refetch]);
return { data, error, loading: isLoading, refresh: refetch };
};
/**
*
* @param documentId
* @returns
*/
export const getDocumentIsCollected = (documentId, cookie = null): Promise<boolean> => {
return HttpClient.request({
method: CollectorApiDefinition.check.method,
url: CollectorApiDefinition.check.client(),
cookie,
data: {
type: CollectType.document,
targetId: documentId,
},
});
};
/**
*
* @param wikiId
* @returns
*/
export const toggleCollectDocument = (documentId, cookie = null): Promise<boolean> => {
return HttpClient.request({
method: CollectorApiDefinition.toggle.method,
url: CollectorApiDefinition.toggle.client(),
cookie,
data: {
type: CollectType.document,
targetId: documentId,
},
});
};
/**
*
* @param documentId
* @returns
*/
export const useDocumentCollectToggle = (documentId, options?: UseQueryOptions<boolean>) => {
const { data, error, refetch } = useQuery(
[CollectorApiDefinition.check.client(), documentId],
() => getDocumentIsCollected(documentId),
options
);
const toggle = useCallback(async () => {
await toggleCollectDocument(documentId);
refetch();
triggerToggleCollectDocument();
}, [refetch, documentId]);
return { data, error, toggle };
};

View File

@ -0,0 +1,212 @@
import { IDocument, IWiki, StarApiDefinition } from '@think/domains';
import { event, TOGGLE_STAR_DOUCMENT, TOGGLE_STAR_WIKI, triggerToggleStarDocument, triggerToggleStarWiki } from 'event';
import { useCallback, useEffect } from 'react';
import { useQuery, UseQueryOptions } from 'react-query';
import { HttpClient } from 'services/http-client';
export type IWikiWithIsMember = IWiki & { isMember?: boolean };
/**
*
* @returns
*/
export const getStarWikis = (cookie = null): Promise<IWikiWithIsMember[]> => {
return HttpClient.request({
method: StarApiDefinition.wikis.method,
url: StarApiDefinition.wikis.client(),
cookie,
});
};
/**
*
* @returns
*/
export const useStarWikis = () => {
const { data, error, isLoading, refetch } = useQuery(StarApiDefinition.wikis.client(), getStarWikis, {
staleTime: 500,
});
useEffect(() => {
event.on(TOGGLE_STAR_WIKI, refetch);
return () => {
event.off(TOGGLE_STAR_WIKI, refetch);
};
}, [refetch]);
return { data, error, loading: isLoading, refresh: refetch };
};
/**
*
* @param wikiId
* @returns
*/
export const getWikiIsStar = (wikiId, cookie = null): Promise<boolean> => {
return HttpClient.request({
method: StarApiDefinition.check.method,
url: StarApiDefinition.check.client(),
cookie,
data: {
wikiId,
},
});
};
/**
*
* @param wikiId
* @returns
*/
export const toggleStarWiki = (wikiId, cookie = null): Promise<boolean> => {
return HttpClient.request({
method: StarApiDefinition.toggle.method,
url: StarApiDefinition.toggle.client(),
cookie,
data: {
wikiId,
},
});
};
/**
*
* @param wikiId
* @returns
*/
export const useWikiStarToggle = (wikiId) => {
const { data, error, refetch } = useQuery([StarApiDefinition.check.client(), wikiId], () => getWikiIsStar(wikiId));
const toggle = useCallback(async () => {
await toggleStarWiki(wikiId);
refetch();
triggerToggleStarWiki();
}, [refetch, wikiId]);
return { data, error, toggle };
};
/**
*
* @returns
*/
export const getStarDocuments = (cookie = null): Promise<IDocument[]> => {
return HttpClient.request({
method: StarApiDefinition.documents.method,
url: StarApiDefinition.documents.client(),
cookie,
});
};
/**
*
* @returns
*/
export const useStarDocuments = () => {
const { data, error, isLoading, refetch } = useQuery(StarApiDefinition.documents.client(), getStarDocuments, {
staleTime: 500,
});
useEffect(() => {
event.on(TOGGLE_STAR_DOUCMENT, refetch);
return () => {
event.off(TOGGLE_STAR_DOUCMENT, refetch);
};
}, [refetch]);
return { data, error, loading: isLoading, refresh: refetch };
};
/**
*
* @param documentId
* @returns
*/
export const getDocumentIsStar = (wikiId, documentId, cookie = null): Promise<boolean> => {
return HttpClient.request({
method: StarApiDefinition.check.method,
url: StarApiDefinition.check.client(),
cookie,
data: {
wikiId,
documentId,
},
});
};
/**
*
* @param wikiId
* @returns
*/
export const toggleDocumentStar = (wikiId, documentId, cookie = null): Promise<boolean> => {
return HttpClient.request({
method: StarApiDefinition.toggle.method,
url: StarApiDefinition.toggle.client(),
cookie,
data: {
wikiId,
documentId,
},
});
};
/**
*
* @param documentId
* @returns
*/
export const useDocumentStarToggle = (wikiId, documentId, options?: UseQueryOptions<boolean>) => {
const { data, error, refetch } = useQuery(
[StarApiDefinition.check.client(), wikiId, documentId],
() => getDocumentIsStar(wikiId, documentId),
options
);
const toggle = useCallback(async () => {
await toggleDocumentStar(wikiId, documentId);
refetch();
triggerToggleStarDocument();
}, [refetch, wikiId, documentId]);
return { data, error, toggle };
};
/**
*
* @returns
*/
export const getWikiStarDocuments = (wikiId, cookie = null): Promise<IWikiWithIsMember[]> => {
return HttpClient.request({
method: StarApiDefinition.wikiDocuments.method,
url: StarApiDefinition.wikiDocuments.client(),
cookie,
params: {
wikiId,
},
});
};
/**
*
* @returns
*/
export const useWikiStarDocuments = (wikiId) => {
const { data, error, isLoading, refetch } = useQuery(
[StarApiDefinition.wikiDocuments.client(), wikiId],
() => getWikiStarDocuments(wikiId),
{
staleTime: 500,
}
);
useEffect(() => {
event.on(TOGGLE_STAR_DOUCMENT, refetch);
return () => {
event.off(TOGGLE_STAR_DOUCMENT, refetch);
};
}, [refetch]);
return { data, error, loading: isLoading, refresh: refetch };
};

View File

@ -5,8 +5,8 @@ export const event = new EventEmitter();
export const REFRESH_TOCS = `REFRESH_TOCS`; // 刷新知识库目录
export const CREATE_DOCUMENT = `CREATE_DOCUMENT`;
export const TOGGLE_COLLECT_WIKI = `TOGGLE_COLLECT_WIKI`; // 收藏或取消收藏知识库
export const TOGGLE_COLLECT_DOUCMENT = `TOGGLE_COLLECT_DOUCMENT`; // 收藏或取消收藏文档
export const TOGGLE_STAR_WIKI = `TOGGLE_STAR_WIKI`; // 收藏或取消收藏知识库
export const TOGGLE_STAR_DOUCMENT = `TOGGLE_STAR_DOUCMENT`; // 收藏或取消收藏文档
/**
*
@ -53,10 +53,10 @@ export const triggerJoinUser = (users: Array<CollaborationUser>) => {
event.emit(JOIN_USER, users);
};
export const triggerToggleCollectWiki = () => {
event.emit(TOGGLE_COLLECT_WIKI);
export const triggerToggleStarWiki = () => {
event.emit(TOGGLE_STAR_WIKI);
};
export const triggerToggleCollectDocument = () => {
event.emit(TOGGLE_COLLECT_DOUCMENT);
export const triggerToggleStarDocument = () => {
event.emit(TOGGLE_STAR_DOUCMENT);
};

View File

@ -0,0 +1,16 @@
import { createGlobalHook } from './create-global-hook';
import { useToggle } from './use-toggle';
const useDocumentVersion = (defaultVisible) => {
const [visible, toggleVisible] = useToggle(defaultVisible);
return {
visible,
toggleVisible,
};
};
export const DocumentVersionControl = createGlobalHook<
{ visible?: boolean; toggleVisible: (arg?: any) => void },
boolean
>(useDocumentVersion);

View File

@ -62,7 +62,7 @@ export const RecentDocs = ({ visible }) => {
</div>
</div>
<div className={styles.rightWrap}>
<DocumentStar documentId={doc.id} />
<DocumentStar wikiId={doc.wikiId} documentId={doc.id} />
</div>
</a>
</Link>

View File

@ -3,7 +3,7 @@ import { Avatar, Dropdown, Modal, Space, Typography } from '@douyinfe/semi-ui';
import { DataRender } from 'components/data-render';
import { Empty } from 'components/empty';
import { WikiStar } from 'components/wiki/star';
import { useCollectedWikis } from 'data/collector';
import { useStarWikis } from 'data/star';
import { useWikiDetail } from 'data/wiki';
import Link from 'next/link';
import { useRouter } from 'next/router';
@ -16,7 +16,7 @@ const { Text } = Typography;
const WikiContent = () => {
const { query } = useRouter();
const { data: starWikis, loading, error, refresh: refreshStarWikis } = useCollectedWikis();
const { data: starWikis, loading, error, refresh: refreshStarWikis } = useStarWikis();
const { data: currentWiki } = useWikiDetail(query.wikiId);
return (

View File

@ -4,6 +4,7 @@ import 'styles/globals.scss';
import 'tiptap/core/styles/index.scss';
import { isMobile } from 'helpers/env';
import { DocumentVersionControl } from 'hooks/use-document-version';
import { IsOnMobile } from 'hooks/use-on-mobile';
import { Theme } from 'hooks/use-theme';
import App from 'next/app';
@ -87,7 +88,9 @@ class MyApp extends App<{ isMobile: boolean }> {
<Hydrate state={pageProps.dehydratedState}>
<Theme.Provider>
<IsOnMobile.Provider initialState={isMobile}>
<DocumentVersionControl.Provider initialState={false}>
<Component {...pageProps} />
</DocumentVersionControl.Provider>
</IsOnMobile.Provider>
</Theme.Provider>
</Hydrate>

View File

@ -1,5 +1,5 @@
import { Avatar, Button, List, Table, Typography } from '@douyinfe/semi-ui';
import { CollectorApiDefinition, DocumentApiDefinition, IDocument } from '@think/domains';
import { DocumentApiDefinition, IDocument, StarApiDefinition } from '@think/domains';
import { DataRender } from 'components/data-render';
import { DocumentActions } from 'components/document/actions';
import { Empty } from 'components/empty';
@ -7,8 +7,8 @@ import { LocaleTime } from 'components/locale-time';
import { Seo } from 'components/seo';
import { WikiCreator } from 'components/wiki/create';
import { WikiPinCard, WikiPinCardPlaceholder } from 'components/wiki/pin-card';
import { getCollectedWikis, useCollectedWikis } from 'data/collector';
import { getRecentVisitedDocuments, useRecentDocuments } from 'data/document';
import { getStarWikis, useStarWikis } from 'data/star';
import { useToggle } from 'hooks/use-toggle';
import { SingleColumnLayout } from 'layouts/single-column';
import type { NextPage } from 'next';
@ -78,7 +78,14 @@ const RecentDocs = () => {
key="operate"
width={80}
render={(_, document) => (
<DocumentActions wikiId={document.wikiId} documentId={document.id} onDelete={refresh} showCreateDocument />
<DocumentActions
wikiId={document.wikiId}
documentId={document.id}
onDelete={refresh}
showCreateDocument
hideDocumentVersion
hideDocumentStyle
/>
)}
/>,
],
@ -118,7 +125,7 @@ const RecentDocs = () => {
const Page: NextPage = () => {
const [visible, toggleVisible] = useToggle(false);
const { data: staredWikis, loading, error, refresh } = useCollectedWikis();
const { data: staredWikis, loading, error, refresh } = useStarWikis();
return (
<SingleColumnLayout>
@ -168,7 +175,7 @@ const Page: NextPage = () => {
Page.getInitialProps = async (ctx) => {
const props = await serverPrefetcher(ctx, [
{ url: CollectorApiDefinition.wikis.client(), action: (cookie) => getCollectedWikis(cookie) },
{ url: StarApiDefinition.wikis.client(), action: (cookie) => getStarWikis(cookie) },
{ url: DocumentApiDefinition.recent.client(), action: (cookie) => getRecentVisitedDocuments(cookie) },
]);
return props;

View File

@ -1,11 +1,11 @@
import { List, Typography } from '@douyinfe/semi-ui';
import { CollectorApiDefinition } from '@think/domains';
import { StarApiDefinition } from '@think/domains';
import { DataRender } from 'components/data-render';
import { DocumentCard, DocumentCardPlaceholder } from 'components/document/card';
import { Empty } from 'components/empty';
import { Seo } from 'components/seo';
import { WikiCard, WikiCardPlaceholder } from 'components/wiki/card';
import { getCollectedDocuments, getCollectedWikis, useCollectedDocuments, useCollectedWikis } from 'data/collector';
import { getStarDocuments, getStarWikis, useStarDocuments, useStarWikis } from 'data/star';
import { SingleColumnLayout } from 'layouts/single-column';
import type { NextPage } from 'next';
import React from 'react';
@ -25,7 +25,7 @@ const grid = {
};
const StarDocs = () => {
const { data: docs, loading, error } = useCollectedDocuments();
const { data: docs, loading, error } = useStarDocuments();
return (
<DataRender
@ -59,7 +59,7 @@ const StarDocs = () => {
};
const StarWikis = () => {
const { data, loading, error } = useCollectedWikis();
const { data, loading, error } = useStarWikis();
return (
<DataRender
@ -117,8 +117,8 @@ const Page: NextPage = () => {
Page.getInitialProps = async (ctx) => {
const props = await serverPrefetcher(ctx, [
{ url: CollectorApiDefinition.wikis.client(), action: (cookie) => getCollectedWikis(cookie) },
{ url: CollectorApiDefinition.documents.client(), action: (cookie) => getCollectedDocuments(cookie) },
{ url: StarApiDefinition.wikis.client(), action: (cookie) => getStarWikis(cookie) },
{ url: StarApiDefinition.documents.client(), action: (cookie) => getStarDocuments(cookie) },
]);
return props;
};

View File

@ -1,33 +0,0 @@
.cardWrap {
display: flex;
width: 100%;
max-height: 260px;
padding: 12px 16px 16px;
margin: 8px 0;
cursor: pointer;
border: 1px solid var(--semi-color-border);
border-radius: 5px;
flex-direction: column;
> header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
color: var(--semi-color-primary);
.rightWrap {
opacity: 0;
}
}
&:hover {
> header .rightWrap {
opacity: 1;
}
}
> footer {
margin-top: 12px;
}
}

View File

@ -1,122 +0,0 @@
import { List, TabPane, Tabs } from '@douyinfe/semi-ui';
import { IWiki, WikiApiDefinition } from '@think/domains';
import { DataRender } from 'components/data-render';
import { DocumentCard, DocumentCardPlaceholder } from 'components/document/card';
import { DocumentCreator } from 'components/document-creator';
import { Empty } from 'components/empty';
import { Seo } from 'components/seo';
import { WikiDocumentsShare } from 'components/wiki/documents-share';
import { WikiTocs } from 'components/wiki/tocs';
import { WikiTocsManager } from 'components/wiki/tocs/manager';
import { getWikiTocs, useWikiDocuments } from 'data/wiki';
import { CreateDocumentIllustration } from 'illustrations/create-document';
import { DoubleColumnLayout } from 'layouts/double-column';
import { NextPage } from 'next';
import Router, { useRouter } from 'next/router';
import React, { useCallback } from 'react';
import { serverPrefetcher } from 'services/server-prefetcher';
interface IProps {
wikiId: string;
}
const grid = {
gutter: 16,
xs: 24,
sm: 12,
md: 12,
lg: 8,
xl: 8,
xxl: 6,
};
const AllDocs = ({ wikiId }) => {
const { data: docs, loading, error } = useWikiDocuments(wikiId);
return (
<DataRender
loading={loading}
loadingContent={() => (
<List
grid={grid}
dataSource={Array.from({ length: 9 })}
renderItem={() => (
<List.Item style={{}}>
<DocumentCardPlaceholder />
</List.Item>
)}
/>
)}
error={error}
normalContent={() => (
<List
grid={grid}
dataSource={docs}
renderItem={(doc) => (
<List.Item style={{}}>
<DocumentCard document={doc} />
</List.Item>
)}
emptyContent={<Empty illustration={<CreateDocumentIllustration />} message={<DocumentCreator />} />}
/>
)}
/>
);
};
const TitleMap = {
tocs: '目录管理',
share: '隐私管理',
documents: '全部文档',
};
const Page: NextPage<IProps> = ({ wikiId }) => {
const { query = {} } = useRouter();
const { tab = 'tocs' } = query as {
tab?: string;
};
const navigate = useCallback(
(tab) => {
Router.push({
pathname: `/wiki/${wikiId}/documents`,
query: { tab },
});
},
[wikiId]
);
return (
<DoubleColumnLayout
leftNode={<WikiTocs wikiId={wikiId} />}
rightNode={
<div style={{ padding: '16px 24px' }}>
<Seo title={TitleMap[tab]} />
<Tabs type="line" activeKey={tab} onChange={(tab) => navigate(tab)}>
<TabPane tab={TitleMap['tocs']} itemKey="tocs">
<WikiTocsManager wikiId={wikiId} />
</TabPane>
<TabPane tab={TitleMap['share']} itemKey="share">
<WikiDocumentsShare wikiId={wikiId} />
</TabPane>
<TabPane tab={TitleMap['documents']} itemKey="documents">
<AllDocs wikiId={wikiId} />
</TabPane>
</Tabs>
</div>
}
></DoubleColumnLayout>
);
};
Page.getInitialProps = async (ctx) => {
const { wikiId } = ctx.query;
const res = await serverPrefetcher(ctx, [
{
url: WikiApiDefinition.getTocsById.client(wikiId as IWiki['id']),
action: (cookie) => getWikiTocs(wikiId, cookie),
},
]);
return { ...res, wikiId } as IProps;
};
export default Page;

View File

@ -56,12 +56,11 @@ export class Awareness extends Observable {
* @type {Map<number, MetaClientState>}
*/
this.meta = new Map();
this._checkInterval = /** @type {any} */ (
setInterval(() => {
this._checkInterval = /** @type {any} */ setInterval(() => {
const now = time.getUnixTime();
if (
this.getLocalState() !== null &&
outdatedTimeout / 2 <= now - /** @type {{lastUpdated:number}} */ (this.meta.get(this.clientID)).lastUpdated
outdatedTimeout / 2 <= now - /** @type {{lastUpdated:number}} */ this.meta.get(this.clientID).lastUpdated
) {
// renew local clock
this.setLocalState(this.getLocalState());
@ -78,8 +77,7 @@ export class Awareness extends Observable {
if (remove.length > 0) {
removeAwarenessStates(this, remove, 'timeout');
}
}, math.floor(outdatedTimeout / 10))
);
}, math.floor(outdatedTimeout / 10));
doc.on('destroy', () => {
this.destroy();
});
@ -176,7 +174,7 @@ export const removeAwarenessStates = (awareness, clients, origin) => {
if (awareness.states.has(clientID)) {
awareness.states.delete(clientID);
if (clientID === awareness.clientID) {
const curMeta = /** @type {MetaClientState} */ (awareness.meta.get(clientID));
const curMeta = /** @type {MetaClientState} */ awareness.meta.get(clientID);
awareness.meta.set(clientID, {
clock: curMeta.clock + 1,
lastUpdated: time.getUnixTime(),
@ -203,7 +201,7 @@ export const encodeAwarenessUpdate = (awareness, clients, states = awareness.sta
for (let i = 0; i < len; i++) {
const clientID = clients[i];
const state = states.get(clientID) || null;
const clock = /** @type {MetaClientState} */ (awareness.meta.get(clientID)).clock;
const clock = /** @type {MetaClientState} */ awareness.meta.get(clientID).clock;
encoding.writeVarUint(encoder, clientID);
encoding.writeVarUint(encoder, clock);
encoding.writeVarString(encoder, JSON.stringify(state));

View File

@ -1,7 +1,7 @@
.wrap {
position: relative;
overflow: auto;
margin-top: 24px;
overflow: auto;
.coverWrap {
position: relative;

View File

@ -1,6 +1,7 @@
import { Spin, Typography } from '@douyinfe/semi-ui';
import { HocuspocusProvider } from '@hocuspocus/provider';
import { DataRender } from 'components/data-render';
import deepEqual from 'deep-equal';
import { throttle } from 'helpers/throttle';
import { useToggle } from 'hooks/use-toggle';
import { SecureDocumentIllustration } from 'illustrations/secure-document';
@ -34,6 +35,7 @@ export const CollaborationEditor = forwardRef((props: ICollaborationEditorProps,
const [loading, toggleLoading] = useToggle(true);
const [error, setError] = useState(null);
const [status, setStatus] = useState<ProviderStatus>('connecting');
const lastAwarenessRef = useRef([]);
const hocuspocusProvider = useMemo(() => {
return new HocuspocusProvider({
@ -48,7 +50,11 @@ export const CollaborationEditor = forwardRef((props: ICollaborationEditorProps,
maxAttempts: 1,
onAwarenessUpdate: throttle(({ states }) => {
const users = states.map((state) => ({ clientId: state.clientId, user: state.user }));
if (deepEqual(user, lastAwarenessRef.current)) {
return;
}
onAwarenessUpdate && onAwarenessUpdate(users);
lastAwarenessRef.current = users;
}, 200),
onAuthenticationFailed() {
toggleLoading(false);

View File

@ -8,7 +8,7 @@ export declare const FileApiDefinition: {
client: () => string;
};
/**
*
*
*/
initChunk: {
method: "post";

View File

@ -11,7 +11,7 @@ exports.FileApiDefinition = {
client: function () { return '/file/upload'; }
},
/**
*
*
*/
initChunk: {
method: 'post',

View File

@ -5,4 +5,4 @@ export * from './file';
export * from './message';
export * from './template';
export * from './comment';
export * from './collector';
export * from './star';

View File

@ -17,4 +17,4 @@ __exportStar(require("./file"), exports);
__exportStar(require("./message"), exports);
__exportStar(require("./template"), exports);
__exportStar(require("./comment"), exports);
__exportStar(require("./collector"), exports);
__exportStar(require("./star"), exports);

View File

@ -0,0 +1,42 @@
export declare const StarApiDefinition: {
/**
*
*/
toggle: {
method: "post";
server: "toggle";
client: () => string;
};
/**
*
*/
check: {
method: "post";
server: "check";
client: () => string;
};
/**
*
*/
wikis: {
method: "get";
server: "wikis";
client: () => string;
};
/**
*
*/
wikiDocuments: {
method: "get";
server: "wiki/documents";
client: () => string;
};
/**
*
*/
documents: {
method: "get";
server: "documents";
client: () => string;
};
};

View File

@ -0,0 +1,45 @@
"use strict";
exports.__esModule = true;
exports.StarApiDefinition = void 0;
exports.StarApiDefinition = {
/**
*
*/
toggle: {
method: 'post',
server: 'toggle',
client: function () { return '/star/toggle'; }
},
/**
*
*/
check: {
method: 'post',
server: 'check',
client: function () { return '/star/check'; }
},
/**
*
*/
wikis: {
method: 'get',
server: 'wikis',
client: function () { return '/star/wikis'; }
},
/**
*
*/
wikiDocuments: {
method: 'get',
server: 'wiki/documents',
client: function () { return '/star/wiki/documents'; }
},
/**
*
*/
documents: {
method: 'get',
server: 'documents',
client: function () { return '/star/documents'; }
}
};

View File

@ -4,5 +4,4 @@ export * from './document';
export * from './message';
export * from './template';
export * from './comment';
export * from './collector';
export * from './pagination';

View File

@ -16,5 +16,4 @@ __exportStar(require("./document"), exports);
__exportStar(require("./message"), exports);
__exportStar(require("./template"), exports);
__exportStar(require("./comment"), exports);
__exportStar(require("./collector"), exports);
__exportStar(require("./pagination"), exports);

View File

@ -5,4 +5,4 @@ export * from './file';
export * from './message';
export * from './template';
export * from './comment';
export * from './collector';
export * from './star';

View File

@ -1,13 +1,11 @@
import { IDocument, IWiki, CollectType } from '../models';
export const CollectorApiDefinition = {
export const StarApiDefinition = {
/**
*
*/
toggle: {
method: 'post' as const,
server: 'toggle' as const,
client: () => '/collector/toggle',
client: () => '/star/toggle',
},
/**
@ -16,7 +14,7 @@ export const CollectorApiDefinition = {
check: {
method: 'post' as const,
server: 'check' as const,
client: () => '/collector/check',
client: () => '/star/check',
},
/**
@ -25,7 +23,16 @@ export const CollectorApiDefinition = {
wikis: {
method: 'get' as const,
server: 'wikis' as const,
client: () => '/collector/wikis',
client: () => '/star/wikis',
},
/**
*
*/
wikiDocuments: {
method: 'get' as const,
server: 'wiki/documents' as const,
client: () => '/star/wiki/documents',
},
/**
@ -34,6 +41,6 @@ export const CollectorApiDefinition = {
documents: {
method: 'get' as const,
server: 'documents' as const,
client: () => '/collector/documents',
client: () => '/star/documents',
},
};

View File

@ -1,4 +0,0 @@
export enum CollectType {
document = 'document',
wiki = 'wiki',
}

View File

@ -4,5 +4,4 @@ export * from './document';
export * from './message';
export * from './template';
export * from './comment';
export * from './collector';
export * from './pagination';

View File

@ -1,8 +1,8 @@
import { CollectorEntity } from '@entities/collector.entity';
import { CommentEntity } from '@entities/comment.entity';
import { DocumentEntity } from '@entities/document.entity';
import { DocumentAuthorityEntity } from '@entities/document-authority.entity';
import { MessageEntity } from '@entities/message.entity';
import { StarEntity } from '@entities/star.entity';
import { TemplateEntity } from '@entities/template.entity';
import { UserEntity } from '@entities/user.entity';
import { ViewEntity } from '@entities/view.entity';
@ -10,11 +10,11 @@ import { WikiEntity } from '@entities/wiki.entity';
import { WikiUserEntity } from '@entities/wiki-user.entity';
import { IS_PRODUCTION } from '@helpers/env.helper';
import { getLogFileName, ONE_DAY } from '@helpers/log.helper';
import { CollectorModule } from '@modules/collector.module';
import { CommentModule } from '@modules/comment.module';
import { DocumentModule } from '@modules/document.module';
import { FileModule } from '@modules/file.module';
import { MessageModule } from '@modules/message.module';
import { StarModule } from '@modules/star.module';
import { TemplateModule } from '@modules/template.module';
import { UserModule } from '@modules/user.module';
import { ViewModule } from '@modules/view.module';
@ -35,7 +35,7 @@ const ENTITIES = [
WikiUserEntity,
DocumentAuthorityEntity,
DocumentEntity,
CollectorEntity,
StarEntity,
CommentEntity,
MessageEntity,
TemplateEntity,
@ -46,7 +46,7 @@ const MODULES = [
UserModule,
WikiModule,
DocumentModule,
CollectorModule,
StarModule,
FileModule,
CommentModule,
MessageModule,

View File

@ -1,65 +0,0 @@
import { CollectDto } from '@dtos/collect.dto';
import { JwtGuard } from '@guard/jwt.guard';
import {
Body,
ClassSerializerInterceptor,
Controller,
Get,
HttpCode,
HttpStatus,
Post,
Request,
UseGuards,
UseInterceptors,
} from '@nestjs/common';
import { CollectorService } from '@services/collector.service';
import { CollectorApiDefinition } from '@think/domains';
@Controller('collector')
export class CollectorController {
constructor(private readonly collectorService: CollectorService) {}
/**
*
*/
@UseInterceptors(ClassSerializerInterceptor)
@Post(CollectorApiDefinition.toggle.server)
@HttpCode(HttpStatus.OK)
@UseGuards(JwtGuard)
async toggleStar(@Request() req, @Body() dto: CollectDto) {
return await this.collectorService.toggleStar(req.user, dto);
}
/**
*
*/
@UseInterceptors(ClassSerializerInterceptor)
@Post(CollectorApiDefinition.check.server)
@HttpCode(HttpStatus.OK)
@UseGuards(JwtGuard)
async checkStar(@Request() req, @Body() dto: CollectDto) {
return await this.collectorService.isStared(req.user, dto);
}
/**
*
*/
@UseInterceptors(ClassSerializerInterceptor)
@Get(CollectorApiDefinition.wikis.server)
@HttpCode(HttpStatus.OK)
@UseGuards(JwtGuard)
async getWikis(@Request() req) {
return await this.collectorService.getWikis(req.user);
}
/**
*
*/
@UseInterceptors(ClassSerializerInterceptor)
@Get(CollectorApiDefinition.documents.server)
@HttpCode(HttpStatus.OK)
@UseGuards(JwtGuard)
async getDocuments(@Request() req) {
return await this.collectorService.getDocuments(req.user);
}
}

View File

@ -0,0 +1,77 @@
import { StarDto } from '@dtos/star.dto';
import { JwtGuard } from '@guard/jwt.guard';
import {
Body,
ClassSerializerInterceptor,
Controller,
Get,
HttpCode,
HttpStatus,
Post,
Query,
Request,
UseGuards,
UseInterceptors,
} from '@nestjs/common';
import { StarService } from '@services/star.service';
import { StarApiDefinition } from '@think/domains';
@Controller('star')
export class StarController {
constructor(private readonly starService: StarService) {}
/**
*
*/
@UseInterceptors(ClassSerializerInterceptor)
@Post(StarApiDefinition.toggle.server)
@HttpCode(HttpStatus.OK)
@UseGuards(JwtGuard)
async toggleStar(@Request() req, @Body() dto: StarDto) {
return await this.starService.toggleStar(req.user, dto);
}
/**
*
*/
@UseInterceptors(ClassSerializerInterceptor)
@Post(StarApiDefinition.check.server)
@HttpCode(HttpStatus.OK)
@UseGuards(JwtGuard)
async checkStar(@Request() req, @Body() dto: StarDto) {
return await this.starService.isStared(req.user, dto);
}
/**
*
*/
@UseInterceptors(ClassSerializerInterceptor)
@Get(StarApiDefinition.wikis.server)
@HttpCode(HttpStatus.OK)
@UseGuards(JwtGuard)
async getWikis(@Request() req) {
return await this.starService.getWikis(req.user);
}
/**
*
*/
@UseInterceptors(ClassSerializerInterceptor)
@Get(StarApiDefinition.wikiDocuments.server)
@HttpCode(HttpStatus.OK)
@UseGuards(JwtGuard)
async getWikiDocuments(@Request() req, @Query() dto: StarDto) {
return await this.starService.getWikiDocuments(req.user, dto);
}
/**
*
*/
@UseInterceptors(ClassSerializerInterceptor)
@Get(StarApiDefinition.documents.server)
@HttpCode(HttpStatus.OK)
@UseGuards(JwtGuard)
async getDocuments(@Request() req) {
return await this.starService.getDocuments(req.user);
}
}

View File

@ -1,12 +0,0 @@
import { CollectType } from '@think/domains';
import { IsNotEmpty, IsString } from 'class-validator';
export class CollectDto {
@IsString({ message: '收藏目标Id类型错误正确类型为String' })
@IsNotEmpty({ message: '收藏目标Id不能为空' })
targetId: string;
@IsString({ message: '收藏目标类型类型错误正确类型为String' })
@IsNotEmpty({ message: '用户密码不能为空' })
type: CollectType;
}

View File

@ -0,0 +1,12 @@
import { IsNotEmpty, IsOptional, IsString } from 'class-validator';
export class StarDto {
@IsString({ message: '加星 wikiId 类型错误正确类型为String' })
@IsNotEmpty({ message: '加星 wikiId 不能为空' })
wikiId: string;
@IsString({ message: '加星 documentId 类型错误正确类型为String' })
@IsNotEmpty({ message: '加星 documentId 不能为空' })
@IsOptional()
documentId?: string;
}

View File

@ -1,24 +1,18 @@
import { CollectType } from '@think/domains';
import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm';
@Entity('collector')
export class CollectorEntity {
@Entity('star')
export class StarEntity {
@PrimaryGeneratedColumn('uuid')
public id: string;
@Column({ type: 'varchar', comment: '用户 Id' })
public userId: string;
@Column({ type: 'varchar', comment: '收藏目标 Id' })
public targetId: string;
@Column({ type: 'varchar', comment: '知识库 Id' })
public wikiId: string;
@Column({
type: 'enum',
enum: CollectType,
default: CollectType.document,
comment: '收藏目标类型',
})
public type: CollectType;
@Column({ type: 'varchar', comment: '文档 Id', default: null })
public documentId: string;
@CreateDateColumn({
type: 'timestamp',

View File

@ -1,5 +1,4 @@
import { HttpResponseExceptionFilter } from '@exceptions/http-response.exception';
import { IS_PRODUCTION } from '@helpers/env.helper';
import { FILE_DEST, FILE_ROOT_PATH } from '@helpers/file.helper/local.client';
import { ConfigService } from '@nestjs/config';
import { NestFactory } from '@nestjs/core';
@ -13,7 +12,6 @@ import rateLimit from 'express-rate-limit';
import helmet from 'helmet';
import { AppModule } from './app.module';
import { AppClusterService } from './app-cluster.service';
async function bootstrap() {
const app = await NestFactory.create(AppModule);

View File

@ -1,8 +1,8 @@
import { DocumentController } from '@controllers/document.controller';
import { DocumentEntity } from '@entities/document.entity';
import { DocumentAuthorityEntity } from '@entities/document-authority.entity';
import { CollectorModule } from '@modules/collector.module';
import { MessageModule } from '@modules/message.module';
import { StarModule } from '@modules/star.module';
import { TemplateModule } from '@modules/template.module';
import { UserModule } from '@modules/user.module';
import { ViewModule } from '@modules/view.module';
@ -20,7 +20,7 @@ import { DocumentService } from '@services/document.service';
forwardRef(() => WikiModule),
forwardRef(() => MessageModule),
forwardRef(() => TemplateModule),
forwardRef(() => CollectorModule),
forwardRef(() => StarModule),
forwardRef(() => ViewModule),
],
providers: [DocumentService],

View File

@ -1,21 +1,21 @@
import { CollectorController } from '@controllers/collector.controller';
import { CollectorEntity } from '@entities/collector.entity';
import { StarController } from '@controllers/star.controller';
import { StarEntity } from '@entities/star.entity';
import { DocumentModule } from '@modules/document.module';
import { UserModule } from '@modules/user.module';
import { WikiModule } from '@modules/wiki.module';
import { forwardRef, Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { CollectorService } from '@services/collector.service';
import { StarService } from '@services/star.service';
@Module({
imports: [
TypeOrmModule.forFeature([CollectorEntity]),
TypeOrmModule.forFeature([StarEntity]),
forwardRef(() => UserModule),
forwardRef(() => WikiModule),
forwardRef(() => DocumentModule),
],
providers: [CollectorService],
exports: [CollectorService],
controllers: [CollectorController],
providers: [StarService],
exports: [StarService],
controllers: [StarController],
})
export class CollectorModule {}
export class StarModule {}

View File

@ -1,7 +1,7 @@
import { UserController } from '@controllers/user.controller';
import { UserEntity } from '@entities/user.entity';
import { CollectorModule } from '@modules/collector.module';
import { MessageModule } from '@modules/message.module';
import { StarModule } from '@modules/star.module';
import { WikiModule } from '@modules/wiki.module';
import { forwardRef, Inject, Injectable, Module, UnauthorizedException } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
@ -60,7 +60,7 @@ const jwtModule = JwtModule.register({
ConfigModule,
forwardRef(() => WikiModule),
forwardRef(() => MessageModule),
forwardRef(() => CollectorModule),
forwardRef(() => StarModule),
passModule,
jwtModule,
],

View File

@ -1,9 +1,9 @@
import { WikiController } from '@controllers/wiki.controller';
import { WikiEntity } from '@entities/wiki.entity';
import { WikiUserEntity } from '@entities/wiki-user.entity';
import { CollectorModule } from '@modules/collector.module';
import { DocumentModule } from '@modules/document.module';
import { MessageModule } from '@modules/message.module';
import { StarModule } from '@modules/star.module';
import { UserModule } from '@modules/user.module';
import { ViewModule } from '@modules/view.module';
import { forwardRef, Module } from '@nestjs/common';
@ -17,7 +17,7 @@ import { WikiService } from '@services/wiki.service';
forwardRef(() => DocumentModule),
forwardRef(() => MessageModule),
forwardRef(() => ViewModule),
forwardRef(() => CollectorModule),
forwardRef(() => StarModule),
],
providers: [WikiService],
exports: [WikiService],

View File

@ -1,80 +0,0 @@
import { CollectDto } from '@dtos/collect.dto';
import { CollectorEntity } from '@entities/collector.entity';
import { forwardRef, Inject, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { DocumentService } from '@services/document.service';
import { OutUser, UserService } from '@services/user.service';
import { WikiService } from '@services/wiki.service';
import { CollectType } from '@think/domains';
import * as lodash from 'lodash';
import { Repository } from 'typeorm';
@Injectable()
export class CollectorService {
constructor(
@InjectRepository(CollectorEntity)
private readonly collectorRepo: Repository<CollectorEntity>,
@Inject(forwardRef(() => UserService))
private readonly userService: UserService,
@Inject(forwardRef(() => WikiService))
private readonly wikiService: WikiService,
@Inject(forwardRef(() => DocumentService))
private readonly documentService: DocumentService
) {}
async toggleStar(user: OutUser, dto: CollectDto) {
const data = {
...dto,
userId: user.id,
};
const record = await this.collectorRepo.findOne(data);
if (record) {
await this.collectorRepo.remove(record);
return;
} else {
const res = await this.collectorRepo.create(data);
const ret = await this.collectorRepo.save(res);
return ret;
}
}
async isStared(user: OutUser, dto: CollectDto) {
const res = await this.collectorRepo.findOne({ userId: user.id, ...dto });
return Boolean(res);
}
async getWikis(user: OutUser) {
const records = await this.collectorRepo.find({
userId: user.id,
type: CollectType.wiki,
});
const res = await this.wikiService.findByIds(records.map((record) => record.targetId));
const withCreateUserRes = await Promise.all(
res.map(async (wiki) => {
const createUser = await this.userService.findById(wiki.createUserId);
const isMember = await this.wikiService.isMember(wiki.id, user.id);
return { createUser, isMember, ...wiki };
})
);
return withCreateUserRes;
}
async getDocuments(user: OutUser) {
const records = await this.collectorRepo.find({
userId: user.id,
type: CollectType.document,
});
const res = await this.documentService.findByIds(records.map((record) => record.targetId));
const withCreateUserRes = await Promise.all(
res.map(async (doc) => {
const createUser = await this.userService.findById(doc.createUserId);
return { createUser, ...doc };
})
);
return withCreateUserRes.map((document) => {
return lodash.omit(document, ['state', 'content', 'index', 'createUserId']);
});
}
}

View File

@ -0,0 +1,135 @@
import { StarDto } from '@dtos/star.dto';
import { StarEntity } from '@entities/star.entity';
import { forwardRef, Inject, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { DocumentService } from '@services/document.service';
import { OutUser, UserService } from '@services/user.service';
import { WikiService } from '@services/wiki.service';
import { IDocument } from '@think/domains';
import * as lodash from 'lodash';
import { Repository } from 'typeorm';
@Injectable()
export class StarService {
constructor(
@InjectRepository(StarEntity)
private readonly starRepo: Repository<StarEntity>,
@Inject(forwardRef(() => UserService))
private readonly userService: UserService,
@Inject(forwardRef(() => WikiService))
private readonly wikiService: WikiService,
@Inject(forwardRef(() => DocumentService))
private readonly documentService: DocumentService
) {}
/**
*
* @param user
* @param dto
* @returns
*/
async toggleStar(user: OutUser, dto: StarDto) {
const data = {
...dto,
userId: user.id,
};
const record = await this.starRepo.findOne(data);
if (record) {
await this.starRepo.remove(record);
return;
} else {
const res = await this.starRepo.create(data);
const ret = await this.starRepo.save(res);
return ret;
}
}
/**
*
* @param user
* @param dto
* @returns
*/
async isStared(user: OutUser, dto: StarDto) {
const res = await this.starRepo.findOne({ userId: user.id, ...dto });
return Boolean(res);
}
/**
*
* @param user
* @returns
*/
async getWikis(user: OutUser) {
const records = await this.starRepo.find({
userId: user.id,
documentId: null,
});
const res = await this.wikiService.findByIds(records.map((record) => record.wikiId));
const withCreateUserRes = await Promise.all(
res.map(async (wiki) => {
const createUser = await this.userService.findById(wiki.createUserId);
const isMember = await this.wikiService.isMember(wiki.id, user.id);
return { createUser, isMember, ...wiki };
})
);
return withCreateUserRes;
}
/**
*
* @param user
* @returns
*/
async getWikiDocuments(user: OutUser, dto: StarDto) {
const records = await this.starRepo.find({
userId: user.id,
wikiId: dto.wikiId,
});
const res = await this.documentService.findByIds(
records.filter((record) => record.documentId).map((record) => record.documentId)
);
const withCreateUserRes = (await Promise.all(
res.map(async (doc) => {
const createUser = await this.userService.findById(doc.createUserId);
return { createUser, ...doc };
})
)) as Array<IDocument & { createUser: OutUser }>;
return withCreateUserRes
.map((document) => {
return lodash.omit(document, ['state', 'content', 'index', 'createUserId']);
})
.map((doc) => {
return {
...doc,
key: doc.id,
label: doc.title,
};
});
}
/**
*
* @param user
* @returns
*/
async getDocuments(user: OutUser) {
const records = await this.starRepo.find({
userId: user.id,
});
const res = await this.documentService.findByIds(records.map((record) => record.documentId));
const withCreateUserRes = await Promise.all(
res.map(async (doc) => {
const createUser = await this.userService.findById(doc.createUserId);
return { createUser, ...doc };
})
);
return withCreateUserRes.map((document) => {
return lodash.omit(document, ['state', 'content', 'index', 'createUserId']);
});
}
}

View File

@ -6,10 +6,10 @@ import { forwardRef, HttpException, HttpStatus, Inject, Injectable } from '@nest
import { ConfigService } from '@nestjs/config';
import { JwtService } from '@nestjs/jwt';
import { InjectRepository } from '@nestjs/typeorm';
import { CollectorService } from '@services/collector.service';
import { MessageService } from '@services/message.service';
import { StarService } from '@services/star.service';
import { WikiService } from '@services/wiki.service';
import { CollectType, UserStatus } from '@think/domains';
import { UserStatus } from '@think/domains';
import { instanceToPlain } from 'class-transformer';
import { Repository } from 'typeorm';
@ -29,8 +29,8 @@ export class UserService {
@Inject(forwardRef(() => MessageService))
private readonly messageService: MessageService,
@Inject(forwardRef(() => CollectorService))
private readonly collectorService: CollectorService,
@Inject(forwardRef(() => StarService))
private readonly starService: StarService,
@Inject(forwardRef(() => WikiService))
private readonly wikiService: WikiService
@ -94,9 +94,8 @@ export class UserService {
name: createdUser.name,
description: `${createdUser.name}的个人空间`,
});
await this.collectorService.toggleStar(createdUser, {
targetId: wiki.id,
type: CollectType.wiki,
await this.starService.toggleStar(createdUser, {
wikiId: wiki.id,
});
await this.messageService.notify(createdUser, {
title: `欢迎「${createdUser.name}`,

View File

@ -7,13 +7,13 @@ import { WikiUserEntity } from '@entities/wiki-user.entity';
import { array2tree } from '@helpers/tree.helper';
import { forwardRef, HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { CollectorService } from '@services/collector.service';
import { DocumentService } from '@services/document.service';
import { MessageService } from '@services/message.service';
import { StarService } from '@services/star.service';
import { UserService } from '@services/user.service';
import { OutUser } from '@services/user.service';
import { ViewService } from '@services/view.service';
import { CollectType, DocumentStatus, IPagination, WikiStatus, WikiUserRole } from '@think/domains';
import { DocumentStatus, IPagination, WikiStatus, WikiUserRole } from '@think/domains';
import { instanceToPlain } from 'class-transformer';
import * as lodash from 'lodash';
import { Repository } from 'typeorm';
@ -30,8 +30,8 @@ export class WikiService {
@Inject(forwardRef(() => MessageService))
private readonly messageService: MessageService,
@Inject(forwardRef(() => CollectorService))
private readonly collectorService: CollectorService,
@Inject(forwardRef(() => StarService))
private readonly starService: StarService,
@Inject(forwardRef(() => DocumentService))
private readonly documentService: DocumentService,
@ -320,7 +320,7 @@ export class WikiService {
},
true
),
await this.collectorService.toggleStar(user, { type: CollectType.wiki, targetId: wiki.id }),
await this.starService.toggleStar(user, { wikiId: wiki.id }),
]);
const homeDocumentId = doc.id;
const withHomeDocumentIdWiki = await this.wikiRepo.merge(wiki, { homeDocumentId });