mirror of
https://github.com/docmost/docmost.git
synced 2026-05-21 01:04:39 +08:00
feat: update page content
This commit is contained in:
@@ -64,6 +64,10 @@ export class CollaborationGateway {
|
|||||||
return this.hocuspocus.getDocumentsCount();
|
return this.hocuspocus.getDocumentsCount();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
openDirectConnection(documentName: string, context?: any) {
|
||||||
|
return this.hocuspocus.openDirectConnection(documentName, context);
|
||||||
|
}
|
||||||
|
|
||||||
async destroy(): Promise<void> {
|
async destroy(): Promise<void> {
|
||||||
await this.hocuspocus.destroy();
|
await this.hocuspocus.destroy();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ import { generateHTML, generateJSON } from '../common/helpers/prosemirror/html';
|
|||||||
// see: https://github.com/ueberdosis/tiptap/issues/5352
|
// see: https://github.com/ueberdosis/tiptap/issues/5352
|
||||||
// see:https://github.com/ueberdosis/tiptap/issues/4089
|
// see:https://github.com/ueberdosis/tiptap/issues/4089
|
||||||
import { Node } from '@tiptap/pm/model';
|
import { Node } from '@tiptap/pm/model';
|
||||||
|
import * as Y from 'yjs';
|
||||||
|
|
||||||
export const tiptapExtensions = [
|
export const tiptapExtensions = [
|
||||||
StarterKit.configure({
|
StarterKit.configure({
|
||||||
@@ -116,3 +117,32 @@ export function jsonToNode(tiptapJson: JSONContent) {
|
|||||||
export function getPageId(documentName: string) {
|
export function getPageId(documentName: string) {
|
||||||
return documentName.split('.')[1];
|
return documentName.split('.')[1];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function prosemirrorNodeToYElement(node: any): Y.XmlElement | Y.XmlText {
|
||||||
|
if (node.type === 'text') {
|
||||||
|
const ytext = new Y.XmlText();
|
||||||
|
ytext.insert(0, node.text || '');
|
||||||
|
if (node.marks?.length > 0) {
|
||||||
|
const attrs: Record<string, any> = {};
|
||||||
|
for (const mark of node.marks) {
|
||||||
|
attrs[mark.type] = mark.attrs || true;
|
||||||
|
}
|
||||||
|
ytext.format(0, node.text?.length || 0, attrs);
|
||||||
|
}
|
||||||
|
return ytext;
|
||||||
|
}
|
||||||
|
|
||||||
|
const element = new Y.XmlElement(node.type);
|
||||||
|
if (node.attrs) {
|
||||||
|
for (const [key, value] of Object.entries(node.attrs)) {
|
||||||
|
if (value !== null && value !== undefined) {
|
||||||
|
element.setAttribute(key, value as any);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (node.content?.length > 0) {
|
||||||
|
const children = node.content.map(prosemirrorNodeToYElement);
|
||||||
|
element.insert(0, children);
|
||||||
|
}
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
|||||||
@@ -179,7 +179,7 @@ export class PersistenceExtension implements Extension {
|
|||||||
|
|
||||||
async onChange(data: onChangePayload) {
|
async onChange(data: onChangePayload) {
|
||||||
const documentName = data.documentName;
|
const documentName = data.documentName;
|
||||||
const userId = data.context?.user.id;
|
const userId = data.context?.user?.id;
|
||||||
if (!userId) return;
|
if (!userId) return;
|
||||||
|
|
||||||
if (!this.contributors.has(documentName)) {
|
if (!this.contributors.has(documentName)) {
|
||||||
|
|||||||
@@ -1,8 +1,18 @@
|
|||||||
import { PartialType } from '@nestjs/mapped-types';
|
import { PartialType } from '@nestjs/mapped-types';
|
||||||
import { CreatePageDto } from './create-page.dto';
|
import { CreatePageDto } from './create-page.dto';
|
||||||
import { IsString } from 'class-validator';
|
import { IsIn, IsOptional, IsString, ValidateIf } from 'class-validator';
|
||||||
|
|
||||||
|
export type ContentMode = 'append' | 'replace';
|
||||||
|
|
||||||
export class UpdatePageDto extends PartialType(CreatePageDto) {
|
export class UpdatePageDto extends PartialType(CreatePageDto) {
|
||||||
@IsString()
|
@IsString()
|
||||||
pageId: string;
|
pageId: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
content?: string;
|
||||||
|
|
||||||
|
@ValidateIf((o) => o.content !== undefined)
|
||||||
|
@IsIn(['append', 'replace'])
|
||||||
|
contentMode?: ContentMode;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,11 +4,12 @@ import { PageController } from './page.controller';
|
|||||||
import { PageHistoryService } from './services/page-history.service';
|
import { PageHistoryService } from './services/page-history.service';
|
||||||
import { TrashCleanupService } from './services/trash-cleanup.service';
|
import { TrashCleanupService } from './services/trash-cleanup.service';
|
||||||
import { StorageModule } from '../../integrations/storage/storage.module';
|
import { StorageModule } from '../../integrations/storage/storage.module';
|
||||||
|
import { CollaborationModule } from '../../collaboration/collaboration.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
controllers: [PageController],
|
controllers: [PageController],
|
||||||
providers: [PageService, PageHistoryService, TrashCleanupService],
|
providers: [PageService, PageHistoryService, TrashCleanupService],
|
||||||
exports: [PageService, PageHistoryService],
|
exports: [PageService, PageHistoryService],
|
||||||
imports: [StorageModule],
|
imports: [StorageModule, CollaborationModule],
|
||||||
})
|
})
|
||||||
export class PageModule {}
|
export class PageModule {}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
NotFoundException,
|
NotFoundException,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { CreatePageDto } from '../dto/create-page.dto';
|
import { CreatePageDto } from '../dto/create-page.dto';
|
||||||
import { UpdatePageDto } from '../dto/update-page.dto';
|
import { ContentMode, UpdatePageDto } from '../dto/update-page.dto';
|
||||||
import { PageRepo } from '@docmost/db/repos/page/page.repo';
|
import { PageRepo } from '@docmost/db/repos/page/page.repo';
|
||||||
import { InsertablePage, Page, User } from '@docmost/db/types/entity.types';
|
import { InsertablePage, Page, User } from '@docmost/db/types/entity.types';
|
||||||
import { PaginationOptions } from '@docmost/db/pagination/pagination-options';
|
import { PaginationOptions } from '@docmost/db/pagination/pagination-options';
|
||||||
@@ -28,7 +28,13 @@ import {
|
|||||||
isAttachmentNode,
|
isAttachmentNode,
|
||||||
removeMarkTypeFromDoc,
|
removeMarkTypeFromDoc,
|
||||||
} from '../../../common/helpers/prosemirror/utils';
|
} from '../../../common/helpers/prosemirror/utils';
|
||||||
import { jsonToNode, jsonToText } from 'src/collaboration/collaboration.util';
|
import {
|
||||||
|
htmlToJson,
|
||||||
|
jsonToNode,
|
||||||
|
jsonToText,
|
||||||
|
prosemirrorNodeToYElement,
|
||||||
|
tiptapExtensions,
|
||||||
|
} from 'src/collaboration/collaboration.util';
|
||||||
import {
|
import {
|
||||||
CopyPageMapEntry,
|
CopyPageMapEntry,
|
||||||
ICopyPageAttachment,
|
ICopyPageAttachment,
|
||||||
@@ -40,6 +46,10 @@ import { Queue } from 'bullmq';
|
|||||||
import { QueueJob, QueueName } from '../../../integrations/queue/constants';
|
import { QueueJob, QueueName } from '../../../integrations/queue/constants';
|
||||||
import { EventName } from '../../../common/events/event.contants';
|
import { EventName } from '../../../common/events/event.contants';
|
||||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
|
import { CollaborationGateway } from '../../../collaboration/collaboration.gateway';
|
||||||
|
import { markdownToHtml } from '@docmost/editor-ext';
|
||||||
|
import { TiptapTransformer } from '@hocuspocus/transformer';
|
||||||
|
import * as Y from 'yjs';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PageService {
|
export class PageService {
|
||||||
@@ -53,6 +63,7 @@ export class PageService {
|
|||||||
@InjectQueue(QueueName.ATTACHMENT_QUEUE) private attachmentQueue: Queue,
|
@InjectQueue(QueueName.ATTACHMENT_QUEUE) private attachmentQueue: Queue,
|
||||||
@InjectQueue(QueueName.AI_QUEUE) private aiQueue: Queue,
|
@InjectQueue(QueueName.AI_QUEUE) private aiQueue: Queue,
|
||||||
private eventEmitter: EventEmitter2,
|
private eventEmitter: EventEmitter2,
|
||||||
|
private collaborationGateway: CollaborationGateway,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async findById(
|
async findById(
|
||||||
@@ -167,6 +178,15 @@ export class PageService {
|
|||||||
page.id,
|
page.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (updatePageDto.content && updatePageDto.contentMode) {
|
||||||
|
await this.updatePageContent(
|
||||||
|
page.id,
|
||||||
|
updatePageDto.content,
|
||||||
|
updatePageDto.contentMode,
|
||||||
|
userId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return await this.pageRepo.findById(page.id, {
|
return await this.pageRepo.findById(page.id, {
|
||||||
includeSpace: true,
|
includeSpace: true,
|
||||||
includeContent: true,
|
includeContent: true,
|
||||||
@@ -176,6 +196,46 @@ export class PageService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async updatePageContent(
|
||||||
|
pageId: string,
|
||||||
|
markdown: string,
|
||||||
|
mode: ContentMode,
|
||||||
|
userId: string,
|
||||||
|
): Promise<void> {
|
||||||
|
const html = await markdownToHtml(markdown);
|
||||||
|
const prosemirrorJson = htmlToJson(html as string);
|
||||||
|
|
||||||
|
const documentName = `page.${pageId}`;
|
||||||
|
const connection = await this.collaborationGateway.openDirectConnection(
|
||||||
|
documentName,
|
||||||
|
{ user: { id: userId } },
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await connection.transact((doc) => {
|
||||||
|
const fragment = doc.getXmlFragment('default');
|
||||||
|
|
||||||
|
if (mode === 'replace') {
|
||||||
|
while (fragment.length > 0) {
|
||||||
|
fragment.delete(0, 1);
|
||||||
|
}
|
||||||
|
const newDoc = TiptapTransformer.toYdoc(
|
||||||
|
prosemirrorJson,
|
||||||
|
'default',
|
||||||
|
tiptapExtensions,
|
||||||
|
);
|
||||||
|
Y.applyUpdate(doc, Y.encodeStateAsUpdate(newDoc));
|
||||||
|
} else {
|
||||||
|
const newContent = prosemirrorJson.content || [];
|
||||||
|
const yElements = newContent.map(prosemirrorNodeToYElement);
|
||||||
|
fragment.insert(fragment.length, yElements);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
await connection.disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async getSidebarPages(
|
async getSidebarPages(
|
||||||
spaceId: string,
|
spaceId: string,
|
||||||
pagination: PaginationOptions,
|
pagination: PaginationOptions,
|
||||||
@@ -259,7 +319,7 @@ export class PageService {
|
|||||||
|
|
||||||
await this.aiQueue.add(QueueJob.PAGE_MOVED_TO_SPACE, {
|
await this.aiQueue.add(QueueJob.PAGE_MOVED_TO_SPACE, {
|
||||||
pageId: pageIds,
|
pageId: pageIds,
|
||||||
workspaceId: rootPage.workspaceId
|
workspaceId: rootPage.workspaceId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -387,9 +447,14 @@ export class PageService {
|
|||||||
workspaceId: page.workspaceId,
|
workspaceId: page.workspaceId,
|
||||||
creatorId: authUser.id,
|
creatorId: authUser.id,
|
||||||
lastUpdatedById: authUser.id,
|
lastUpdatedById: authUser.id,
|
||||||
parentPageId: page.id === rootPage.id
|
parentPageId:
|
||||||
? (isDuplicateInSameSpace ? rootPage.parentPageId : null)
|
page.id === rootPage.id
|
||||||
: (page.parentPageId ? pageMap.get(page.parentPageId)?.newPageId : null),
|
? isDuplicateInSameSpace
|
||||||
|
? rootPage.parentPageId
|
||||||
|
: null
|
||||||
|
: page.parentPageId
|
||||||
|
? pageMap.get(page.parentPageId)?.newPageId
|
||||||
|
: null,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user