chore: add user in document version

This commit is contained in:
fantasticit 2022-05-26 12:29:50 +08:00
parent c05b98b58b
commit a316f97bb9
6 changed files with 159 additions and 76 deletions

View File

@ -16,34 +16,73 @@
}
.contentWrap {
position: relative;
display: flex;
height: calc(100vh - 56px);
margin: 0 -24px;
flex-wrap: nowrap;
:global {
.semi-navigation-inner {
flex-direction: column;
main {
padding: 24px;
overflow: auto;
background-color: var(--semi-color-bg-0);
flex: 1;
.semi-navigation-header-list-outer {
flex: 1;
height: calc(100% - 64px);
}
&.isMobile {
padding: 0;
overflow: hidden;
.semi-navigation-footer {
display: block;
.semi-navigation-collapse-btn {
display: flex;
align-items: center;
justify-content: flex-start;
}
> div {
height: 100%;
padding: 24px;
margin-top: 71px;
overflow: auto;
}
}
}
.selected {
color: var(--semi-color-primary);
background-color: var(--semi-color-primary-light-default);
aside {
width: 240px;
border-left: 1px solid var(--semi-color-border);
&.isMobile {
position: absolute;
top: 0;
left: 0;
display: flex;
width: 100%;
height: 71px;
padding: 8px 24px;
overflow: auto;
border-left: 0;
border-bottom: 1px solid var(--semi-color-border);
flex-wrap: nowrap;
}
.item {
display: flex;
height: 54px;
padding: 8px 12px;
margin-top: 8px;
white-space: nowrap;
cursor: pointer;
border-radius: var(--semi-border-radius-small);
align-items: center;
flex-direction: column;
&.isMobile {
margin: 0;
}
&:hover {
color: var(--semi-color-text-1);
background-color: var(--semi-color-fill-1);
}
&.selected {
color: var(--semi-color-primary);
background-color: var(--semi-color-primary-light-default);
}
}
}
}

View File

@ -1,11 +1,12 @@
import { IconChevronLeft } from '@douyinfe/semi-icons';
import { Button, Layout, Modal, Nav, Typography } from '@douyinfe/semi-ui';
import { Button, Modal, Typography } from '@douyinfe/semi-ui';
import { EditorContent, useEditor } from '@tiptap/react';
import cls from 'classnames';
import { DataRender } from 'components/data-render';
import { LocaleTime } from 'components/locale-time';
import { useDocumentVersion } from 'data/document';
import { safeJSONParse } from 'helpers/json';
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';
@ -18,12 +19,12 @@ interface IProps {
onSelect?: (data) => void;
}
const { Sider, Content } = Layout;
const { Title } = Typography;
const { Title, Text } = Typography;
export const DocumentVersion: React.FC<IProps> = ({ documentId, disabled = false, onSelect }) => {
const { isMobile } = IsOnMobile.useHook();
const [visible, toggleVisible] = useToggle(false);
const { data, loading, error, refresh } = useDocumentVersion(documentId);
const { data, loading, error, refresh } = useDocumentVersion(documentId, { enabled: visible });
const [selectedVersion, setSelectedVersion] = useState(null);
const editor = useEditor({
@ -83,6 +84,11 @@ export const DocumentVersion: React.FC<IProps> = ({ documentId, disabled = false
</Title>
</div>
<div>
<Button type="primary" theme="solid" disabled={!onSelect || !selectedVersion} onClick={restore}>
</Button>
</div>
<div>
<Button
theme="light"
@ -90,15 +96,10 @@ export const DocumentVersion: React.FC<IProps> = ({ documentId, disabled = false
style={{ marginRight: 8 }}
disabled={loading || !!error}
loading={loading}
onClick={() => refresh()}
onClick={refresh}
>
</Button>
{onSelect && (
<Button type="primary" theme="solid" disabled={!selectedVersion} onClick={restore}>
</Button>
)}
</div>
</div>
}
@ -109,40 +110,35 @@ export const DocumentVersion: React.FC<IProps> = ({ documentId, disabled = false
error={error}
empty={!loading && !data.length}
normalContent={() => (
<Layout className={styles.contentWrap}>
<Sider style={{ backgroundColor: 'var(--semi-color-bg-1)' }}>
<Nav
style={{ maxWidth: 200, height: '100%' }}
bodyStyle={{ height: '100%' }}
selectedKeys={[selectedVersion]}
footer={{
collapseButton: true,
}}
>
{data.map(({ version, data }) => {
return (
<Nav.Item
key={version}
itemKey={version}
className={cls(selectedVersion && selectedVersion.version === version && styles.selected)}
text={<LocaleTime date={+version} />}
onClick={() => select({ version, data })}
/>
);
})}
</Nav>
</Sider>
<Content
style={{
padding: 16,
backgroundColor: 'var(--semi-color-bg-0)',
}}
>
<div className={styles.contentWrap}>
<main className={cls(isMobile && styles.isMobile)}>
<div className={'container'}>
<EditorContent editor={editor} />
</div>
</Content>
</Layout>
</main>
<aside className={cls(isMobile && styles.isMobile)}>
{data.map(({ version, data, createUser }) => {
return (
<div
key={version}
className={cls(
styles.item,
isMobile && styles.isMobile,
selectedVersion && selectedVersion.version === version && styles.selected
)}
onClick={() => select({ version, data })}
>
<p>
<LocaleTime date={+version} />
</p>
<p>
<Text>{createUser && createUser.name}</Text>
</p>
</div>
);
})}
</aside>
</div>
)}
/>
</Modal>

View File

@ -2,7 +2,7 @@ import { DocumentApiDefinition, IAuthority, IDocument, IUser, IWiki } from '@thi
import { triggerRefreshTocs } from 'event';
import { useAsyncLoading } from 'hooks/use-async-loading';
import { useCallback, useState } from 'react';
import { useQuery } from 'react-query';
import { QueriesOptions, useQuery, UseQueryOptions } from 'react-query';
import { HttpClient } from 'services/http-client';
type IDocumentWithVisitedAt = IDocument & { visitedAt: string };
@ -186,7 +186,10 @@ export const useDocumentDetail = (documentId) => {
* @param documentId
* @returns
*/
export const getDocumentVersion = (documentId, cookie = null): Promise<Array<{ version: string; data: string }>> => {
export const getDocumentVersion = (
documentId,
cookie = null
): Promise<Array<{ version: string; data: string; createUser: IUser }>> => {
return HttpClient.request({
method: DocumentApiDefinition.getVersionById.method,
url: DocumentApiDefinition.getVersionById.client(documentId),
@ -199,9 +202,14 @@ export const getDocumentVersion = (documentId, cookie = null): Promise<Array<{ v
* @param documentId
* @returns
*/
export const useDocumentVersion = (documentId) => {
const { data, error, isLoading, refetch } = useQuery(DocumentApiDefinition.getVersionById.client(documentId), () =>
getDocumentVersion(documentId)
export const useDocumentVersion = (
documentId,
options: UseQueryOptions<Array<{ version: string; data: string; createUser: IUser }>>
) => {
const { data, error, isLoading, refetch } = useQuery(
DocumentApiDefinition.getVersionById.client(documentId),
() => getDocumentVersion(documentId),
options
);
return { data: data || [], loading: isLoading, error, refresh: refetch };
};

View File

@ -176,13 +176,18 @@ export class CollaborationService {
const targetId = requestParameters.get('targetId');
const docType = requestParameters.get('docType');
const userId = requestParameters.get('userId');
const updateDocument = async (user: OutUser, documentId: string, data) => {
await this.documentService.updateDocument(user, documentId, data);
this.debounce(
`onStoreDocumentVersion-${documentId}`,
() => {
this.documentVersionService.storeDocumentVersion(documentId, data.content);
this.documentVersionService.storeDocumentVersion({
documentId,
data: data.content,
userId,
});
},
this.debounceTime * 2
);

View File

@ -1,10 +1,12 @@
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { forwardRef, HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common';
import { getConfig } from '@think/config';
import { IDocument } from '@think/domains';
import { IDocument, IUser } from '@think/domains';
import Redis from 'ioredis';
import * as lodash from 'lodash';
type VerisonDataItem = { version: string; data: string };
import { UserService } from './user.service';
type VerisonDataItem = { version: string; data: string; userId: IUser['id']; createUser: IUser };
@Injectable()
export class DocumentVersionService {
@ -12,14 +14,36 @@ export class DocumentVersionService {
private max = 0;
private error: string | null = '[think] 文档版本服务启动中';
constructor() {
constructor(
@Inject(forwardRef(() => UserService))
private readonly userService: UserService
) {
this.init();
}
private versionDataToArray(data: Record<string, string>): Array<VerisonDataItem> {
private async withUser(data: Array<Omit<VerisonDataItem, 'createUser'>>): Promise<VerisonDataItem[]> {
return await Promise.all(
data.filter(Boolean).map(async (record) => {
const { userId } = record;
const createUser = await this.userService.findById(userId);
return { ...record, createUser };
})
);
}
private versionDataToArray(data: Record<string, string>): Array<Omit<VerisonDataItem, 'createUser'>> {
return Object.keys(data)
.sort((a, b) => +b - +a)
.map((key) => ({ version: key, data: data[key] }));
.map((key) => {
const str = data[key];
try {
const json = JSON.parse(str);
return { version: key, ...json };
} catch (e) {
return null;
}
})
.filter(Boolean);
}
private async init() {
@ -66,7 +90,7 @@ export class DocumentVersionService {
if (this.max <= 0) return;
const res = await this.redis.hgetall(documentId);
if (!res) return;
const data = this.versionDataToArray(res);
const data = await this.versionDataToArray(res);
while (data.length > this.max) {
const lastVersion = data.pop().version;
@ -80,10 +104,20 @@ export class DocumentVersionService {
* @param data
* @returns
*/
public async storeDocumentVersion(documentId: IDocument['id'], data: IDocument['content']) {
public async storeDocumentVersion(arg: {
documentId: IDocument['id'];
data: IDocument['content'];
userId: IUser['id'];
}) {
if (!this.redis) return;
const { documentId, data, userId } = arg;
const storeData = JSON.stringify({
data,
userId,
});
const version = '' + Date.now();
await this.redis.hsetnx(documentId, version, data);
await this.redis.hsetnx(documentId, version, storeData);
await this.checkCacheLength(documentId);
}
@ -97,6 +131,7 @@ export class DocumentVersionService {
throw new HttpException(this.error, HttpStatus.NOT_IMPLEMENTED);
}
const res = await this.redis.hgetall(documentId);
return res ? this.versionDataToArray(res) : [];
if (!res) return [];
return await this.withUser(this.versionDataToArray(res));
}
}

View File

@ -50,7 +50,7 @@ export class DocumentService {
@Inject(forwardRef(() => ViewService))
private readonly viewService: ViewService
) {
this.documentVersionService = new DocumentVersionService();
this.documentVersionService = new DocumentVersionService(this.userService);
this.collaborationService = new CollaborationService(
this.userService,
this,