mirror of https://github.com/fantasticit/think.git
chore: add user in document version
This commit is contained in:
parent
c05b98b58b
commit
a316f97bb9
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 };
|
||||
};
|
||||
|
|
|
@ -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
|
||||
);
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue