mirror of
https://github.com/docmost/docmost.git
synced 2026-08-31 02:46:27 +08:00
feat: support creating page with content
This commit is contained in:
@@ -44,6 +44,7 @@ import { generateHTML, generateJSON } from '../common/helpers/prosemirror/html';
|
|||||||
// 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';
|
import * as Y from 'yjs';
|
||||||
|
import { turndown } from '../integrations/export/turndown-utils';
|
||||||
|
|
||||||
export const tiptapExtensions = [
|
export const tiptapExtensions = [
|
||||||
StarterKit.configure({
|
StarterKit.configure({
|
||||||
@@ -146,3 +147,8 @@ export function prosemirrorNodeToYElement(node: any): Y.XmlElement | Y.XmlText {
|
|||||||
}
|
}
|
||||||
return element;
|
return element;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function jsonToMarkdown(tiptapJson: any): string {
|
||||||
|
const html = jsonToHtml(tiptapJson);
|
||||||
|
return turndown(html);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,12 @@
|
|||||||
import { IsOptional, IsString, IsUUID } from 'class-validator';
|
import {
|
||||||
|
IsIn,
|
||||||
|
IsOptional,
|
||||||
|
IsString,
|
||||||
|
IsUUID,
|
||||||
|
ValidateIf,
|
||||||
|
} from 'class-validator';
|
||||||
|
|
||||||
|
export type InputFormat = 'json' | 'markdown' | 'html';
|
||||||
|
|
||||||
export class CreatePageDto {
|
export class CreatePageDto {
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@@ -15,4 +23,11 @@ export class CreatePageDto {
|
|||||||
|
|
||||||
@IsUUID()
|
@IsUUID()
|
||||||
spaceId: string;
|
spaceId: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
content?: string | object;
|
||||||
|
|
||||||
|
@ValidateIf((o) => o.content !== undefined)
|
||||||
|
@IsIn(['json', 'markdown', 'html'])
|
||||||
|
input?: InputFormat;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
IsBoolean,
|
IsBoolean,
|
||||||
|
IsIn,
|
||||||
IsNotEmpty,
|
IsNotEmpty,
|
||||||
IsOptional,
|
IsOptional,
|
||||||
IsString,
|
IsString,
|
||||||
@@ -22,14 +23,20 @@ export class PageHistoryIdDto {
|
|||||||
historyId: string;
|
historyId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type OutputFormat = 'json' | 'markdown' | 'html';
|
||||||
|
|
||||||
export class PageInfoDto extends PageIdDto {
|
export class PageInfoDto extends PageIdDto {
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
includeSpace: boolean;
|
includeSpace?: boolean;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
includeContent: boolean;
|
includeContent?: boolean;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsIn(['json', 'markdown', 'html'])
|
||||||
|
output?: OutputFormat;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DeletePageDto extends PageIdDto {
|
export class DeletePageDto extends PageIdDto {
|
||||||
|
|||||||
@@ -1,18 +1,21 @@
|
|||||||
import { PartialType } from '@nestjs/mapped-types';
|
import { PartialType } from '@nestjs/mapped-types';
|
||||||
import { CreatePageDto } from './create-page.dto';
|
import { CreatePageDto, InputFormat } from './create-page.dto';
|
||||||
import { IsIn, IsOptional, IsString, ValidateIf } from 'class-validator';
|
import { IsIn, IsOptional, IsString, ValidateIf } from 'class-validator';
|
||||||
|
|
||||||
export type ContentMode = 'append' | 'replace';
|
export type ContentOperation = 'append' | 'replace';
|
||||||
|
|
||||||
export class UpdatePageDto extends PartialType(CreatePageDto) {
|
export class UpdatePageDto extends PartialType(CreatePageDto) {
|
||||||
@IsString()
|
@IsString()
|
||||||
pageId: string;
|
pageId: string;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
content?: string | object;
|
||||||
content?: string;
|
|
||||||
|
|
||||||
@ValidateIf((o) => o.content !== undefined)
|
@ValidateIf((o) => o.content !== undefined)
|
||||||
@IsIn(['append', 'replace'])
|
@IsIn(['append', 'replace'])
|
||||||
contentMode?: ContentMode;
|
operation?: ContentOperation;
|
||||||
|
|
||||||
|
@ValidateIf((o) => o.content !== undefined)
|
||||||
|
@IsIn(['json', 'markdown', 'html'])
|
||||||
|
input?: InputFormat;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,10 @@ import { PageRepo } from '@docmost/db/repos/page/page.repo';
|
|||||||
import { RecentPageDto } from './dto/recent-page.dto';
|
import { RecentPageDto } from './dto/recent-page.dto';
|
||||||
import { DuplicatePageDto } from './dto/duplicate-page.dto';
|
import { DuplicatePageDto } from './dto/duplicate-page.dto';
|
||||||
import { DeletedPageDto } from './dto/deleted-page.dto';
|
import { DeletedPageDto } from './dto/deleted-page.dto';
|
||||||
|
import {
|
||||||
|
jsonToHtml,
|
||||||
|
jsonToMarkdown,
|
||||||
|
} from '../../collaboration/collaboration.util';
|
||||||
|
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@Controller('pages')
|
@Controller('pages')
|
||||||
@@ -66,6 +70,17 @@ export class PageController {
|
|||||||
throw new ForbiddenException();
|
throw new ForbiddenException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (dto.output && dto.output !== 'json' && page.content) {
|
||||||
|
const contentOutput =
|
||||||
|
dto.output === 'markdown'
|
||||||
|
? jsonToMarkdown(page.content)
|
||||||
|
: jsonToHtml(page.content);
|
||||||
|
return {
|
||||||
|
...page,
|
||||||
|
content: contentOutput,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return page;
|
return page;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import {
|
|||||||
Logger,
|
Logger,
|
||||||
NotFoundException,
|
NotFoundException,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { CreatePageDto } from '../dto/create-page.dto';
|
import { CreatePageDto, InputFormat } from '../dto/create-page.dto';
|
||||||
import { ContentMode, UpdatePageDto } from '../dto/update-page.dto';
|
import { ContentOperation, 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';
|
||||||
@@ -99,7 +99,42 @@ export class PageService {
|
|||||||
parentPageId = parentPage.id;
|
parentPageId = parentPage.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
const createdPage = await this.pageRepo.insertPage({
|
let content = undefined;
|
||||||
|
let textContent = undefined;
|
||||||
|
let ydoc = undefined;
|
||||||
|
|
||||||
|
if (createPageDto?.content && createPageDto?.input) {
|
||||||
|
let prosemirrorJson: any;
|
||||||
|
|
||||||
|
switch (createPageDto.input) {
|
||||||
|
case 'markdown': {
|
||||||
|
const html = await markdownToHtml(createPageDto.content as string);
|
||||||
|
prosemirrorJson = htmlToJson(html as string);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'html': {
|
||||||
|
prosemirrorJson = htmlToJson(createPageDto.content as string);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'json':
|
||||||
|
default: {
|
||||||
|
prosemirrorJson = createPageDto.content;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
jsonToNode(prosemirrorJson);
|
||||||
|
} catch (err) {
|
||||||
|
throw new BadRequestException('Invalid content format');
|
||||||
|
}
|
||||||
|
|
||||||
|
content = prosemirrorJson;
|
||||||
|
textContent = jsonToText(prosemirrorJson);
|
||||||
|
ydoc = createYdocFromJson(prosemirrorJson);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.pageRepo.insertPage({
|
||||||
slugId: generateSlugId(),
|
slugId: generateSlugId(),
|
||||||
title: createPageDto.title,
|
title: createPageDto.title,
|
||||||
position: await this.nextPagePosition(
|
position: await this.nextPagePosition(
|
||||||
@@ -112,9 +147,10 @@ export class PageService {
|
|||||||
creatorId: userId,
|
creatorId: userId,
|
||||||
workspaceId: workspaceId,
|
workspaceId: workspaceId,
|
||||||
lastUpdatedById: userId,
|
lastUpdatedById: userId,
|
||||||
|
content,
|
||||||
|
textContent,
|
||||||
|
ydoc,
|
||||||
});
|
});
|
||||||
|
|
||||||
return createdPage;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async nextPagePosition(spaceId: string, parentPageId?: string) {
|
async nextPagePosition(spaceId: string, parentPageId?: string) {
|
||||||
@@ -178,11 +214,16 @@ export class PageService {
|
|||||||
page.id,
|
page.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (updatePageDto.content && updatePageDto.contentMode) {
|
if (
|
||||||
|
updatePageDto.content &&
|
||||||
|
updatePageDto.operation &&
|
||||||
|
updatePageDto.input
|
||||||
|
) {
|
||||||
await this.updatePageContent(
|
await this.updatePageContent(
|
||||||
page.id,
|
page.id,
|
||||||
updatePageDto.content,
|
updatePageDto.content,
|
||||||
updatePageDto.contentMode,
|
updatePageDto.operation,
|
||||||
|
updatePageDto.input,
|
||||||
userId,
|
userId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -198,12 +239,35 @@ export class PageService {
|
|||||||
|
|
||||||
async updatePageContent(
|
async updatePageContent(
|
||||||
pageId: string,
|
pageId: string,
|
||||||
markdown: string,
|
content: string | object,
|
||||||
mode: ContentMode,
|
operation: ContentOperation,
|
||||||
|
input: InputFormat,
|
||||||
userId: string,
|
userId: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const html = await markdownToHtml(markdown);
|
let prosemirrorJson: any;
|
||||||
const prosemirrorJson = htmlToJson(html as string);
|
|
||||||
|
switch (input) {
|
||||||
|
case 'markdown': {
|
||||||
|
const html = await markdownToHtml(content as string);
|
||||||
|
prosemirrorJson = htmlToJson(html as string);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'html': {
|
||||||
|
prosemirrorJson = htmlToJson(content as string);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'json':
|
||||||
|
default: {
|
||||||
|
prosemirrorJson = content;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
jsonToNode(prosemirrorJson);
|
||||||
|
} catch (err) {
|
||||||
|
throw new BadRequestException('Invalid content format');
|
||||||
|
}
|
||||||
|
|
||||||
const documentName = `page.${pageId}`;
|
const documentName = `page.${pageId}`;
|
||||||
const connection = await this.collaborationGateway.openDirectConnection(
|
const connection = await this.collaborationGateway.openDirectConnection(
|
||||||
@@ -215,7 +279,7 @@ export class PageService {
|
|||||||
await connection.transact((doc) => {
|
await connection.transact((doc) => {
|
||||||
const fragment = doc.getXmlFragment('default');
|
const fragment = doc.getXmlFragment('default');
|
||||||
|
|
||||||
if (mode === 'replace') {
|
if (operation === 'replace') {
|
||||||
while (fragment.length > 0) {
|
while (fragment.length > 0) {
|
||||||
fragment.delete(0, 1);
|
fragment.delete(0, 1);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user