diff --git a/apps/server/src/collaboration/collaboration.handler.ts b/apps/server/src/collaboration/collaboration.handler.ts index 37631c7a..37842a03 100644 --- a/apps/server/src/collaboration/collaboration.handler.ts +++ b/apps/server/src/collaboration/collaboration.handler.ts @@ -30,18 +30,13 @@ export class CollaborationHandler { updatePageContent: async ( documentName: string, payload: { - pageId: string; prosemirrorJson: any; - operation: string; + contentOperation: string; user: User; }, ) => { - const { pageId, prosemirrorJson, operation, user } = payload; - this.logger.debug( - 'Updating page content via yjs', - documentName, - payload, - ); + const { prosemirrorJson, contentOperation, user } = payload; + this.logger.debug('Updating page content via yjs', documentName); await this.withYdocConnection( hocuspocus, documentName, @@ -49,7 +44,7 @@ export class CollaborationHandler { (doc) => { const fragment = doc.getXmlFragment('default'); - if (operation === 'replace') { + if (contentOperation === 'replace') { if (fragment.length > 0) { fragment.delete(0, fragment.length); } @@ -63,7 +58,9 @@ export class CollaborationHandler { } else { const newContent = prosemirrorJson.content || []; const yElements = newContent.map(prosemirrorNodeToYElement); - fragment.insert(fragment.length, yElements); + const position = + contentOperation === 'prepend' ? 0 : fragment.length; + fragment.insert(position, yElements); } }, ); diff --git a/apps/server/src/collaboration/collaboration.module.ts b/apps/server/src/collaboration/collaboration.module.ts index 0724e9ed..c2dffd22 100644 --- a/apps/server/src/collaboration/collaboration.module.ts +++ b/apps/server/src/collaboration/collaboration.module.ts @@ -18,7 +18,6 @@ import { LoggerExtension } from './extensions/logger.extension'; import { CollaborationHandler } from './collaboration.handler'; import { CollabHistoryService } from './services/collab-history.service'; -@Global() @Module({ providers: [ CollaborationGateway, diff --git a/apps/server/src/collaboration/extensions/persistence.extension.ts b/apps/server/src/collaboration/extensions/persistence.extension.ts index 4548b40c..5a358d99 100644 --- a/apps/server/src/collaboration/extensions/persistence.extension.ts +++ b/apps/server/src/collaboration/extensions/persistence.extension.ts @@ -181,7 +181,8 @@ export class PersistenceExtension implements Extension { async onChange(data: onChangePayload) { const documentName = data.documentName; - const userId = data.context?.user.id; + const userId = data.context?.user?.id; + if (!userId) return; if (!this.contributors.has(documentName)) { diff --git a/apps/server/src/core/page/dto/create-page.dto.ts b/apps/server/src/core/page/dto/create-page.dto.ts index 4144f38b..5cf71e5a 100644 --- a/apps/server/src/core/page/dto/create-page.dto.ts +++ b/apps/server/src/core/page/dto/create-page.dto.ts @@ -7,7 +7,7 @@ import { } from 'class-validator'; import { Transform } from 'class-transformer'; -export type InputFormat = 'json' | 'markdown' | 'html'; +export type ContentFormat = 'json' | 'markdown' | 'html'; export class CreatePageDto { @IsOptional() @@ -29,7 +29,7 @@ export class CreatePageDto { content?: string | object; @ValidateIf((o) => o.content !== undefined) - @Transform(({ value }) => value?.toLowerCase()) + @Transform(({ value }) => value?.toLowerCase() ?? 'json') @IsIn(['json', 'markdown', 'html']) - input?: InputFormat; + format?: ContentFormat; } diff --git a/apps/server/src/core/page/dto/page.dto.ts b/apps/server/src/core/page/dto/page.dto.ts index d7116565..c53f8ade 100644 --- a/apps/server/src/core/page/dto/page.dto.ts +++ b/apps/server/src/core/page/dto/page.dto.ts @@ -8,7 +8,7 @@ import { } from 'class-validator'; import { Transform } from 'class-transformer'; -export type OutputFormat = 'json' | 'markdown' | 'html'; +import { ContentFormat } from './create-page.dto'; export class PageIdDto { @IsString() @@ -38,7 +38,7 @@ export class PageInfoDto extends PageIdDto { @IsOptional() @Transform(({ value }) => value?.toLowerCase()) @IsIn(['json', 'markdown', 'html']) - output?: OutputFormat; + format?: ContentFormat; } export class DeletePageDto extends PageIdDto { diff --git a/apps/server/src/core/page/dto/update-page.dto.ts b/apps/server/src/core/page/dto/update-page.dto.ts index 46104b7f..f79c55aa 100644 --- a/apps/server/src/core/page/dto/update-page.dto.ts +++ b/apps/server/src/core/page/dto/update-page.dto.ts @@ -1,9 +1,9 @@ import { PartialType } from '@nestjs/mapped-types'; -import { CreatePageDto, InputFormat } from './create-page.dto'; +import { CreatePageDto, ContentFormat } from './create-page.dto'; import { IsIn, IsOptional, IsString, ValidateIf } from 'class-validator'; import { Transform } from 'class-transformer'; -export type ContentOperation = 'append' | 'replace'; +export type ContentOperation = 'append' | 'prepend' | 'replace'; export class UpdatePageDto extends PartialType(CreatePageDto) { @IsString() @@ -14,11 +14,11 @@ export class UpdatePageDto extends PartialType(CreatePageDto) { @ValidateIf((o) => o.content !== undefined) @Transform(({ value }) => value?.toLowerCase()) - @IsIn(['append', 'replace']) - operation?: ContentOperation; + @IsIn(['append', 'prepend', 'replace']) + contentOperation?: ContentOperation; @ValidateIf((o) => o.content !== undefined) - @Transform(({ value }) => value?.toLowerCase()) + @Transform(({ value }) => value?.toLowerCase() ?? 'json') @IsIn(['json', 'markdown', 'html']) - input?: InputFormat; + format?: ContentFormat; } diff --git a/apps/server/src/core/page/page.controller.ts b/apps/server/src/core/page/page.controller.ts index 4396cdeb..e1227f28 100644 --- a/apps/server/src/core/page/page.controller.ts +++ b/apps/server/src/core/page/page.controller.ts @@ -70,9 +70,9 @@ export class PageController { throw new ForbiddenException(); } - if (dto.output && dto.output !== 'json' && page.content) { + if (dto.format && dto.format !== 'json' && page.content) { const contentOutput = - dto.output === 'markdown' + dto.format === 'markdown' ? jsonToMarkdown(page.content) : jsonToHtml(page.content); return { @@ -99,7 +99,25 @@ export class PageController { throw new ForbiddenException(); } - return this.pageService.create(user.id, workspace.id, createPageDto); + const page = await this.pageService.create( + user.id, + workspace.id, + createPageDto, + ); + + if ( + createPageDto.format && + createPageDto.format !== 'json' && + page.content + ) { + const contentOutput = + createPageDto.format === 'markdown' + ? jsonToMarkdown(page.content) + : jsonToHtml(page.content); + return { ...page, content: contentOutput }; + } + + return page; } @HttpCode(HttpStatus.OK) @@ -116,7 +134,25 @@ export class PageController { throw new ForbiddenException(); } - return this.pageService.update(page, updatePageDto, user); + const updatedPage = await this.pageService.update( + page, + updatePageDto, + user, + ); + + if ( + updatePageDto.format && + updatePageDto.format !== 'json' && + updatedPage.content + ) { + const contentOutput = + updatePageDto.format === 'markdown' + ? jsonToMarkdown(updatedPage.content) + : jsonToHtml(updatedPage.content); + return { ...updatedPage, content: contentOutput }; + } + + return updatedPage; } @HttpCode(HttpStatus.OK) diff --git a/apps/server/src/core/page/services/page.service.ts b/apps/server/src/core/page/services/page.service.ts index 19a0b087..1b559557 100644 --- a/apps/server/src/core/page/services/page.service.ts +++ b/apps/server/src/core/page/services/page.service.ts @@ -4,7 +4,7 @@ import { Logger, NotFoundException, } from '@nestjs/common'; -import { CreatePageDto, InputFormat } from '../dto/create-page.dto'; +import { CreatePageDto, ContentFormat } from '../dto/create-page.dto'; import { ContentOperation, UpdatePageDto } from '../dto/update-page.dto'; import { PageRepo } from '@docmost/db/repos/page/page.repo'; import { InsertablePage, Page, User } from '@docmost/db/types/entity.types'; @@ -99,10 +99,10 @@ export class PageService { let textContent = undefined; let ydoc = undefined; - if (createPageDto?.content && createPageDto?.input) { + if (createPageDto?.content && createPageDto?.format) { let prosemirrorJson: any; - switch (createPageDto.input) { + switch (createPageDto.format) { case 'markdown': { const html = await markdownToHtml(createPageDto.content as string); prosemirrorJson = htmlToJson(html as string); @@ -212,14 +212,14 @@ export class PageService { if ( updatePageDto.content && - updatePageDto.operation && - updatePageDto.input + updatePageDto.contentOperation && + updatePageDto.format ) { await this.updatePageContent( page.id, updatePageDto.content, - updatePageDto.operation, - updatePageDto.input, + updatePageDto.contentOperation, + updatePageDto.format, user, ); } @@ -236,13 +236,13 @@ export class PageService { async updatePageContent( pageId: string, content: string | object, - operation: ContentOperation, - input: InputFormat, + contentOperation: ContentOperation, + format: ContentFormat, user: User, ): Promise { let prosemirrorJson: any; - switch (input) { + switch (format) { case 'markdown': { const html = await markdownToHtml(content as string); prosemirrorJson = htmlToJson(html as string); @@ -262,14 +262,14 @@ export class PageService { try { jsonToNode(prosemirrorJson); } catch (err) { - throw new BadRequestException('Invalid content format'); + throw new BadRequestException('Invalid content'); } const documentName = `page.${pageId}`; await this.collaborationGateway.handleYjsEvent( 'updatePageContent', documentName, - { pageId, operation, prosemirrorJson, user }, + { contentOperation, prosemirrorJson, user }, ); } @@ -306,7 +306,11 @@ export class PageService { cursor: pagination.cursor, beforeCursor: pagination.beforeCursor, fields: [ - { expression: 'position', direction: 'asc', orderModifier: (ob) => ob.collate('C').asc() }, + { + expression: 'position', + direction: 'asc', + orderModifier: (ob) => ob.collate('C').asc(), + }, { expression: 'id', direction: 'asc' }, ], parseCursor: (cursor) => ({