diff --git a/packages/client/src/components/document/actions/index.module.scss b/packages/client/src/components/document/actions/index.module.scss new file mode 100644 index 00000000..6030de93 --- /dev/null +++ b/packages/client/src/components/document/actions/index.module.scss @@ -0,0 +1,8 @@ +.hoverVisible { + opacity: 0; + + &:hover, + &.isActive { + opacity: 1; + } +} diff --git a/packages/client/src/components/document/actions/index.tsx b/packages/client/src/components/document/actions/index.tsx index 27c2fef7..3540e47d 100644 --- a/packages/client/src/components/document/actions/index.tsx +++ b/packages/client/src/components/document/actions/index.tsx @@ -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 = ({ 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 = ({ return ( <> - = ({ )} + { + return ( + + + + + {isPublic ? '分享中' : '分享'} + + + + ); + }} + /> + ( = ({ wikiId={wikiId} documentId={documentId} render={({ copy, children }) => { - return {children}; + return ( + + {children} + + ); }} /> + {!hideDocumentVersion && ( + { + return ( + { + togglePopoverVisible(false); + onClick(); + }} + > + + + + 历史记录 + + + + ); + }} + /> + )} + + {!hideDocumentVersion && ( + { + return ( + + + + + 文档排版 + + + + ); + }} + /> + )} + = ({ } > - {children || = ({ documentId, disabled = false ); }; + +export const DocumentVersionTrigger: React.FC> = ({ render, disabled }) => { + const { toggleVisible } = DocumentVersionControl.useHook(); + + return ( + <> + {render ? ( + render({ onClick: toggleVisible, disabled }) + ) : ( + <> + + + )} + + ); +}; diff --git a/packages/client/src/components/search/index.tsx b/packages/client/src/components/search/index.tsx index bd852d73..f4f6297d 100644 --- a/packages/client/src/components/search/index.tsx +++ b/packages/client/src/components/search/index.tsx @@ -54,7 +54,7 @@ const List: React.FC<{ data: IDocument[] }> = ({ data }) => {
- +
diff --git a/packages/client/src/components/wiki/card/index.tsx b/packages/client/src/components/wiki/card/index.tsx index 0d4b453d..b855579e 100644 --- a/packages/client/src/components/wiki/card/index.tsx +++ b/packages/client/src/components/wiki/card/index.tsx @@ -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'; diff --git a/packages/client/src/components/wiki/setting/index.tsx b/packages/client/src/components/wiki/setting/index.tsx index a1f0e24a..1ad8098f 100644 --- a/packages/client/src/components/wiki/setting/index.tsx +++ b/packages/client/src/components/wiki/setting/index.tsx @@ -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 = ({ wikiId, tab, onNavigate }) => { + + + + + + diff --git a/packages/client/src/components/wiki/setting/privacy/index.module.scss b/packages/client/src/components/wiki/setting/privacy/index.module.scss index 8249c635..7e0411e9 100644 --- a/packages/client/src/components/wiki/setting/privacy/index.module.scss +++ b/packages/client/src/components/wiki/setting/privacy/index.module.scss @@ -1,13 +1,64 @@ /* stylelint-disable */ -.wrap { - .statusWrap { - padding: 10px 12px; - margin-top: 16px; - border: 1px solid var(--semi-color-border); - border-radius: 4px; +.statusWrap { + padding: 10px 12px; + margin-top: 16px; + border: 1px solid var(--semi-color-border); + border-radius: 4px; - .title { - margin-bottom: 16px; + .title { + 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; +} diff --git a/packages/client/src/components/wiki/setting/privacy/index.tsx b/packages/client/src/components/wiki/setting/privacy/index.tsx index d9822bfe..c7dc3911 100644 --- a/packages/client/src/components/wiki/setting/privacy/index.tsx +++ b/packages/client/src/components/wiki/setting/privacy/index.tsx @@ -16,11 +16,65 @@ interface IProps { export const Privacy: React.FC = ({ 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 ( +
+ { + item.onChange(); + }} + key={item.label} + checked={item.checked} + > + {item.title} + +
+ ); + }, []); + + const renderSelectedItem = useCallback((item) => { + return ( +
+ {item.title} + +
+ ); + }, []); + + 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 = ({ wikiId }) => { } /> )} +
是否公开知识库? @@ -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> diff --git a/packages/client/src/components/wiki/setting/users/index.tsx b/packages/client/src/components/wiki/setting/users/index.tsx index 1dedd48e..98f43f6e 100644 --- a/packages/client/src/components/wiki/setting/users/index.tsx +++ b/packages/client/src/components/wiki/setting/users/index.tsx @@ -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" /> diff --git a/packages/client/src/components/wiki/star/index.tsx b/packages/client/src/components/wiki/star/index.tsx index 76b6ae23..ddc5931d 100644 --- a/packages/client/src/components/wiki/star/index.tsx +++ b/packages/client/src/components/wiki/star/index.tsx @@ -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 ( diff --git a/packages/client/src/components/wiki/tocs/index.module.scss b/packages/client/src/components/wiki/tocs/index.module.scss index f4d34ec7..98a8c3d8 100644 --- a/packages/client/src/components/wiki/tocs/index.module.scss +++ b/packages/client/src/components/wiki/tocs/index.module.scss @@ -1,15 +1,87 @@ +/* stylelint-disable */ .wrap { + height: 100%; + padding-top: 12px; + overflow: auto; + display: flex; flex-direction: column; - height: 100%; - .treeWrap { + overflow: hidden; + + > main { flex: 1; - padding: 0 8px 32px 16px; 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 { + padding: 8px 12px; + + .title { + display: flex; + justify-content: space-between; + align-items: center; + padding-left: 8px; + } + + :global { + .semi-tree-option-list { + overflow-y: hidden; + } + } +} + .treeInnerWrap { :global { .semi-tree-option-list-block .semi-tree-option-selected { @@ -30,16 +102,15 @@ align-items: center; width: calc(100% - 48px); } + } +} - .right { - opacity: 0; - } +.hoverVisible { + opacity: 0; - &:hover { - .right { - opacity: 1; - } - } + &:hover, + &.isActive { + opacity: 1; } } @@ -99,12 +170,4 @@ flex: 1; } } - - .rightWrap { - padding-right: 4px; - } -} - -.docListTitle { - margin: 12px 0.5rem; } diff --git a/packages/client/src/components/wiki/tocs/index.tsx b/packages/client/src/components/wiki/tocs/index.tsx index ad484acb..fc0e4182 100644 --- a/packages/client/src/components/wiki/tocs/index.tsx +++ b/packages/client/src/components/wiki/tocs/index.tsx @@ -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,168 +48,256 @@ export const WikiTocs: React.FC<IProps> = ({ return ( <div className={styles.wrap}> - <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 }} />} - /> - } - error={wikiError} - normalContent={() => ( - <NavItem - icon={ - <Avatar - shape="square" - size="small" - src={wiki.avatar} - style={{ - marginRight: 8, - width: 24, - height: 24, - borderRadius: 4, - }} - > - {wiki.name.charAt(0)} - </Avatar> - } - text={<Text strong>{wiki.name}</Text>} - hoverable={false} - /> - )} - /> - - <NavItem - icon={<IconOverview />} - text={'概述'} - href={{ - pathname: `/wiki/[wikiId]`, - query: { wikiId }, - }} - isActive={pathname === '/wiki/[wikiId]' || (query && wiki && query.documentId === wiki.homeDocumentId)} - /> - - <NavItem - icon={<IconSetting />} - text={'设置'} - href={{ - pathname: `/wiki/[wikiId]/setting`, - query: { tab: 'base', wikiId }, - }} - isActive={pathname === '/wiki/[wikiId]/setting'} - /> - - <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 }} />} - /> - } - 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 - /> - ) : null - } - /> - - <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 />} - /> - } - error={wikiError} - normalContent={() => ( - <NavItem - icon={<IconDocument />} - text={'文档管理'} - href={{ - pathname: `/wiki/[wikiId]/documents`, - query: { wikiId }, - }} - isActive={pathname === '/wiki/[wikiId]/documents'} - rightNode={ - <Button - style={{ fontSize: '1em' }} - theme="borderless" - type="tertiary" - icon={<IconPlus style={{ fontSize: '1em' }} />} - size="small" - onClick={() => { - triggerCreateDocument({ wikiId: wiki.id, documentId: null }); - }} - /> - } - /> - )} - /> - - <div className={styles.treeWrap}> + <header> <DataRender - loading={tocsLoading} - loadingContent={<NavItem icon={null} text={<Skeleton.Title style={{ width: '100%' }} />} />} - error={tocsError} + 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={() => ( - <Tree - data={tocs || []} - docAsLink={docAsLink} - getDocLink={getDocLink} - parentIds={parentIds} - activeId={documentId} - /> + <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" + src={wiki.avatar} + style={{ + marginRight: 8, + width: 24, + height: 24, + borderRadius: 4, + }} + > + {wiki.name.charAt(0)} + </Avatar> + <Text strong ellipsis={{ rows: 1 }}> + {wiki.name} + </Text> + </span> + </a> + </Link> + </Dropdown.Item> + ); + })} + </Dropdown.Menu> + } + > + <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> )} /> - </div> + + <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 }, + }} + > + <a> + <IconOverview style={{ fontSize: '1em' }} /> + <span>主页</span> + </a> + </Link> + </div> + )} + /> + + <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 }, + }} + > + <a> + <IconSetting style={{ fontSize: '1em' }} /> + <span>设置</span> + </a> + </Link> + </div> + )} + /> + </header> + + <main> + <div className={styles.treeWrap}> + <DataRender + loading={starDocumentsLoading} + loadingContent={<Skeleton.Title style={{ width: '100%' }} />} + error={starDocumentsError} + normalContent={() => ( + <div className={styles.title}> + <Text type="tertiary" size="small"> + 已加星标 + </Text> + </div> + )} + /> + <DataRender + loading={starDocumentsLoading} + loadingContent={<div>1</div>} + error={starDocumentsError} + normalContent={() => ( + <Tree + data={starDocuments || []} + docAsLink={docAsLink} + getDocLink={getDocLink} + parentIds={parentIds} + activeId={documentId} + /> + )} + /> + </div> + + <div className={styles.treeWrap}> + <DataRender + loading={tocsLoading} + loadingContent={<Skeleton.Title style={{ width: '100%' }} />} + error={wikiError} + normalContent={() => ( + <div className={styles.title}> + <Text type="tertiary" size="small"> + 文档集 + </Text> + <Button + style={{ fontSize: '1em' }} + theme="borderless" + type="tertiary" + icon={<IconPlus style={{ fontSize: '1em' }} />} + size="small" + onClick={() => { + triggerCreateDocument({ wikiId: wiki.id, documentId: null }); + }} + /> + </div> + )} + /> + <DataRender + loading={tocsLoading} + loadingContent={<div>1</div>} + error={tocsError} + normalContent={() => ( + <Tree + needAddDocument + data={tocs || []} + docAsLink={docAsLink} + getDocLink={getDocLink} + parentIds={parentIds} + activeId={documentId} + /> + )} + /> + </div> + </main> </div> ); }; diff --git a/packages/client/src/components/wiki/tocs/manager/index.module.scss b/packages/client/src/components/wiki/tocs/manager/index.module.scss index 5296e016..4b196fe0 100644 --- a/packages/client/src/components/wiki/tocs/manager/index.module.scss +++ b/packages/client/src/components/wiki/tocs/manager/index.module.scss @@ -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); diff --git a/packages/client/src/components/wiki/tocs/manager/index.tsx b/packages/client/src/components/wiki/tocs/manager/index.tsx index fb0b69cd..1ae3fbfe 100644 --- a/packages/client/src/components/wiki/tocs/manager/index.tsx +++ b/packages/client/src/components/wiki/tocs/manager/index.tsx @@ -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> diff --git a/packages/client/src/components/wiki/tocs/public.tsx b/packages/client/src/components/wiki/tocs/public.tsx index b77daa97..986cc40e 100644 --- a/packages/client/src/components/wiki/tocs/public.tsx +++ b/packages/client/src/components/wiki/tocs/public.tsx @@ -43,72 +43,17 @@ export const WikiPublicTocs: React.FC<IProps> = ({ return ( <div className={styles.wrap}> - <div className={styles.navItemWrap}> - <div className={styles.navItem}> - <Space> - <LogoImage /> <LogoText /> - </Space> + <header> + <div className={styles.navItemWrap}> + <div className={styles.navItem}> + <Space> + <LogoImage /> <LogoText /> + </Space> + </div> </div> - </div> - <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 }} />} - /> - } - error={wikiError} - normalContent={() => ( - <> - <Seo title={wiki.name + ' - ' + pageTitle} /> - <NavItem - icon={ - <Avatar - shape="square" - size="small" - src={wiki.avatar} - style={{ - marginRight: 8, - width: 24, - height: 24, - borderRadius: 4, - }} - > - {wiki.name.charAt(0)} - </Avatar> - } - text={<Text strong>{wiki.name}</Text>} - hoverable={false} - /> - </> - )} - /> - - <NavItem - icon={<IconOverview />} - text={'概述'} - href={{ - pathname: `/share/wiki/[wikiId]`, - query: { wikiId }, - }} - isActive={pathname === '/share/wiki/[wikiId]'} - /> - - <div className={styles.treeWrap} style={{ marginTop: 12 }}> <DataRender - loading={tocsLoading} + loading={wikiLoading} loadingContent={ <NavItem icon={ @@ -123,22 +68,81 @@ export const WikiPublicTocs: React.FC<IProps> = ({ ></Skeleton.Avatar> } text={<Skeleton.Title style={{ width: 120 }} />} - rightNode={<IconPlus />} /> } - error={tocsError} + error={wikiError} normalContent={() => ( - <Tree - data={tocs || []} - docAsLink={docAsLink} - getDocLink={getDocLink} - parentIds={parentIds} - activeId={documentId} - isShareMode - /> + <> + <Seo title={wiki.name + ' - ' + pageTitle} /> + <NavItem + icon={ + <Avatar + shape="square" + size="small" + src={wiki.avatar} + style={{ + marginRight: 8, + width: 24, + height: 24, + borderRadius: 4, + }} + > + {wiki.name.charAt(0)} + </Avatar> + } + text={<Text strong>{wiki.name}</Text>} + hoverable={false} + /> + </> )} /> - </div> + + <NavItem + icon={<IconOverview />} + text={'概述'} + href={{ + pathname: `/share/wiki/[wikiId]`, + query: { wikiId }, + }} + isActive={pathname === '/share/wiki/[wikiId]'} + /> + </header> + + <main> + <div className={styles.treeWrap}> + <DataRender + loading={tocsLoading} + 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 />} + /> + } + error={tocsError} + normalContent={() => ( + <Tree + data={tocs || []} + docAsLink={docAsLink} + getDocLink={getDocLink} + parentIds={parentIds} + activeId={documentId} + isShareMode + /> + )} + /> + </div> + </main> </div> ); }; diff --git a/packages/client/src/components/wiki/tocs/tree.tsx b/packages/client/src/components/wiki/tocs/tree.tsx index 559eeb5a..a93492f4 100644 --- a/packages/client/src/components/wiki/tocs/tree.tsx +++ b/packages/client/src/components/wiki/tocs/tree.tsx @@ -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 />} - size="small" - /> - </DocumentActions> + <DocumentActions + key={node.id} + hoverVisible + wikiId={node.wikiId} + documentId={node.id} + size="small" + 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(() => { + const target = $container.current.querySelector(`#item-${activeId}`); + if (!target) return; clearTimeout(scrollTimer); scrollTimer = setTimeout(() => { - const target = $container.current.querySelector(`#item-${activeId}`); - if (!target) return; - target.scrollIntoView(); 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> ); }; diff --git a/packages/client/src/data/collector.ts b/packages/client/src/data/collector.ts deleted file mode 100644 index 1b8fe884..00000000 --- a/packages/client/src/data/collector.ts +++ /dev/null @@ -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 }; -}; diff --git a/packages/client/src/data/star.ts b/packages/client/src/data/star.ts new file mode 100644 index 00000000..e56ade8c --- /dev/null +++ b/packages/client/src/data/star.ts @@ -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 }; +}; diff --git a/packages/client/src/event/index.ts b/packages/client/src/event/index.ts index dd21f481..3a3627ff 100644 --- a/packages/client/src/event/index.ts +++ b/packages/client/src/event/index.ts @@ -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); }; diff --git a/packages/client/src/hooks/use-document-version.tsx b/packages/client/src/hooks/use-document-version.tsx new file mode 100644 index 00000000..ea8cdd08 --- /dev/null +++ b/packages/client/src/hooks/use-document-version.tsx @@ -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); diff --git a/packages/client/src/layouts/router-header/recent.tsx b/packages/client/src/layouts/router-header/recent.tsx index 233a0881..2df27f4d 100644 --- a/packages/client/src/layouts/router-header/recent.tsx +++ b/packages/client/src/layouts/router-header/recent.tsx @@ -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> diff --git a/packages/client/src/layouts/router-header/wiki.tsx b/packages/client/src/layouts/router-header/wiki.tsx index 77fb3e16..431538a8 100644 --- a/packages/client/src/layouts/router-header/wiki.tsx +++ b/packages/client/src/layouts/router-header/wiki.tsx @@ -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 ( diff --git a/packages/client/src/pages/_app.tsx b/packages/client/src/pages/_app.tsx index 66362888..a25b73bf 100644 --- a/packages/client/src/pages/_app.tsx +++ b/packages/client/src/pages/_app.tsx @@ -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}> - <Component {...pageProps} /> + <DocumentVersionControl.Provider initialState={false}> + <Component {...pageProps} /> + </DocumentVersionControl.Provider> </IsOnMobile.Provider> </Theme.Provider> </Hydrate> diff --git a/packages/client/src/pages/index.tsx b/packages/client/src/pages/index.tsx index a06785c7..5b809cfe 100644 --- a/packages/client/src/pages/index.tsx +++ b/packages/client/src/pages/index.tsx @@ -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; diff --git a/packages/client/src/pages/star/index.tsx b/packages/client/src/pages/star/index.tsx index c020e5fa..0cfef3e8 100644 --- a/packages/client/src/pages/star/index.tsx +++ b/packages/client/src/pages/star/index.tsx @@ -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; }; diff --git a/packages/client/src/pages/wiki/[wikiId]/documents/index.module.scss b/packages/client/src/pages/wiki/[wikiId]/documents/index.module.scss deleted file mode 100644 index 80e9b935..00000000 --- a/packages/client/src/pages/wiki/[wikiId]/documents/index.module.scss +++ /dev/null @@ -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; - } -} diff --git a/packages/client/src/pages/wiki/[wikiId]/documents/index.tsx b/packages/client/src/pages/wiki/[wikiId]/documents/index.tsx deleted file mode 100644 index 42f0ba04..00000000 --- a/packages/client/src/pages/wiki/[wikiId]/documents/index.tsx +++ /dev/null @@ -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; diff --git a/packages/client/src/tiptap/core/thritypart/y-prosemirror/plugins/awareness.js b/packages/client/src/tiptap/core/thritypart/y-prosemirror/plugins/awareness.js index 8b6a6f46..5b175428 100644 --- a/packages/client/src/tiptap/core/thritypart/y-prosemirror/plugins/awareness.js +++ b/packages/client/src/tiptap/core/thritypart/y-prosemirror/plugins/awareness.js @@ -56,30 +56,28 @@ export class Awareness extends Observable { * @type {Map<number, MetaClientState>} */ this.meta = new Map(); - 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 - ) { - // renew local clock - this.setLocalState(this.getLocalState()); + 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 + ) { + // renew local clock + this.setLocalState(this.getLocalState()); + } + /** + * @type {Array<number>} + */ + const remove = []; + this.meta.forEach((meta, clientid) => { + if (clientid !== this.clientID && outdatedTimeout <= now - meta.lastUpdated && this.states.has(clientid)) { + remove.push(clientid); } - /** - * @type {Array<number>} - */ - const remove = []; - this.meta.forEach((meta, clientid) => { - if (clientid !== this.clientID && outdatedTimeout <= now - meta.lastUpdated && this.states.has(clientid)) { - remove.push(clientid); - } - }); - if (remove.length > 0) { - removeAwarenessStates(this, remove, 'timeout'); - } - }, math.floor(outdatedTimeout / 10)) - ); + }); + if (remove.length > 0) { + removeAwarenessStates(this, remove, 'timeout'); + } + }, 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)); diff --git a/packages/client/src/tiptap/core/wrappers/title/index.module.scss b/packages/client/src/tiptap/core/wrappers/title/index.module.scss index cdd8088a..4388c35a 100644 --- a/packages/client/src/tiptap/core/wrappers/title/index.module.scss +++ b/packages/client/src/tiptap/core/wrappers/title/index.module.scss @@ -1,7 +1,7 @@ .wrap { position: relative; - overflow: auto; margin-top: 24px; + overflow: auto; .coverWrap { position: relative; diff --git a/packages/client/src/tiptap/editor/collaboration/collaboration/index.tsx b/packages/client/src/tiptap/editor/collaboration/collaboration/index.tsx index 5f6ce80d..980bf3f2 100644 --- a/packages/client/src/tiptap/editor/collaboration/collaboration/index.tsx +++ b/packages/client/src/tiptap/editor/collaboration/collaboration/index.tsx @@ -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); diff --git a/packages/domains/lib/api/file.d.ts b/packages/domains/lib/api/file.d.ts index 87590036..72e14e3e 100644 --- a/packages/domains/lib/api/file.d.ts +++ b/packages/domains/lib/api/file.d.ts @@ -8,7 +8,7 @@ export declare const FileApiDefinition: { client: () => string; }; /** - * 上传分块文件 + * 初始分块上传 */ initChunk: { method: "post"; diff --git a/packages/domains/lib/api/file.js b/packages/domains/lib/api/file.js index 14bef717..e01b5f04 100644 --- a/packages/domains/lib/api/file.js +++ b/packages/domains/lib/api/file.js @@ -11,7 +11,7 @@ exports.FileApiDefinition = { client: function () { return '/file/upload'; } }, /** - * 上传分块文件 + * 初始分块上传 */ initChunk: { method: 'post', diff --git a/packages/domains/lib/api/index.d.ts b/packages/domains/lib/api/index.d.ts index 8cc7ffad..d53ffd45 100644 --- a/packages/domains/lib/api/index.d.ts +++ b/packages/domains/lib/api/index.d.ts @@ -5,4 +5,4 @@ export * from './file'; export * from './message'; export * from './template'; export * from './comment'; -export * from './collector'; +export * from './star'; diff --git a/packages/domains/lib/api/index.js b/packages/domains/lib/api/index.js index 2df03c3a..57e2c6aa 100644 --- a/packages/domains/lib/api/index.js +++ b/packages/domains/lib/api/index.js @@ -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); diff --git a/packages/domains/lib/api/star.d.ts b/packages/domains/lib/api/star.d.ts new file mode 100644 index 00000000..bc698aa6 --- /dev/null +++ b/packages/domains/lib/api/star.d.ts @@ -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; + }; +}; diff --git a/packages/domains/lib/api/star.js b/packages/domains/lib/api/star.js new file mode 100644 index 00000000..21d7ea7f --- /dev/null +++ b/packages/domains/lib/api/star.js @@ -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'; } + } +}; diff --git a/packages/domains/lib/models/index.d.ts b/packages/domains/lib/models/index.d.ts index 7f718fc3..f9692ca8 100644 --- a/packages/domains/lib/models/index.d.ts +++ b/packages/domains/lib/models/index.d.ts @@ -4,5 +4,4 @@ export * from './document'; export * from './message'; export * from './template'; export * from './comment'; -export * from './collector'; export * from './pagination'; diff --git a/packages/domains/lib/models/index.js b/packages/domains/lib/models/index.js index 0088a0b6..d3a790c4 100644 --- a/packages/domains/lib/models/index.js +++ b/packages/domains/lib/models/index.js @@ -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); diff --git a/packages/domains/src/api/index.ts b/packages/domains/src/api/index.ts index 8cc7ffad..d53ffd45 100644 --- a/packages/domains/src/api/index.ts +++ b/packages/domains/src/api/index.ts @@ -5,4 +5,4 @@ export * from './file'; export * from './message'; export * from './template'; export * from './comment'; -export * from './collector'; +export * from './star'; diff --git a/packages/domains/src/api/collector.ts b/packages/domains/src/api/star.ts similarity index 57% rename from packages/domains/src/api/collector.ts rename to packages/domains/src/api/star.ts index 66fa0ec6..d85ae24f 100644 --- a/packages/domains/src/api/collector.ts +++ b/packages/domains/src/api/star.ts @@ -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', }, }; diff --git a/packages/domains/src/models/collector.ts b/packages/domains/src/models/collector.ts deleted file mode 100644 index 4524d597..00000000 --- a/packages/domains/src/models/collector.ts +++ /dev/null @@ -1,4 +0,0 @@ -export enum CollectType { - document = 'document', - wiki = 'wiki', -} diff --git a/packages/domains/src/models/index.ts b/packages/domains/src/models/index.ts index 7f718fc3..f9692ca8 100644 --- a/packages/domains/src/models/index.ts +++ b/packages/domains/src/models/index.ts @@ -4,5 +4,4 @@ export * from './document'; export * from './message'; export * from './template'; export * from './comment'; -export * from './collector'; export * from './pagination'; diff --git a/packages/server/src/app.module.ts b/packages/server/src/app.module.ts index fc29e2a6..bfc10bc4 100644 --- a/packages/server/src/app.module.ts +++ b/packages/server/src/app.module.ts @@ -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, diff --git a/packages/server/src/controllers/collector.controller.ts b/packages/server/src/controllers/collector.controller.ts deleted file mode 100644 index 3eb17942..00000000 --- a/packages/server/src/controllers/collector.controller.ts +++ /dev/null @@ -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); - } -} diff --git a/packages/server/src/controllers/star.controller.ts b/packages/server/src/controllers/star.controller.ts new file mode 100644 index 00000000..213d221b --- /dev/null +++ b/packages/server/src/controllers/star.controller.ts @@ -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); + } +} diff --git a/packages/server/src/dtos/collect.dto.ts b/packages/server/src/dtos/collect.dto.ts deleted file mode 100644 index 8023944f..00000000 --- a/packages/server/src/dtos/collect.dto.ts +++ /dev/null @@ -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; -} diff --git a/packages/server/src/dtos/star.dto.ts b/packages/server/src/dtos/star.dto.ts new file mode 100644 index 00000000..eabd77d9 --- /dev/null +++ b/packages/server/src/dtos/star.dto.ts @@ -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; +} diff --git a/packages/server/src/entities/collector.entity.ts b/packages/server/src/entities/star.entity.ts similarity index 58% rename from packages/server/src/entities/collector.entity.ts rename to packages/server/src/entities/star.entity.ts index 8bbd8ad0..be2766ba 100644 --- a/packages/server/src/entities/collector.entity.ts +++ b/packages/server/src/entities/star.entity.ts @@ -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', diff --git a/packages/server/src/main.ts b/packages/server/src/main.ts index 25c94e81..a0e02529 100644 --- a/packages/server/src/main.ts +++ b/packages/server/src/main.ts @@ -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); diff --git a/packages/server/src/modules/document.module.ts b/packages/server/src/modules/document.module.ts index 1240c9a8..e8b404f0 100644 --- a/packages/server/src/modules/document.module.ts +++ b/packages/server/src/modules/document.module.ts @@ -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], diff --git a/packages/server/src/modules/collector.module.ts b/packages/server/src/modules/star.module.ts similarity index 51% rename from packages/server/src/modules/collector.module.ts rename to packages/server/src/modules/star.module.ts index 01713cff..01b2d756 100644 --- a/packages/server/src/modules/collector.module.ts +++ b/packages/server/src/modules/star.module.ts @@ -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 {} diff --git a/packages/server/src/modules/user.module.ts b/packages/server/src/modules/user.module.ts index 50bde5be..6eaf195a 100644 --- a/packages/server/src/modules/user.module.ts +++ b/packages/server/src/modules/user.module.ts @@ -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, ], diff --git a/packages/server/src/modules/wiki.module.ts b/packages/server/src/modules/wiki.module.ts index 85750e1f..4efda75c 100644 --- a/packages/server/src/modules/wiki.module.ts +++ b/packages/server/src/modules/wiki.module.ts @@ -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], diff --git a/packages/server/src/services/collector.service.ts b/packages/server/src/services/collector.service.ts deleted file mode 100644 index 260257e4..00000000 --- a/packages/server/src/services/collector.service.ts +++ /dev/null @@ -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']); - }); - } -} diff --git a/packages/server/src/services/star.service.ts b/packages/server/src/services/star.service.ts new file mode 100644 index 00000000..304d15ee --- /dev/null +++ b/packages/server/src/services/star.service.ts @@ -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']); + }); + } +} diff --git a/packages/server/src/services/user.service.ts b/packages/server/src/services/user.service.ts index e82265c9..b96fbed1 100644 --- a/packages/server/src/services/user.service.ts +++ b/packages/server/src/services/user.service.ts @@ -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}」`, diff --git a/packages/server/src/services/wiki.service.ts b/packages/server/src/services/wiki.service.ts index 349a5308..5070c5f2 100644 --- a/packages/server/src/services/wiki.service.ts +++ b/packages/server/src/services/wiki.service.ts @@ -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 });