From 2721ab6a29e6e1cef801d54773d412f43eaa9e02 Mon Sep 17 00:00:00 2001 From: ja49619 <107350002+ja49619@users.noreply.github.com> Date: Wed, 9 Oct 2024 21:52:20 +0400 Subject: [PATCH 01/71] Add alignment styles for task list items (#378) --- apps/client/src/features/editor/styles/task-list.css | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/client/src/features/editor/styles/task-list.css b/apps/client/src/features/editor/styles/task-list.css index a2c5f6f..cb2720c 100644 --- a/apps/client/src/features/editor/styles/task-list.css +++ b/apps/client/src/features/editor/styles/task-list.css @@ -13,6 +13,8 @@ ul[data-type="taskList"] { flex: 0 0 auto; margin-right: 0.5rem; user-select: none; + display: flex; + align-items: center; } > div { From 7ec6a3651550bd766f3f2d2539fe7248f657c52a Mon Sep 17 00:00:00 2001 From: servostar <72654954+Servostar@users.noreply.github.com> Date: Thu, 10 Oct 2024 16:35:20 +0200 Subject: [PATCH 02/71] fix: removed font overwrite for KaTeX elements (#377) --- .../features/editor/components/math/math.module.css | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/apps/client/src/features/editor/components/math/math.module.css b/apps/client/src/features/editor/components/math/math.module.css index 9edb887..0bd15f7 100644 --- a/apps/client/src/features/editor/components/math/math.module.css +++ b/apps/client/src/features/editor/components/math/math.module.css @@ -17,10 +17,6 @@ color: light-dark(var(--mantine-color-red-8), var(--mantine-color-red-7)); background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-gray-8)); } - - &:not(.error, .empty) * { - font-family: KaTeX_Main, Times New Roman, serif; - } } .mathBlock { @@ -52,10 +48,4 @@ color: light-dark(var(--mantine-color-red-8), var(--mantine-color-red-7)); background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-gray-8)); } - - &:not(.error, .empty) * { - font-family: KaTeX_Main, Times New Roman, serif; - } } - - From e333eee08b765dad458e938f39ec501e2ed5dab7 Mon Sep 17 00:00:00 2001 From: servostar <72654954+Servostar@users.noreply.github.com> Date: Thu, 10 Oct 2024 16:39:04 +0200 Subject: [PATCH 03/71] fix: base64 encoded drawio image decoded to Latin-1 instead of UTF-8 (#369) --- apps/client/src/lib/utils.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/apps/client/src/lib/utils.ts b/apps/client/src/lib/utils.ts index e935701..9ce7d35 100644 --- a/apps/client/src/lib/utils.ts +++ b/apps/client/src/lib/utils.ts @@ -53,11 +53,21 @@ export async function svgStringToFile( return new File([blob], fileName, { type: "image/svg+xml" }); } +// Convert a string holding Base64 encoded UTF-8 data into a proper UTF-8 encoded string +// as a replacement for `atob`. +// based on: https://developer.mozilla.org/en-US/docs/Glossary/Base64 +function decodeBase64(base64: string): string { + // convert string to bytes + const bytes = Uint8Array.from(atob(base64), (m) => m.codePointAt(0)); + // properly decode bytes to UTF-8 encoded string + return new TextDecoder().decode(bytes); +} + export function decodeBase64ToSvgString(base64Data: string): string { const base64Prefix = 'data:image/svg+xml;base64,'; if (base64Data.startsWith(base64Prefix)) { base64Data = base64Data.replace(base64Prefix, ''); } - return atob(base64Data); + return decodeBase64(base64Data); } From 384f11f2b79a98993cb8bfc8d18405247874b1b6 Mon Sep 17 00:00:00 2001 From: Philip Okugbe Date: Thu, 10 Oct 2024 21:28:28 +0100 Subject: [PATCH 04/71] make file upload size limit configurable (#386) --- .env.example | 3 + .../attachment/upload-attachment-action.tsx | 6 +- .../components/image/upload-image-action.tsx | 6 +- .../components/video/upload-video-action.tsx | 7 +- apps/client/src/lib/config.ts | 57 +- apps/client/vite.config.ts | 3 +- apps/server/package.json | 2 - .../core/attachment/attachment.constants.ts | 1 - .../core/attachment/attachment.controller.ts | 560 +++++++++--------- .../environment/environment.service.ts | 5 + .../integrations/import/import.controller.ts | 5 +- .../src/integrations/static/static.module.ts | 1 + package.json | 2 + pnpm-lock.yaml | 170 +++--- 14 files changed, 428 insertions(+), 400 deletions(-) diff --git a/.env.example b/.env.example index 5bc884c..3c356d9 100644 --- a/.env.example +++ b/.env.example @@ -21,6 +21,9 @@ AWS_S3_BUCKET= AWS_S3_ENDPOINT= AWS_S3_FORCE_PATH_STYLE= +# default: 50mb +FILE_UPLOAD_SIZE_LIMIT= + # options: smtp | postmark MAIL_DRIVER=smtp MAIL_FROM_ADDRESS=hello@example.com diff --git a/apps/client/src/features/editor/components/attachment/upload-attachment-action.tsx b/apps/client/src/features/editor/components/attachment/upload-attachment-action.tsx index 921d30f..2386361 100644 --- a/apps/client/src/features/editor/components/attachment/upload-attachment-action.tsx +++ b/apps/client/src/features/editor/components/attachment/upload-attachment-action.tsx @@ -1,6 +1,8 @@ import { handleAttachmentUpload } from "@docmost/editor-ext"; import { uploadFile } from "@/features/page/services/page-service.ts"; import { notifications } from "@mantine/notifications"; +import {getFileUploadSizeLimit} from "@/lib/config.ts"; +import {formatBytes} from "@/lib"; export const uploadAttachmentAction = handleAttachmentUpload({ onUpload: async (file: File, pageId: string): Promise => { @@ -18,10 +20,10 @@ export const uploadAttachmentAction = handleAttachmentUpload({ if (file.type.includes("image/") || file.type.includes("video/")) { return false; } - if (file.size / 1024 / 1024 > 50) { + if (file.size > getFileUploadSizeLimit()) { notifications.show({ color: "red", - message: `File exceeds the 50 MB attachment limit`, + message: `File exceeds the ${formatBytes(getFileUploadSizeLimit())} attachment limit`, }); return false; } diff --git a/apps/client/src/features/editor/components/image/upload-image-action.tsx b/apps/client/src/features/editor/components/image/upload-image-action.tsx index ea7f621..6c6e8db 100644 --- a/apps/client/src/features/editor/components/image/upload-image-action.tsx +++ b/apps/client/src/features/editor/components/image/upload-image-action.tsx @@ -1,6 +1,8 @@ import { handleImageUpload } from "@docmost/editor-ext"; import { uploadFile } from "@/features/page/services/page-service.ts"; import { notifications } from "@mantine/notifications"; +import {getFileUploadSizeLimit} from "@/lib/config.ts"; +import { formatBytes } from "@/lib"; export const uploadImageAction = handleImageUpload({ onUpload: async (file: File, pageId: string): Promise => { @@ -18,10 +20,10 @@ export const uploadImageAction = handleImageUpload({ if (!file.type.includes("image/")) { return false; } - if (file.size / 1024 / 1024 > 50) { + if (file.size > getFileUploadSizeLimit()) { notifications.show({ color: "red", - message: `File exceeds the 50 MB attachment limit`, + message: `File exceeds the ${formatBytes(getFileUploadSizeLimit())} attachment limit`, }); return false; } diff --git a/apps/client/src/features/editor/components/video/upload-video-action.tsx b/apps/client/src/features/editor/components/video/upload-video-action.tsx index 3a59ad1..4173207 100644 --- a/apps/client/src/features/editor/components/video/upload-video-action.tsx +++ b/apps/client/src/features/editor/components/video/upload-video-action.tsx @@ -1,6 +1,8 @@ import { handleVideoUpload } from "@docmost/editor-ext"; import { uploadFile } from "@/features/page/services/page-service.ts"; import { notifications } from "@mantine/notifications"; +import {getFileUploadSizeLimit} from "@/lib/config.ts"; +import {formatBytes} from "@/lib"; export const uploadVideoAction = handleVideoUpload({ onUpload: async (file: File, pageId: string): Promise => { @@ -19,13 +21,14 @@ export const uploadVideoAction = handleVideoUpload({ return false; } - if (file.size / 1024 / 1024 > 50) { + if (file.size > getFileUploadSizeLimit()) { notifications.show({ color: "red", - message: `File exceeds the 50 MB attachment limit`, + message: `File exceeds the ${formatBytes(getFileUploadSizeLimit())} attachment limit`, }); return false; } return true; }, }); + diff --git a/apps/client/src/lib/config.ts b/apps/client/src/lib/config.ts index add2bf7..64c7c4e 100644 --- a/apps/client/src/lib/config.ts +++ b/apps/client/src/lib/config.ts @@ -1,51 +1,58 @@ +import bytes from "bytes"; + declare global { - interface Window { - CONFIG?: Record; - } + interface Window { + CONFIG?: Record; + } } export function getAppUrl(): string { - //let appUrl = window.CONFIG?.APP_URL || process.env.APP_URL; + //let appUrl = window.CONFIG?.APP_URL || process.env.APP_URL; - // if (import.meta.env.DEV) { - // return appUrl || "http://localhost:3000"; - //} + // if (import.meta.env.DEV) { + // return appUrl || "http://localhost:3000"; + //} - return `${window.location.protocol}//${window.location.host}`; + return `${window.location.protocol}//${window.location.host}`; } export function getBackendUrl(): string { - return getAppUrl() + '/api'; + return getAppUrl() + '/api'; } export function getCollaborationUrl(): string { - const COLLAB_PATH = '/collab'; + const COLLAB_PATH = '/collab'; - let url = getAppUrl(); - if (import.meta.env.DEV) { - url = process.env.APP_URL; - } + let url = getAppUrl(); + if (import.meta.env.DEV) { + url = process.env.APP_URL; + } - const wsProtocol = url.startsWith('https') ? 'wss' : 'ws'; - return `${wsProtocol}://${url.split('://')[1]}${COLLAB_PATH}`; + const wsProtocol = url.startsWith('https') ? 'wss' : 'ws'; + return `${wsProtocol}://${url.split('://')[1]}${COLLAB_PATH}`; } export function getAvatarUrl(avatarUrl: string) { - if (!avatarUrl) { - return null; - } + if (!avatarUrl) { + return null; + } - if (avatarUrl?.startsWith('http')) { - return avatarUrl; - } + if (avatarUrl?.startsWith('http')) { + return avatarUrl; + } - return getBackendUrl() + '/attachments/img/avatar/' + avatarUrl; + return getBackendUrl() + '/attachments/img/avatar/' + avatarUrl; } export function getSpaceUrl(spaceSlug: string) { - return '/s/' + spaceSlug; + return '/s/' + spaceSlug; } export function getFileUrl(src: string) { - return src?.startsWith('/files/') ? getBackendUrl() + src : src; + return src?.startsWith('/files/') ? getBackendUrl() + src : src; } + +export function getFileUploadSizeLimit() { + const limit = window.CONFIG?.FILE_UPLOAD_SIZE_LIMIT || process?.env.FILE_UPLOAD_SIZE_LIMIT || '50mb'; + return bytes(limit); +} \ No newline at end of file diff --git a/apps/client/vite.config.ts b/apps/client/vite.config.ts index 9da366e..4ffa6b5 100644 --- a/apps/client/vite.config.ts +++ b/apps/client/vite.config.ts @@ -5,12 +5,13 @@ import * as path from "path"; export const envPath = path.resolve(process.cwd(), "..", ".."); export default defineConfig(({ mode }) => { - const { APP_URL } = loadEnv(mode, envPath, ""); + const { APP_URL, FILE_UPLOAD_SIZE_LIMIT } = loadEnv(mode, envPath, ""); return { define: { "process.env": { APP_URL, + FILE_UPLOAD_SIZE_LIMIT }, 'APP_VERSION': JSON.stringify(process.env.npm_package_version), }, diff --git a/apps/server/package.json b/apps/server/package.json index 61baefb..f1ca9ee 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -51,7 +51,6 @@ "@socket.io/redis-adapter": "^8.3.0", "bcrypt": "^5.1.1", "bullmq": "^5.12.12", - "bytes": "^3.1.2", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", "fix-esm": "^1.0.1", @@ -81,7 +80,6 @@ "@nestjs/schematics": "^10.1.4", "@nestjs/testing": "^10.4.1", "@types/bcrypt": "^5.0.2", - "@types/bytes": "^3.1.4", "@types/debounce": "^1.2.4", "@types/fs-extra": "^11.0.4", "@types/jest": "^29.5.12", diff --git a/apps/server/src/core/attachment/attachment.constants.ts b/apps/server/src/core/attachment/attachment.constants.ts index 73c2c30..2710d88 100644 --- a/apps/server/src/core/attachment/attachment.constants.ts +++ b/apps/server/src/core/attachment/attachment.constants.ts @@ -16,4 +16,3 @@ export const inlineFileExtensions = [ '.mp4', '.mov', ]; -export const MAX_FILE_SIZE = '50MB'; diff --git a/apps/server/src/core/attachment/attachment.controller.ts b/apps/server/src/core/attachment/attachment.controller.ts index 7777378..0227f59 100644 --- a/apps/server/src/core/attachment/attachment.controller.ts +++ b/apps/server/src/core/attachment/attachment.controller.ts @@ -1,308 +1,310 @@ import { - BadRequestException, - Controller, - ForbiddenException, - Get, - HttpCode, - HttpStatus, - Logger, - NotFoundException, - Param, - Post, - Req, - Res, - UseGuards, - UseInterceptors, + BadRequestException, + Controller, + ForbiddenException, + Get, + HttpCode, + HttpStatus, + Logger, + NotFoundException, + Param, + Post, + Req, + Res, + UseGuards, + UseInterceptors, } from '@nestjs/common'; -import { AttachmentService } from './services/attachment.service'; -import { FastifyReply } from 'fastify'; -import { FileInterceptor } from '../../common/interceptors/file.interceptor'; +import {AttachmentService} from './services/attachment.service'; +import {FastifyReply} from 'fastify'; +import {FileInterceptor} from '../../common/interceptors/file.interceptor'; import * as bytes from 'bytes'; -import { AuthUser } from '../../common/decorators/auth-user.decorator'; -import { AuthWorkspace } from '../../common/decorators/auth-workspace.decorator'; -import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard'; -import { User, Workspace } from '@docmost/db/types/entity.types'; -import { StorageService } from '../../integrations/storage/storage.service'; +import {AuthUser} from '../../common/decorators/auth-user.decorator'; +import {AuthWorkspace} from '../../common/decorators/auth-workspace.decorator'; +import {JwtAuthGuard} from '../../common/guards/jwt-auth.guard'; +import {User, Workspace} from '@docmost/db/types/entity.types'; +import {StorageService} from '../../integrations/storage/storage.service'; import { - getAttachmentFolderPath, - validAttachmentTypes, + getAttachmentFolderPath, + validAttachmentTypes, } from './attachment.utils'; -import { getMimeType } from '../../common/helpers'; +import {getMimeType} from '../../common/helpers'; import { - AttachmentType, - inlineFileExtensions, - MAX_AVATAR_SIZE, - MAX_FILE_SIZE, + AttachmentType, + inlineFileExtensions, + MAX_AVATAR_SIZE, } from './attachment.constants'; import { - SpaceCaslAction, - SpaceCaslSubject, + SpaceCaslAction, + SpaceCaslSubject, } from '../casl/interfaces/space-ability.type'; import SpaceAbilityFactory from '../casl/abilities/space-ability.factory'; import { - WorkspaceCaslAction, - WorkspaceCaslSubject, + WorkspaceCaslAction, + WorkspaceCaslSubject, } from '../casl/interfaces/workspace-ability.type'; import WorkspaceAbilityFactory from '../casl/abilities/workspace-ability.factory'; -import { PageRepo } from '@docmost/db/repos/page/page.repo'; -import { AttachmentRepo } from '@docmost/db/repos/attachment/attachment.repo'; -import { validate as isValidUUID } from 'uuid'; +import {PageRepo} from '@docmost/db/repos/page/page.repo'; +import {AttachmentRepo} from '@docmost/db/repos/attachment/attachment.repo'; +import {validate as isValidUUID} from 'uuid'; +import {EnvironmentService} from "../../integrations/environment/environment.service"; @Controller() export class AttachmentController { - private readonly logger = new Logger(AttachmentController.name); + private readonly logger = new Logger(AttachmentController.name); - constructor( - private readonly attachmentService: AttachmentService, - private readonly storageService: StorageService, - private readonly workspaceAbility: WorkspaceAbilityFactory, - private readonly spaceAbility: SpaceAbilityFactory, - private readonly pageRepo: PageRepo, - private readonly attachmentRepo: AttachmentRepo, - ) {} - - @UseGuards(JwtAuthGuard) - @HttpCode(HttpStatus.OK) - @Post('files/upload') - @UseInterceptors(FileInterceptor) - async uploadFile( - @Req() req: any, - @Res() res: FastifyReply, - @AuthUser() user: User, - @AuthWorkspace() workspace: Workspace, - ) { - const maxFileSize = bytes(MAX_FILE_SIZE); - - let file = null; - try { - file = await req.file({ - limits: { fileSize: maxFileSize, fields: 3, files: 1 }, - }); - } catch (err: any) { - this.logger.error(err.message); - if (err?.statusCode === 413) { - throw new BadRequestException( - `File too large. Exceeds the ${MAX_FILE_SIZE} limit`, - ); - } - } - - if (!file) { - throw new BadRequestException('Failed to upload file'); - } - - const pageId = file.fields?.pageId?.value; - - if (!pageId) { - throw new BadRequestException('PageId is required'); - } - - const page = await this.pageRepo.findById(pageId); - - if (!page) { - throw new NotFoundException('Page not found'); - } - - const spaceAbility = await this.spaceAbility.createForUser( - user, - page.spaceId, - ); - if (spaceAbility.cannot(SpaceCaslAction.Manage, SpaceCaslSubject.Page)) { - throw new ForbiddenException(); - } - - const spaceId = page.spaceId; - - const attachmentId = file.fields?.attachmentId?.value; - if (attachmentId && !isValidUUID(attachmentId)) { - throw new BadRequestException('Invalid attachment id'); - } - - try { - const fileResponse = await this.attachmentService.uploadFile({ - filePromise: file, - pageId: pageId, - spaceId: spaceId, - userId: user.id, - workspaceId: workspace.id, - attachmentId: attachmentId, - }); - - return res.send(fileResponse); - } catch (err: any) { - if (err?.statusCode === 413) { - const errMessage = `File too large. Exceeds the ${MAX_FILE_SIZE} limit`; - this.logger.error(errMessage); - throw new BadRequestException(errMessage); - } - this.logger.error(err); - throw new BadRequestException('Error processing file upload.'); - } - } - - @UseGuards(JwtAuthGuard) - @Get('/files/:fileId/:fileName') - async getFile( - @Res() res: FastifyReply, - @AuthUser() user: User, - @AuthWorkspace() workspace: Workspace, - @Param('fileId') fileId: string, - @Param('fileName') fileName?: string, - ) { - if (!isValidUUID(fileId)) { - throw new NotFoundException('Invalid file id'); - } - - const attachment = await this.attachmentRepo.findById(fileId); - if ( - !attachment || - attachment.workspaceId !== workspace.id || - !attachment.pageId || - !attachment.spaceId + constructor( + private readonly attachmentService: AttachmentService, + private readonly storageService: StorageService, + private readonly workspaceAbility: WorkspaceAbilityFactory, + private readonly spaceAbility: SpaceAbilityFactory, + private readonly pageRepo: PageRepo, + private readonly attachmentRepo: AttachmentRepo, + private readonly environmentService: EnvironmentService, ) { - throw new NotFoundException(); } - const spaceAbility = await this.spaceAbility.createForUser( - user, - attachment.spaceId, - ); - - if (spaceAbility.cannot(SpaceCaslAction.Read, SpaceCaslSubject.Page)) { - throw new ForbiddenException(); - } - - try { - const fileStream = await this.storageService.read(attachment.filePath); - res.headers({ - 'Content-Type': attachment.mimeType, - 'Cache-Control': 'public, max-age=3600', - }); - - if (!inlineFileExtensions.includes(attachment.fileExt)) { - res.header( - 'Content-Disposition', - `attachment; filename="${encodeURIComponent(attachment.fileName)}"`, - ); - } - - return res.send(fileStream); - } catch (err) { - this.logger.error(err); - throw new NotFoundException('File not found'); - } - } - - @UseGuards(JwtAuthGuard) - @HttpCode(HttpStatus.OK) - @Post('attachments/upload-image') - @UseInterceptors(FileInterceptor) - async uploadAvatarOrLogo( - @Req() req: any, - @Res() res: FastifyReply, - @AuthUser() user: User, - @AuthWorkspace() workspace: Workspace, - ) { - const maxFileSize = bytes(MAX_AVATAR_SIZE); - - let file = null; - try { - file = await req.file({ - limits: { fileSize: maxFileSize, fields: 3, files: 1 }, - }); - } catch (err: any) { - if (err?.statusCode === 413) { - throw new BadRequestException( - `File too large. Exceeds the ${MAX_AVATAR_SIZE} limit`, - ); - } - } - - if (!file) { - throw new BadRequestException('Invalid file upload'); - } - - const attachmentType = file.fields?.type?.value; - const spaceId = file.fields?.spaceId?.value; - - if (!attachmentType) { - throw new BadRequestException('attachment type is required'); - } - - if ( - !validAttachmentTypes.includes(attachmentType) || - attachmentType === AttachmentType.File + @UseGuards(JwtAuthGuard) + @HttpCode(HttpStatus.OK) + @Post('files/upload') + @UseInterceptors(FileInterceptor) + async uploadFile( + @Req() req: any, + @Res() res: FastifyReply, + @AuthUser() user: User, + @AuthWorkspace() workspace: Workspace, ) { - throw new BadRequestException('Invalid image attachment type'); + const maxFileSize = bytes(this.environmentService.getFileUploadSizeLimit()); + + let file = null; + try { + file = await req.file({ + limits: {fileSize: maxFileSize, fields: 3, files: 1}, + }); + } catch (err: any) { + this.logger.error(err.message); + if (err?.statusCode === 413) { + throw new BadRequestException( + `File too large. Exceeds the ${this.environmentService.getFileUploadSizeLimit()} limit`, + ); + } + } + + if (!file) { + throw new BadRequestException('Failed to upload file'); + } + + const pageId = file.fields?.pageId?.value; + + if (!pageId) { + throw new BadRequestException('PageId is required'); + } + + const page = await this.pageRepo.findById(pageId); + + if (!page) { + throw new NotFoundException('Page not found'); + } + + const spaceAbility = await this.spaceAbility.createForUser( + user, + page.spaceId, + ); + if (spaceAbility.cannot(SpaceCaslAction.Manage, SpaceCaslSubject.Page)) { + throw new ForbiddenException(); + } + + const spaceId = page.spaceId; + + const attachmentId = file.fields?.attachmentId?.value; + if (attachmentId && !isValidUUID(attachmentId)) { + throw new BadRequestException('Invalid attachment id'); + } + + try { + const fileResponse = await this.attachmentService.uploadFile({ + filePromise: file, + pageId: pageId, + spaceId: spaceId, + userId: user.id, + workspaceId: workspace.id, + attachmentId: attachmentId, + }); + + return res.send(fileResponse); + } catch (err: any) { + if (err?.statusCode === 413) { + const errMessage = `File too large. Exceeds the ${this.environmentService.getFileUploadSizeLimit()} limit`; + this.logger.error(errMessage); + throw new BadRequestException(errMessage); + } + this.logger.error(err); + throw new BadRequestException('Error processing file upload.'); + } } - if (attachmentType === AttachmentType.WorkspaceLogo) { - const ability = this.workspaceAbility.createForUser(user, workspace); - if ( - ability.cannot( - WorkspaceCaslAction.Manage, - WorkspaceCaslSubject.Settings, - ) - ) { - throw new ForbiddenException(); - } - } - - if (attachmentType === AttachmentType.SpaceLogo) { - if (!spaceId) { - throw new BadRequestException('spaceId is required'); - } - - const spaceAbility = await this.spaceAbility.createForUser(user, spaceId); - if ( - spaceAbility.cannot(SpaceCaslAction.Manage, SpaceCaslSubject.Settings) - ) { - throw new ForbiddenException(); - } - } - - try { - const fileResponse = await this.attachmentService.uploadImage( - file, - attachmentType, - user.id, - workspace.id, - spaceId, - ); - - return res.send(fileResponse); - } catch (err: any) { - this.logger.error(err); - throw new BadRequestException('Error processing file upload.'); - } - } - - @Get('attachments/img/:attachmentType/:fileName') - async getLogoOrAvatar( - @Res() res: FastifyReply, - @AuthWorkspace() workspace: Workspace, - @Param('attachmentType') attachmentType: AttachmentType, - @Param('fileName') fileName?: string, - ) { - if ( - !validAttachmentTypes.includes(attachmentType) || - attachmentType === AttachmentType.File + @UseGuards(JwtAuthGuard) + @Get('/files/:fileId/:fileName') + async getFile( + @Res() res: FastifyReply, + @AuthUser() user: User, + @AuthWorkspace() workspace: Workspace, + @Param('fileId') fileId: string, + @Param('fileName') fileName?: string, ) { - throw new BadRequestException('Invalid image attachment type'); + if (!isValidUUID(fileId)) { + throw new NotFoundException('Invalid file id'); + } + + const attachment = await this.attachmentRepo.findById(fileId); + if ( + !attachment || + attachment.workspaceId !== workspace.id || + !attachment.pageId || + !attachment.spaceId + ) { + throw new NotFoundException(); + } + + const spaceAbility = await this.spaceAbility.createForUser( + user, + attachment.spaceId, + ); + + if (spaceAbility.cannot(SpaceCaslAction.Read, SpaceCaslSubject.Page)) { + throw new ForbiddenException(); + } + + try { + const fileStream = await this.storageService.read(attachment.filePath); + res.headers({ + 'Content-Type': attachment.mimeType, + 'Cache-Control': 'public, max-age=3600', + }); + + if (!inlineFileExtensions.includes(attachment.fileExt)) { + res.header( + 'Content-Disposition', + `attachment; filename="${encodeURIComponent(attachment.fileName)}"`, + ); + } + + return res.send(fileStream); + } catch (err) { + this.logger.error(err); + throw new NotFoundException('File not found'); + } } - const filePath = `${getAttachmentFolderPath(attachmentType, workspace.id)}/${fileName}`; + @UseGuards(JwtAuthGuard) + @HttpCode(HttpStatus.OK) + @Post('attachments/upload-image') + @UseInterceptors(FileInterceptor) + async uploadAvatarOrLogo( + @Req() req: any, + @Res() res: FastifyReply, + @AuthUser() user: User, + @AuthWorkspace() workspace: Workspace, + ) { + const maxFileSize = bytes(MAX_AVATAR_SIZE); - try { - const fileStream = await this.storageService.read(filePath); - res.headers({ - 'Content-Type': getMimeType(filePath), - 'Cache-Control': 'public, max-age=86400', - }); - return res.send(fileStream); - } catch (err) { - this.logger.error(err); - throw new NotFoundException('File not found'); + let file = null; + try { + file = await req.file({ + limits: {fileSize: maxFileSize, fields: 3, files: 1}, + }); + } catch (err: any) { + if (err?.statusCode === 413) { + throw new BadRequestException( + `File too large. Exceeds the ${MAX_AVATAR_SIZE} limit`, + ); + } + } + + if (!file) { + throw new BadRequestException('Invalid file upload'); + } + + const attachmentType = file.fields?.type?.value; + const spaceId = file.fields?.spaceId?.value; + + if (!attachmentType) { + throw new BadRequestException('attachment type is required'); + } + + if ( + !validAttachmentTypes.includes(attachmentType) || + attachmentType === AttachmentType.File + ) { + throw new BadRequestException('Invalid image attachment type'); + } + + if (attachmentType === AttachmentType.WorkspaceLogo) { + const ability = this.workspaceAbility.createForUser(user, workspace); + if ( + ability.cannot( + WorkspaceCaslAction.Manage, + WorkspaceCaslSubject.Settings, + ) + ) { + throw new ForbiddenException(); + } + } + + if (attachmentType === AttachmentType.SpaceLogo) { + if (!spaceId) { + throw new BadRequestException('spaceId is required'); + } + + const spaceAbility = await this.spaceAbility.createForUser(user, spaceId); + if ( + spaceAbility.cannot(SpaceCaslAction.Manage, SpaceCaslSubject.Settings) + ) { + throw new ForbiddenException(); + } + } + + try { + const fileResponse = await this.attachmentService.uploadImage( + file, + attachmentType, + user.id, + workspace.id, + spaceId, + ); + + return res.send(fileResponse); + } catch (err: any) { + this.logger.error(err); + throw new BadRequestException('Error processing file upload.'); + } + } + + @Get('attachments/img/:attachmentType/:fileName') + async getLogoOrAvatar( + @Res() res: FastifyReply, + @AuthWorkspace() workspace: Workspace, + @Param('attachmentType') attachmentType: AttachmentType, + @Param('fileName') fileName?: string, + ) { + if ( + !validAttachmentTypes.includes(attachmentType) || + attachmentType === AttachmentType.File + ) { + throw new BadRequestException('Invalid image attachment type'); + } + + const filePath = `${getAttachmentFolderPath(attachmentType, workspace.id)}/${fileName}`; + + try { + const fileStream = await this.storageService.read(filePath); + res.headers({ + 'Content-Type': getMimeType(filePath), + 'Cache-Control': 'public, max-age=86400', + }); + return res.send(fileStream); + } catch (err) { + this.logger.error(err); + throw new NotFoundException('File not found'); + } } - } } diff --git a/apps/server/src/integrations/environment/environment.service.ts b/apps/server/src/integrations/environment/environment.service.ts index 3385d0e..3daa822 100644 --- a/apps/server/src/integrations/environment/environment.service.ts +++ b/apps/server/src/integrations/environment/environment.service.ts @@ -43,6 +43,11 @@ export class EnvironmentService { return this.configService.get('STORAGE_DRIVER', 'local'); } + getFileUploadSizeLimit(): string { + + return this.configService.get('FILE_UPLOAD_SIZE_LIMIT', '50mb'); + } + getAwsS3AccessKeyId(): string { return this.configService.get('AWS_S3_ACCESS_KEY_ID'); } diff --git a/apps/server/src/integrations/import/import.controller.ts b/apps/server/src/integrations/import/import.controller.ts index 902376d..975301a 100644 --- a/apps/server/src/integrations/import/import.controller.ts +++ b/apps/server/src/integrations/import/import.controller.ts @@ -21,7 +21,6 @@ import { import { FileInterceptor } from '../../common/interceptors/file.interceptor'; import * as bytes from 'bytes'; import * as path from 'path'; -import { MAX_FILE_SIZE } from '../../core/attachment/attachment.constants'; import { ImportService } from './import.service'; import { AuthWorkspace } from '../../common/decorators/auth-workspace.decorator'; @@ -45,7 +44,7 @@ export class ImportController { ) { const validFileExtensions = ['.md', '.html']; - const maxFileSize = bytes(MAX_FILE_SIZE); + const maxFileSize = bytes('100mb'); let file = null; try { @@ -56,7 +55,7 @@ export class ImportController { this.logger.error(err.message); if (err?.statusCode === 413) { throw new BadRequestException( - `File too large. Exceeds the ${MAX_FILE_SIZE} limit`, + `File too large. Exceeds the 100mb import limit`, ); } } diff --git a/apps/server/src/integrations/static/static.module.ts b/apps/server/src/integrations/static/static.module.ts index 9ab43eb..1753605 100644 --- a/apps/server/src/integrations/static/static.module.ts +++ b/apps/server/src/integrations/static/static.module.ts @@ -33,6 +33,7 @@ export class StaticModule implements OnModuleInit { ENV: this.environmentService.getNodeEnv(), APP_URL: this.environmentService.getAppUrl(), IS_CLOUD: this.environmentService.isCloud(), + FILE_UPLOAD_SIZE_LIMIT: this.environmentService.getFileUploadSizeLimit() }; const windowScriptContent = ``; diff --git a/package.json b/package.json index e4c6c59..d15d997 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "@tiptap/react": "^2.6.6", "@tiptap/starter-kit": "^2.6.6", "@tiptap/suggestion": "^2.6.6", + "bytes": "^3.1.2", "cross-env": "^7.0.3", "fractional-indexing-jittered": "^0.9.1", "ioredis": "^5.4.1", @@ -68,6 +69,7 @@ }, "devDependencies": { "@nx/js": "19.6.3", + "@types/bytes": "^3.1.4", "@types/uuid": "^10.0.0", "concurrently": "^8.2.2", "nx": "19.6.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 03645aa..73535bd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -137,6 +137,9 @@ importers: '@tiptap/suggestion': specifier: ^2.6.6 version: 2.6.6(@tiptap/core@2.6.6(@tiptap/pm@2.6.6))(@tiptap/pm@2.6.6) + bytes: + specifier: ^3.1.2 + version: 3.1.2 cross-env: specifier: ^7.0.3 version: 7.0.3 @@ -158,7 +161,10 @@ importers: devDependencies: '@nx/js': specifier: 19.6.3 - version: 19.6.3(@babel/traverse@7.24.6)(@swc/core@1.5.25)(@types/node@22.5.2)(nx@19.6.3(@swc/core@1.5.25))(typescript@5.5.4) + version: 19.6.3(@babel/traverse@7.24.6)(@swc/core@1.5.25(@swc/helpers@0.5.5))(@types/node@22.5.2)(nx@19.6.3(@swc/core@1.5.25(@swc/helpers@0.5.5)))(typescript@5.5.4) + '@types/bytes': + specifier: ^3.1.4 + version: 3.1.4 '@types/uuid': specifier: ^10.0.0 version: 10.0.0 @@ -167,7 +173,7 @@ importers: version: 8.2.2 nx: specifier: 19.6.3 - version: 19.6.3(@swc/core@1.5.25) + version: 19.6.3(@swc/core@1.5.25(@swc/helpers@0.5.5)) tsx: specifier: ^4.19.0 version: 4.19.0 @@ -369,7 +375,7 @@ importers: version: 7.0.4 '@nestjs/bullmq': specifier: ^10.2.1 - version: 10.2.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/websockets@10.4.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(bullmq@5.12.12) + version: 10.2.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1)(bullmq@5.12.12) '@nestjs/common': specifier: ^10.4.1 version: 10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) @@ -381,7 +387,7 @@ importers: version: 10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/websockets@10.4.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) '@nestjs/event-emitter': specifier: ^2.0.4 - version: 2.0.4(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/websockets@10.4.1)(reflect-metadata@0.2.2)(rxjs@7.8.1)) + version: 2.0.4(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1) '@nestjs/jwt': specifier: ^10.2.0 version: 10.2.0(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1)) @@ -393,13 +399,13 @@ importers: version: 10.0.3(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(passport@0.7.0) '@nestjs/platform-fastify': specifier: ^10.4.1 - version: 10.4.1(@fastify/static@7.0.4)(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/websockets@10.4.1)(reflect-metadata@0.2.2)(rxjs@7.8.1)) + version: 10.4.1(@fastify/static@7.0.4)(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1) '@nestjs/platform-socket.io': specifier: ^10.4.1 version: 10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/websockets@10.4.1)(rxjs@7.8.1) '@nestjs/terminus': specifier: ^10.2.3 - version: 10.2.3(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/websockets@10.4.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(reflect-metadata@0.2.2)(rxjs@7.8.1) + version: 10.2.3(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) '@nestjs/websockets': specifier: ^10.4.1 version: 10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1)(@nestjs/platform-socket.io@10.4.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) @@ -418,9 +424,6 @@ importers: bullmq: specifier: ^5.12.12 version: 5.12.12 - bytes: - specifier: ^3.1.2 - version: 3.1.2 class-transformer: specifier: ^0.5.1 version: 0.5.1 @@ -453,7 +456,7 @@ importers: version: 5.0.7 nestjs-kysely: specifier: ^1.0.0 - version: 1.0.0(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/websockets@10.4.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(kysely@0.27.4)(reflect-metadata@0.2.2) + version: 1.0.0(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1)(kysely@0.27.4)(reflect-metadata@0.2.2) nodemailer: specifier: ^6.9.14 version: 6.9.14 @@ -493,19 +496,16 @@ importers: devDependencies: '@nestjs/cli': specifier: ^10.4.5 - version: 10.4.5(@swc/core@1.5.25) + version: 10.4.5(@swc/core@1.5.25(@swc/helpers@0.5.5)) '@nestjs/schematics': specifier: ^10.1.4 version: 10.1.4(chokidar@3.6.0)(typescript@5.5.4) '@nestjs/testing': specifier: ^10.4.1 - version: 10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/websockets@10.4.1)(reflect-metadata@0.2.2)(rxjs@7.8.1)) + version: 10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1) '@types/bcrypt': specifier: ^5.0.2 version: 5.0.2 - '@types/bytes': - specifier: ^3.1.4 - version: 3.1.4 '@types/debounce': specifier: ^1.2.4 version: 1.2.4 @@ -553,7 +553,7 @@ importers: version: 5.2.1(@types/eslint@8.56.10)(eslint-config-prettier@9.1.0(eslint@9.9.1(jiti@1.21.0)))(eslint@9.9.1(jiti@1.21.0))(prettier@3.3.3) jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@22.5.2)(ts-node@10.9.2(@swc/core@1.5.25)(@types/node@22.5.2)(typescript@5.5.4)) + version: 29.7.0(@types/node@22.5.2)(ts-node@10.9.2(@swc/core@1.5.25(@swc/helpers@0.5.5))(@types/node@22.5.2)(typescript@5.5.4)) kysely-codegen: specifier: ^0.16.3 version: 0.16.3(kysely@0.27.4)(pg@8.12.0) @@ -571,13 +571,13 @@ importers: version: 7.0.0 ts-jest: specifier: ^29.2.5 - version: 29.2.5(@babel/core@7.24.3)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.24.3))(jest@29.7.0(@types/node@22.5.2)(ts-node@10.9.2(@swc/core@1.5.25)(@types/node@22.5.2)(typescript@5.5.4)))(typescript@5.5.4) + version: 29.2.5(@babel/core@7.24.3)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.24.3))(jest@29.7.0(@types/node@22.5.2)(ts-node@10.9.2(@swc/core@1.5.25(@swc/helpers@0.5.5))(@types/node@22.5.2)(typescript@5.5.4)))(typescript@5.5.4) ts-loader: specifier: ^9.5.1 - version: 9.5.1(typescript@5.5.4)(webpack@5.94.0(@swc/core@1.5.25)) + version: 9.5.1(typescript@5.5.4)(webpack@5.94.0(@swc/core@1.5.25(@swc/helpers@0.5.5))) ts-node: specifier: ^10.9.2 - version: 10.9.2(@swc/core@1.5.25)(@types/node@22.5.2)(typescript@5.5.4) + version: 10.9.2(@swc/core@1.5.25(@swc/helpers@0.5.5))(@types/node@22.5.2)(typescript@5.5.4) tsconfig-paths: specifier: ^4.2.0 version: 4.2.0 @@ -4036,6 +4036,7 @@ packages: are-we-there-yet@2.0.0: resolution: {integrity: sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==} engines: {node: '>=10'} + deprecated: This package is no longer supported. arg@4.1.3: resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} @@ -5228,6 +5229,7 @@ packages: gauge@3.0.2: resolution: {integrity: sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==} engines: {node: '>=10'} + deprecated: This package is no longer supported. generic-pool@3.9.0: resolution: {integrity: sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==} @@ -6321,6 +6323,7 @@ packages: npmlog@5.0.1: resolution: {integrity: sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==} + deprecated: This package is no longer supported. nwsapi@2.2.10: resolution: {integrity: sha512-QK0sRs7MKv0tKe1+5uZIQk/C8XGza4DAnztJG8iD+TpJIORARrCxczA738awHrZoHeTjSSoHqao2teO0dC/gFQ==} @@ -10084,7 +10087,7 @@ snapshots: jest-util: 29.7.0 slash: 3.0.0 - '@jest/core@29.7.0(ts-node@10.9.2(@swc/core@1.5.25)(@types/node@22.5.2)(typescript@5.5.4))': + '@jest/core@29.7.0(ts-node@10.9.2(@swc/core@1.5.25(@swc/helpers@0.5.5))(@types/node@22.5.2)(typescript@5.5.4))': dependencies: '@jest/console': 29.7.0 '@jest/reporters': 29.7.0 @@ -10098,7 +10101,7 @@ snapshots: exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@22.5.2)(ts-node@10.9.2(@swc/core@1.5.25)(@types/node@22.5.2)(typescript@5.5.4)) + jest-config: 29.7.0(@types/node@22.5.2)(ts-node@10.9.2(@swc/core@1.5.25(@swc/helpers@0.5.5))(@types/node@22.5.2)(typescript@5.5.4)) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -10384,21 +10387,21 @@ snapshots: '@emnapi/runtime': 1.2.0 '@tybys/wasm-util': 0.9.0 - '@nestjs/bull-shared@10.2.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/websockets@10.4.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))': + '@nestjs/bull-shared@10.2.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1)': dependencies: '@nestjs/common': 10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) '@nestjs/core': 10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/websockets@10.4.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) tslib: 2.6.3 - '@nestjs/bullmq@10.2.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/websockets@10.4.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(bullmq@5.12.12)': + '@nestjs/bullmq@10.2.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1)(bullmq@5.12.12)': dependencies: - '@nestjs/bull-shared': 10.2.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/websockets@10.4.1)(reflect-metadata@0.2.2)(rxjs@7.8.1)) + '@nestjs/bull-shared': 10.2.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1) '@nestjs/common': 10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) '@nestjs/core': 10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/websockets@10.4.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) bullmq: 5.12.12 tslib: 2.6.3 - '@nestjs/cli@10.4.5(@swc/core@1.5.25)': + '@nestjs/cli@10.4.5(@swc/core@1.5.25(@swc/helpers@0.5.5))': dependencies: '@angular-devkit/core': 17.3.8(chokidar@3.6.0) '@angular-devkit/schematics': 17.3.8(chokidar@3.6.0) @@ -10408,7 +10411,7 @@ snapshots: chokidar: 3.6.0 cli-table3: 0.6.5 commander: 4.1.1 - fork-ts-checker-webpack-plugin: 9.0.2(typescript@5.3.3)(webpack@5.94.0(@swc/core@1.5.25)) + fork-ts-checker-webpack-plugin: 9.0.2(typescript@5.3.3)(webpack@5.94.0(@swc/core@1.5.25(@swc/helpers@0.5.5))) glob: 10.4.2 inquirer: 8.2.6 node-emoji: 1.11.0 @@ -10417,10 +10420,10 @@ snapshots: tsconfig-paths: 4.2.0 tsconfig-paths-webpack-plugin: 4.1.0 typescript: 5.3.3 - webpack: 5.94.0(@swc/core@1.5.25) + webpack: 5.94.0(@swc/core@1.5.25(@swc/helpers@0.5.5)) webpack-node-externals: 3.0.0 optionalDependencies: - '@swc/core': 1.5.25 + '@swc/core': 1.5.25(@swc/helpers@0.5.5) transitivePeerDependencies: - esbuild - uglify-js @@ -10461,7 +10464,7 @@ snapshots: transitivePeerDependencies: - encoding - '@nestjs/event-emitter@2.0.4(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/websockets@10.4.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))': + '@nestjs/event-emitter@2.0.4(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1)': dependencies: '@nestjs/common': 10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) '@nestjs/core': 10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/websockets@10.4.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) @@ -10486,7 +10489,7 @@ snapshots: '@nestjs/common': 10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) passport: 0.7.0 - '@nestjs/platform-fastify@10.4.1(@fastify/static@7.0.4)(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/websockets@10.4.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))': + '@nestjs/platform-fastify@10.4.1(@fastify/static@7.0.4)(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1)': dependencies: '@fastify/cors': 9.0.1 '@fastify/formbody': 7.4.0 @@ -10536,7 +10539,7 @@ snapshots: transitivePeerDependencies: - chokidar - '@nestjs/terminus@10.2.3(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/websockets@10.4.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(reflect-metadata@0.2.2)(rxjs@7.8.1)': + '@nestjs/terminus@10.2.3(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1)(reflect-metadata@0.2.2)(rxjs@7.8.1)': dependencies: '@nestjs/common': 10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) '@nestjs/core': 10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/websockets@10.4.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) @@ -10545,7 +10548,7 @@ snapshots: reflect-metadata: 0.2.2 rxjs: 7.8.1 - '@nestjs/testing@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/websockets@10.4.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))': + '@nestjs/testing@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1)': dependencies: '@nestjs/common': 10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) '@nestjs/core': 10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/websockets@10.4.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) @@ -10604,15 +10607,15 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.17.1 - '@nrwl/devkit@19.6.3(nx@19.6.3(@swc/core@1.5.25))': + '@nrwl/devkit@19.6.3(nx@19.6.3(@swc/core@1.5.25(@swc/helpers@0.5.5)))': dependencies: - '@nx/devkit': 19.6.3(nx@19.6.3(@swc/core@1.5.25)) + '@nx/devkit': 19.6.3(nx@19.6.3(@swc/core@1.5.25(@swc/helpers@0.5.5))) transitivePeerDependencies: - nx - '@nrwl/js@19.6.3(@babel/traverse@7.24.6)(@swc/core@1.5.25)(@types/node@22.5.2)(nx@19.6.3(@swc/core@1.5.25))(typescript@5.5.4)': + '@nrwl/js@19.6.3(@babel/traverse@7.24.6)(@swc/core@1.5.25(@swc/helpers@0.5.5))(@types/node@22.5.2)(nx@19.6.3(@swc/core@1.5.25(@swc/helpers@0.5.5)))(typescript@5.5.4)': dependencies: - '@nx/js': 19.6.3(@babel/traverse@7.24.6)(@swc/core@1.5.25)(@types/node@22.5.2)(nx@19.6.3(@swc/core@1.5.25))(typescript@5.5.4) + '@nx/js': 19.6.3(@babel/traverse@7.24.6)(@swc/core@1.5.25(@swc/helpers@0.5.5))(@types/node@22.5.2)(nx@19.6.3(@swc/core@1.5.25(@swc/helpers@0.5.5)))(typescript@5.5.4) transitivePeerDependencies: - '@babel/traverse' - '@swc-node/register' @@ -10625,18 +10628,18 @@ snapshots: - typescript - verdaccio - '@nrwl/tao@19.6.3(@swc/core@1.5.25)': + '@nrwl/tao@19.6.3(@swc/core@1.5.25(@swc/helpers@0.5.5))': dependencies: - nx: 19.6.3(@swc/core@1.5.25) + nx: 19.6.3(@swc/core@1.5.25(@swc/helpers@0.5.5)) tslib: 2.6.2 transitivePeerDependencies: - '@swc-node/register' - '@swc/core' - debug - '@nrwl/workspace@19.6.3(@swc/core@1.5.25)': + '@nrwl/workspace@19.6.3(@swc/core@1.5.25(@swc/helpers@0.5.5))': dependencies: - '@nx/workspace': 19.6.3(@swc/core@1.5.25) + '@nx/workspace': 19.6.3(@swc/core@1.5.25(@swc/helpers@0.5.5)) transitivePeerDependencies: - '@swc-node/register' - '@swc/core' @@ -10650,20 +10653,20 @@ snapshots: transitivePeerDependencies: - encoding - '@nx/devkit@19.6.3(nx@19.6.3(@swc/core@1.5.25))': + '@nx/devkit@19.6.3(nx@19.6.3(@swc/core@1.5.25(@swc/helpers@0.5.5)))': dependencies: - '@nrwl/devkit': 19.6.3(nx@19.6.3(@swc/core@1.5.25)) + '@nrwl/devkit': 19.6.3(nx@19.6.3(@swc/core@1.5.25(@swc/helpers@0.5.5))) ejs: 3.1.9 enquirer: 2.3.6 ignore: 5.3.1 minimatch: 9.0.3 - nx: 19.6.3(@swc/core@1.5.25) + nx: 19.6.3(@swc/core@1.5.25(@swc/helpers@0.5.5)) semver: 7.6.2 tmp: 0.2.1 tslib: 2.6.2 yargs-parser: 21.1.1 - '@nx/js@19.6.3(@babel/traverse@7.24.6)(@swc/core@1.5.25)(@types/node@22.5.2)(nx@19.6.3(@swc/core@1.5.25))(typescript@5.5.4)': + '@nx/js@19.6.3(@babel/traverse@7.24.6)(@swc/core@1.5.25(@swc/helpers@0.5.5))(@types/node@22.5.2)(nx@19.6.3(@swc/core@1.5.25(@swc/helpers@0.5.5)))(typescript@5.5.4)': dependencies: '@babel/core': 7.24.6 '@babel/plugin-proposal-decorators': 7.23.7(@babel/core@7.24.6) @@ -10672,9 +10675,9 @@ snapshots: '@babel/preset-env': 7.23.8(@babel/core@7.24.6) '@babel/preset-typescript': 7.23.3(@babel/core@7.24.6) '@babel/runtime': 7.23.7 - '@nrwl/js': 19.6.3(@babel/traverse@7.24.6)(@swc/core@1.5.25)(@types/node@22.5.2)(nx@19.6.3(@swc/core@1.5.25))(typescript@5.5.4) - '@nx/devkit': 19.6.3(nx@19.6.3(@swc/core@1.5.25)) - '@nx/workspace': 19.6.3(@swc/core@1.5.25) + '@nrwl/js': 19.6.3(@babel/traverse@7.24.6)(@swc/core@1.5.25(@swc/helpers@0.5.5))(@types/node@22.5.2)(nx@19.6.3(@swc/core@1.5.25(@swc/helpers@0.5.5)))(typescript@5.5.4) + '@nx/devkit': 19.6.3(nx@19.6.3(@swc/core@1.5.25(@swc/helpers@0.5.5))) + '@nx/workspace': 19.6.3(@swc/core@1.5.25(@swc/helpers@0.5.5)) babel-plugin-const-enum: 1.2.0(@babel/core@7.24.6) babel-plugin-macros: 2.8.0 babel-plugin-transform-typescript-metadata: 0.3.2(@babel/core@7.24.6)(@babel/traverse@7.24.6) @@ -10692,7 +10695,7 @@ snapshots: ora: 5.3.0 semver: 7.6.2 source-map-support: 0.5.19 - ts-node: 10.9.1(@swc/core@1.5.25)(@types/node@22.5.2)(typescript@5.5.4) + ts-node: 10.9.1(@swc/core@1.5.25(@swc/helpers@0.5.5))(@types/node@22.5.2)(typescript@5.5.4) tsconfig-paths: 4.2.0 tslib: 2.6.2 transitivePeerDependencies: @@ -10736,13 +10739,13 @@ snapshots: '@nx/nx-win32-x64-msvc@19.6.3': optional: true - '@nx/workspace@19.6.3(@swc/core@1.5.25)': + '@nx/workspace@19.6.3(@swc/core@1.5.25(@swc/helpers@0.5.5))': dependencies: - '@nrwl/workspace': 19.6.3(@swc/core@1.5.25) - '@nx/devkit': 19.6.3(nx@19.6.3(@swc/core@1.5.25)) + '@nrwl/workspace': 19.6.3(@swc/core@1.5.25(@swc/helpers@0.5.5)) + '@nx/devkit': 19.6.3(nx@19.6.3(@swc/core@1.5.25(@swc/helpers@0.5.5))) chalk: 4.1.2 enquirer: 2.3.6 - nx: 19.6.3(@swc/core@1.5.25) + nx: 19.6.3(@swc/core@1.5.25(@swc/helpers@0.5.5)) tslib: 2.6.2 yargs-parser: 21.1.1 transitivePeerDependencies: @@ -11351,7 +11354,7 @@ snapshots: '@swc/core-win32-x64-msvc@1.5.25': optional: true - '@swc/core@1.5.25': + '@swc/core@1.5.25(@swc/helpers@0.5.5)': dependencies: '@swc/counter': 0.1.3 '@swc/types': 0.1.7 @@ -11366,6 +11369,7 @@ snapshots: '@swc/core-win32-arm64-msvc': 1.5.25 '@swc/core-win32-ia32-msvc': 1.5.25 '@swc/core-win32-x64-msvc': 1.5.25 + '@swc/helpers': 0.5.5 optional: true '@swc/counter@0.1.3': {} @@ -12711,13 +12715,13 @@ snapshots: optionalDependencies: typescript: 5.3.3 - create-jest@29.7.0(@types/node@22.5.2)(ts-node@10.9.2(@swc/core@1.5.25)(@types/node@22.5.2)(typescript@5.5.4)): + create-jest@29.7.0(@types/node@22.5.2)(ts-node@10.9.2(@swc/core@1.5.25(@swc/helpers@0.5.5))(@types/node@22.5.2)(typescript@5.5.4)): dependencies: '@jest/types': 29.6.3 chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@22.5.2)(ts-node@10.9.2(@swc/core@1.5.25)(@types/node@22.5.2)(typescript@5.5.4)) + jest-config: 29.7.0(@types/node@22.5.2)(ts-node@10.9.2(@swc/core@1.5.25(@swc/helpers@0.5.5))(@types/node@22.5.2)(typescript@5.5.4)) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -13529,7 +13533,7 @@ snapshots: cross-spawn: 7.0.3 signal-exit: 4.1.0 - fork-ts-checker-webpack-plugin@9.0.2(typescript@5.3.3)(webpack@5.94.0(@swc/core@1.5.25)): + fork-ts-checker-webpack-plugin@9.0.2(typescript@5.3.3)(webpack@5.94.0(@swc/core@1.5.25(@swc/helpers@0.5.5))): dependencies: '@babel/code-frame': 7.24.6 chalk: 4.1.2 @@ -13544,7 +13548,7 @@ snapshots: semver: 7.6.2 tapable: 2.2.1 typescript: 5.3.3 - webpack: 5.94.0(@swc/core@1.5.25) + webpack: 5.94.0(@swc/core@1.5.25(@swc/helpers@0.5.5)) form-data@4.0.0: dependencies: @@ -14044,16 +14048,16 @@ snapshots: - babel-plugin-macros - supports-color - jest-cli@29.7.0(@types/node@22.5.2)(ts-node@10.9.2(@swc/core@1.5.25)(@types/node@22.5.2)(typescript@5.5.4)): + jest-cli@29.7.0(@types/node@22.5.2)(ts-node@10.9.2(@swc/core@1.5.25(@swc/helpers@0.5.5))(@types/node@22.5.2)(typescript@5.5.4)): dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2(@swc/core@1.5.25)(@types/node@22.5.2)(typescript@5.5.4)) + '@jest/core': 29.7.0(ts-node@10.9.2(@swc/core@1.5.25(@swc/helpers@0.5.5))(@types/node@22.5.2)(typescript@5.5.4)) '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 chalk: 4.1.2 - create-jest: 29.7.0(@types/node@22.5.2)(ts-node@10.9.2(@swc/core@1.5.25)(@types/node@22.5.2)(typescript@5.5.4)) + create-jest: 29.7.0(@types/node@22.5.2)(ts-node@10.9.2(@swc/core@1.5.25(@swc/helpers@0.5.5))(@types/node@22.5.2)(typescript@5.5.4)) exit: 0.1.2 import-local: 3.1.0 - jest-config: 29.7.0(@types/node@22.5.2)(ts-node@10.9.2(@swc/core@1.5.25)(@types/node@22.5.2)(typescript@5.5.4)) + jest-config: 29.7.0(@types/node@22.5.2)(ts-node@10.9.2(@swc/core@1.5.25(@swc/helpers@0.5.5))(@types/node@22.5.2)(typescript@5.5.4)) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.7.2 @@ -14063,7 +14067,7 @@ snapshots: - supports-color - ts-node - jest-config@29.7.0(@types/node@22.5.2)(ts-node@10.9.2(@swc/core@1.5.25)(@types/node@22.5.2)(typescript@5.5.4)): + jest-config@29.7.0(@types/node@22.5.2)(ts-node@10.9.2(@swc/core@1.5.25(@swc/helpers@0.5.5))(@types/node@22.5.2)(typescript@5.5.4)): dependencies: '@babel/core': 7.24.6 '@jest/test-sequencer': 29.7.0 @@ -14089,7 +14093,7 @@ snapshots: strip-json-comments: 3.1.1 optionalDependencies: '@types/node': 22.5.2 - ts-node: 10.9.2(@swc/core@1.5.25)(@types/node@22.5.2)(typescript@5.5.4) + ts-node: 10.9.2(@swc/core@1.5.25(@swc/helpers@0.5.5))(@types/node@22.5.2)(typescript@5.5.4) transitivePeerDependencies: - babel-plugin-macros - supports-color @@ -14315,12 +14319,12 @@ snapshots: merge-stream: 2.0.0 supports-color: 8.1.1 - jest@29.7.0(@types/node@22.5.2)(ts-node@10.9.2(@swc/core@1.5.25)(@types/node@22.5.2)(typescript@5.5.4)): + jest@29.7.0(@types/node@22.5.2)(ts-node@10.9.2(@swc/core@1.5.25(@swc/helpers@0.5.5))(@types/node@22.5.2)(typescript@5.5.4)): dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2(@swc/core@1.5.25)(@types/node@22.5.2)(typescript@5.5.4)) + '@jest/core': 29.7.0(ts-node@10.9.2(@swc/core@1.5.25(@swc/helpers@0.5.5))(@types/node@22.5.2)(typescript@5.5.4)) '@jest/types': 29.6.3 import-local: 3.1.0 - jest-cli: 29.7.0(@types/node@22.5.2)(ts-node@10.9.2(@swc/core@1.5.25)(@types/node@22.5.2)(typescript@5.5.4)) + jest-cli: 29.7.0(@types/node@22.5.2)(ts-node@10.9.2(@swc/core@1.5.25(@swc/helpers@0.5.5))(@types/node@22.5.2)(typescript@5.5.4)) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -14798,7 +14802,7 @@ snapshots: neo-async@2.6.2: {} - nestjs-kysely@1.0.0(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/websockets@10.4.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(kysely@0.27.4)(reflect-metadata@0.2.2): + nestjs-kysely@1.0.0(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1)(kysely@0.27.4)(reflect-metadata@0.2.2): dependencies: '@nestjs/common': 10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) '@nestjs/core': 10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/websockets@10.4.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) @@ -14885,10 +14889,10 @@ snapshots: nwsapi@2.2.10: {} - nx@19.6.3(@swc/core@1.5.25): + nx@19.6.3(@swc/core@1.5.25(@swc/helpers@0.5.5)): dependencies: '@napi-rs/wasm-runtime': 0.2.4 - '@nrwl/tao': 19.6.3(@swc/core@1.5.25) + '@nrwl/tao': 19.6.3(@swc/core@1.5.25(@swc/helpers@0.5.5)) '@yarnpkg/lockfile': 1.1.0 '@yarnpkg/parsers': 3.0.0-rc.46 '@zkochan/js-yaml': 0.0.7 @@ -14933,7 +14937,7 @@ snapshots: '@nx/nx-linux-x64-musl': 19.6.3 '@nx/nx-win32-arm64-msvc': 19.6.3 '@nx/nx-win32-x64-msvc': 19.6.3 - '@swc/core': 1.5.25 + '@swc/core': 1.5.25(@swc/helpers@0.5.5) transitivePeerDependencies: - debug @@ -16077,16 +16081,16 @@ snapshots: mkdirp: 1.0.4 yallist: 4.0.0 - terser-webpack-plugin@5.3.10(@swc/core@1.5.25)(webpack@5.94.0(@swc/core@1.5.25)): + terser-webpack-plugin@5.3.10(@swc/core@1.5.25(@swc/helpers@0.5.5))(webpack@5.94.0(@swc/core@1.5.25(@swc/helpers@0.5.5))): dependencies: '@jridgewell/trace-mapping': 0.3.25 jest-worker: 27.5.1 schema-utils: 3.3.0 serialize-javascript: 6.0.2 terser: 5.29.2 - webpack: 5.94.0(@swc/core@1.5.25) + webpack: 5.94.0(@swc/core@1.5.25(@swc/helpers@0.5.5)) optionalDependencies: - '@swc/core': 1.5.25 + '@swc/core': 1.5.25(@swc/helpers@0.5.5) terser@5.29.2: dependencies: @@ -16160,12 +16164,12 @@ snapshots: ts-dedent@2.2.0: {} - ts-jest@29.2.5(@babel/core@7.24.3)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.24.3))(jest@29.7.0(@types/node@22.5.2)(ts-node@10.9.2(@swc/core@1.5.25)(@types/node@22.5.2)(typescript@5.5.4)))(typescript@5.5.4): + ts-jest@29.2.5(@babel/core@7.24.3)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.24.3))(jest@29.7.0(@types/node@22.5.2)(ts-node@10.9.2(@swc/core@1.5.25(@swc/helpers@0.5.5))(@types/node@22.5.2)(typescript@5.5.4)))(typescript@5.5.4): dependencies: bs-logger: 0.2.6 ejs: 3.1.10 fast-json-stable-stringify: 2.1.0 - jest: 29.7.0(@types/node@22.5.2)(ts-node@10.9.2(@swc/core@1.5.25)(@types/node@22.5.2)(typescript@5.5.4)) + jest: 29.7.0(@types/node@22.5.2)(ts-node@10.9.2(@swc/core@1.5.25(@swc/helpers@0.5.5))(@types/node@22.5.2)(typescript@5.5.4)) jest-util: 29.7.0 json5: 2.2.3 lodash.memoize: 4.1.2 @@ -16179,7 +16183,7 @@ snapshots: '@jest/types': 29.6.3 babel-jest: 29.7.0(@babel/core@7.24.3) - ts-loader@9.5.1(typescript@5.5.4)(webpack@5.94.0(@swc/core@1.5.25)): + ts-loader@9.5.1(typescript@5.5.4)(webpack@5.94.0(@swc/core@1.5.25(@swc/helpers@0.5.5))): dependencies: chalk: 4.1.2 enhanced-resolve: 5.16.0 @@ -16187,9 +16191,9 @@ snapshots: semver: 7.6.0 source-map: 0.7.4 typescript: 5.5.4 - webpack: 5.94.0(@swc/core@1.5.25) + webpack: 5.94.0(@swc/core@1.5.25(@swc/helpers@0.5.5)) - ts-node@10.9.1(@swc/core@1.5.25)(@types/node@22.5.2)(typescript@5.5.4): + ts-node@10.9.1(@swc/core@1.5.25(@swc/helpers@0.5.5))(@types/node@22.5.2)(typescript@5.5.4): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.9 @@ -16207,9 +16211,9 @@ snapshots: v8-compile-cache-lib: 3.0.1 yn: 3.1.1 optionalDependencies: - '@swc/core': 1.5.25 + '@swc/core': 1.5.25(@swc/helpers@0.5.5) - ts-node@10.9.2(@swc/core@1.5.25)(@types/node@22.5.2)(typescript@5.5.4): + ts-node@10.9.2(@swc/core@1.5.25(@swc/helpers@0.5.5))(@types/node@22.5.2)(typescript@5.5.4): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.9 @@ -16227,7 +16231,7 @@ snapshots: v8-compile-cache-lib: 3.0.1 yn: 3.1.1 optionalDependencies: - '@swc/core': 1.5.25 + '@swc/core': 1.5.25(@swc/helpers@0.5.5) tsconfig-paths-webpack-plugin@4.1.0: dependencies: @@ -16430,7 +16434,7 @@ snapshots: webpack-sources@3.2.3: {} - webpack@5.94.0(@swc/core@1.5.25): + webpack@5.94.0(@swc/core@1.5.25(@swc/helpers@0.5.5)): dependencies: '@types/estree': 1.0.5 '@webassemblyjs/ast': 1.12.1 @@ -16452,7 +16456,7 @@ snapshots: neo-async: 2.6.2 schema-utils: 3.3.0 tapable: 2.2.1 - terser-webpack-plugin: 5.3.10(@swc/core@1.5.25)(webpack@5.94.0(@swc/core@1.5.25)) + terser-webpack-plugin: 5.3.10(@swc/core@1.5.25(@swc/helpers@0.5.5))(webpack@5.94.0(@swc/core@1.5.25(@swc/helpers@0.5.5))) watchpack: 2.4.1 webpack-sources: 3.2.3 transitivePeerDependencies: From 078361b367dae4e7f3b69dde3ed9d770a3156e64 Mon Sep 17 00:00:00 2001 From: Philipinho <16838612+Philipinho@users.noreply.github.com> Date: Thu, 10 Oct 2024 21:57:36 +0100 Subject: [PATCH 05/71] add local editor-ext to client package.json * update vite --- apps/client/package.json | 3 ++- pnpm-lock.yaml | 19 +++++++++++-------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/apps/client/package.json b/apps/client/package.json index fea40cb..848c4e1 100644 --- a/apps/client/package.json +++ b/apps/client/package.json @@ -9,6 +9,7 @@ "preview": "vite preview" }, "dependencies": { + "@docmost/editor-ext": "workspace:*", "@casl/ability": "^6.7.1", "@casl/react": "^4.0.0", "@emoji-mart/data": "^1.2.1", @@ -67,6 +68,6 @@ "postcss-simple-vars": "^7.0.1", "prettier": "^3.3.3", "typescript": "^5.5.4", - "vite": "^5.4.2" + "vite": "^5.4.8" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 73535bd..e67d793 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -186,6 +186,9 @@ importers: '@casl/react': specifier: ^4.0.0 version: 4.0.0(@casl/ability@6.7.1)(react@18.3.1) + '@docmost/editor-ext': + specifier: workspace:* + version: link:../../packages/editor-ext '@emoji-mart/data': specifier: ^1.2.1 version: 1.2.1 @@ -321,7 +324,7 @@ importers: version: 8.3.0(eslint@9.9.1(jiti@1.21.0))(typescript@5.5.4) '@vitejs/plugin-react': specifier: ^4.3.1 - version: 4.3.1(vite@5.4.2(@types/node@22.5.2)(less@4.2.0)(sugarss@4.0.1(postcss@8.4.43))(terser@5.29.2)) + version: 4.3.1(vite@5.4.8(@types/node@22.5.2)(less@4.2.0)(sugarss@4.0.1(postcss@8.4.43))(terser@5.29.2)) eslint: specifier: ^9.9.1 version: 9.9.1(jiti@1.21.0) @@ -350,8 +353,8 @@ importers: specifier: ^5.5.4 version: 5.5.4 vite: - specifier: ^5.4.2 - version: 5.4.2(@types/node@22.5.2)(less@4.2.0)(sugarss@4.0.1(postcss@8.4.43))(terser@5.29.2) + specifier: ^5.4.8 + version: 5.4.8(@types/node@22.5.2)(less@4.2.0)(sugarss@4.0.1(postcss@8.4.43))(terser@5.29.2) apps/server: dependencies: @@ -7756,8 +7759,8 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} - vite@5.4.2: - resolution: {integrity: sha512-dDrQTRHp5C1fTFzcSaMxjk6vdpKvT+2/mIdE07Gw2ykehT49O0z/VHS3zZ8iV/Gh8BJJKHWOe5RjaNrW5xf/GA==} + vite@5.4.8: + resolution: {integrity: sha512-FqrItQ4DT1NC4zCUqMB4c4AZORMKIa0m8/URVCZ77OZ/QSNeJ54bU1vrFADbDsuwfIPcgknRkmqakQcgnL4GiQ==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: @@ -11994,14 +11997,14 @@ snapshots: dependencies: '@ucast/core': 1.10.2 - '@vitejs/plugin-react@4.3.1(vite@5.4.2(@types/node@22.5.2)(less@4.2.0)(sugarss@4.0.1(postcss@8.4.43))(terser@5.29.2))': + '@vitejs/plugin-react@4.3.1(vite@5.4.8(@types/node@22.5.2)(less@4.2.0)(sugarss@4.0.1(postcss@8.4.43))(terser@5.29.2))': dependencies: '@babel/core': 7.24.6 '@babel/plugin-transform-react-jsx-self': 7.24.6(@babel/core@7.24.6) '@babel/plugin-transform-react-jsx-source': 7.24.6(@babel/core@7.24.6) '@types/babel__core': 7.20.5 react-refresh: 0.14.2 - vite: 5.4.2(@types/node@22.5.2)(less@4.2.0)(sugarss@4.0.1(postcss@8.4.43))(terser@5.29.2) + vite: 5.4.8(@types/node@22.5.2)(less@4.2.0)(sugarss@4.0.1(postcss@8.4.43))(terser@5.29.2) transitivePeerDependencies: - supports-color @@ -16378,7 +16381,7 @@ snapshots: vary@1.1.2: {} - vite@5.4.2(@types/node@22.5.2)(less@4.2.0)(sugarss@4.0.1(postcss@8.4.43))(terser@5.29.2): + vite@5.4.8(@types/node@22.5.2)(less@4.2.0)(sugarss@4.0.1(postcss@8.4.43))(terser@5.29.2): dependencies: esbuild: 0.21.5 postcss: 8.4.43 From 038d21b438e3acc64d75bd56bbc9b456addfb0d1 Mon Sep 17 00:00:00 2001 From: Philipinho <16838612+Philipinho@users.noreply.github.com> Date: Thu, 10 Oct 2024 22:03:16 +0100 Subject: [PATCH 06/71] v0.4.0 --- apps/client/package.json | 2 +- apps/server/package.json | 2 +- package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/client/package.json b/apps/client/package.json index 848c4e1..be240f1 100644 --- a/apps/client/package.json +++ b/apps/client/package.json @@ -1,7 +1,7 @@ { "name": "client", "private": true, - "version": "0.3.1", + "version": "0.4.0", "scripts": { "dev": "vite", "build": "tsc && vite build", diff --git a/apps/server/package.json b/apps/server/package.json index f1ca9ee..1bced2a 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -1,6 +1,6 @@ { "name": "server", - "version": "0.3.1", + "version": "0.4.0", "description": "", "author": "", "private": true, diff --git a/package.json b/package.json index d15d997..4ab7042 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "docmost", "homepage": "https://docmost.com", - "version": "0.3.1", + "version": "0.4.0", "private": true, "scripts": { "build": "nx run-many -t build", From e17b975aaa0d907f070355fc99db886d332242e0 Mon Sep 17 00:00:00 2001 From: Philip Okugbe Date: Fri, 11 Oct 2024 11:37:39 +0100 Subject: [PATCH 07/71] Revert "Add alignment styles for task list items (#378)" (#390) This reverts commit 2721ab6a29e6e1cef801d54773d412f43eaa9e02. --- apps/client/src/features/editor/styles/task-list.css | 2 -- 1 file changed, 2 deletions(-) diff --git a/apps/client/src/features/editor/styles/task-list.css b/apps/client/src/features/editor/styles/task-list.css index cb2720c..a2c5f6f 100644 --- a/apps/client/src/features/editor/styles/task-list.css +++ b/apps/client/src/features/editor/styles/task-list.css @@ -13,8 +13,6 @@ ul[data-type="taskList"] { flex: 0 0 auto; margin-right: 0.5rem; user-select: none; - display: flex; - align-items: center; } > div { From 46d92fbabcff25988eeb8ede276857967999d6f8 Mon Sep 17 00:00:00 2001 From: Philipinho <16838612+Philipinho@users.noreply.github.com> Date: Fri, 11 Oct 2024 11:39:12 +0100 Subject: [PATCH 08/71] v0.4.1 --- apps/client/package.json | 2 +- apps/server/package.json | 2 +- package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/client/package.json b/apps/client/package.json index be240f1..f849241 100644 --- a/apps/client/package.json +++ b/apps/client/package.json @@ -1,7 +1,7 @@ { "name": "client", "private": true, - "version": "0.4.0", + "version": "0.4.1", "scripts": { "dev": "vite", "build": "tsc && vite build", diff --git a/apps/server/package.json b/apps/server/package.json index 1bced2a..7f2b1f7 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -1,6 +1,6 @@ { "name": "server", - "version": "0.4.0", + "version": "0.4.1", "description": "", "author": "", "private": true, diff --git a/package.json b/package.json index 4ab7042..e1fa362 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "docmost", "homepage": "https://docmost.com", - "version": "0.4.0", + "version": "0.4.1", "private": true, "scripts": { "build": "nx run-many -t build", From fa3c8a03e1592897b4a3d0efca820351c2d39181 Mon Sep 17 00:00:00 2001 From: Philip Okugbe Date: Sat, 12 Oct 2024 13:14:29 +0100 Subject: [PATCH 09/71] fix: remove space tree delete shortcut key (#394) --- package.json | 5 +++++ patches/react-arborist@3.4.0.patch | 33 ++++++++++++++++++++++++++++++ pnpm-lock.yaml | 9 ++++++-- 3 files changed, 45 insertions(+), 2 deletions(-) create mode 100644 patches/react-arborist@3.4.0.patch diff --git a/package.json b/package.json index e1fa362..fb58de0 100644 --- a/package.json +++ b/package.json @@ -80,5 +80,10 @@ "apps/*", "packages/*" ] + }, + "pnpm": { + "patchedDependencies": { + "react-arborist@3.4.0": "patches/react-arborist@3.4.0.patch" + } } } diff --git a/patches/react-arborist@3.4.0.patch b/patches/react-arborist@3.4.0.patch new file mode 100644 index 0000000..0d8c1ae --- /dev/null +++ b/patches/react-arborist@3.4.0.patch @@ -0,0 +1,33 @@ +diff --git a/dist/module/components/default-container.js b/dist/module/components/default-container.js +index 47724f59b482454fe3144dbb98bd16d3df6a9c17..2285e35ea0073a773b7b74e22758056fd3514c1a 100644 +--- a/dist/module/components/default-container.js ++++ b/dist/module/components/default-container.js +@@ -34,28 +34,6 @@ export function DefaultContainer() { + return; + } + if (e.key === "Backspace") { +- if (!tree.props.onDelete) +- return; +- const ids = Array.from(tree.selectedIds); +- if (ids.length > 1) { +- let nextFocus = tree.mostRecentNode; +- while (nextFocus && nextFocus.isSelected) { +- nextFocus = nextFocus.nextSibling; +- } +- if (!nextFocus) +- nextFocus = tree.lastNode; +- tree.focus(nextFocus, { scroll: false }); +- tree.delete(Array.from(ids)); +- } +- else { +- const node = tree.focusedNode; +- if (node) { +- const sib = node.nextSibling; +- const parent = node.parent; +- tree.focus(sib || parent, { scroll: false }); +- tree.delete(node); +- } +- } + return; + } + if (e.key === "Tab" && !e.shiftKey) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e67d793..ab9aa93 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,6 +4,11 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +patchedDependencies: + react-arborist@3.4.0: + hash: gjrtleyvvmuvu5j5zdnhxauhsu + path: patches/react-arborist@3.4.0.patch + importers: .: @@ -263,7 +268,7 @@ importers: version: 18.3.1 react-arborist: specifier: ^3.4.0 - version: 3.4.0(@types/node@22.5.2)(@types/react@18.3.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 3.4.0(patch_hash=gjrtleyvvmuvu5j5zdnhxauhsu)(@types/node@22.5.2)(@types/react@18.3.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react-clear-modal: specifier: ^2.0.9 version: 2.0.9(@types/react@18.3.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -15434,7 +15439,7 @@ snapshots: dependencies: safe-buffer: 5.2.1 - react-arborist@3.4.0(@types/node@22.5.2)(@types/react@18.3.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + react-arborist@3.4.0(patch_hash=gjrtleyvvmuvu5j5zdnhxauhsu)(@types/node@22.5.2)(@types/react@18.3.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: react: 18.3.1 react-dnd: 14.0.5(@types/node@22.5.2)(@types/react@18.3.5)(react@18.3.1) From 36e720920b773f2791870d4f51bc682f56f3440a Mon Sep 17 00:00:00 2001 From: Philip Okugbe Date: Sun, 13 Oct 2024 17:09:45 +0100 Subject: [PATCH 10/71] fix: bug fixes (#397) * Add more html page titles * Make tables responsive * fix react query keys * Add tooltip to sidebar toggle * fix: trim inputs * fix inputs --- .../src/components/common/recent-changes.tsx | 30 +-- .../components/layouts/global/app-header.tsx | 47 +++-- .../components/ui/sidebar-toggle-button.tsx | 42 ++-- .../auth/components/invite-sign-up-form.tsx | 2 +- .../auth/components/setup-workspace-form.tsx | 4 +- .../group/components/create-group-form.tsx | 2 +- .../features/group/components/group-list.tsx | 119 ++++++----- .../group/components/group-members.tsx | 136 ++++++------ .../src/features/group/queries/group-query.ts | 24 ++- .../space/components/create-space-form.tsx | 3 +- .../space/components/settings-modal.tsx | 80 +++---- .../features/space/components/space-list.tsx | 92 ++++---- .../space/components/space-members.tsx | 198 +++++++++--------- .../src/features/space/queries/space-query.ts | 6 +- .../components/workspace-invites-table.tsx | 88 ++++---- .../components/workspace-members-table.tsx | 100 ++++----- apps/client/src/lib/config.ts | 4 + .../client/src/pages/auth/forgot-password.tsx | 3 +- apps/client/src/pages/auth/invite-signup.tsx | 7 +- apps/client/src/pages/auth/login.tsx | 3 +- apps/client/src/pages/auth/password-reset.tsx | 5 +- .../client/src/pages/auth/setup-workspace.tsx | 3 +- apps/client/src/pages/dashboard/home.tsx | 23 +- .../settings/account/account-preferences.tsx | 23 +- .../settings/account/account-settings.tsx | 5 + .../src/pages/settings/group/group-info.tsx | 19 +- .../src/pages/settings/group/groups.tsx | 5 + .../src/pages/settings/space/spaces.tsx | 27 ++- .../settings/workspace/workspace-members.tsx | 97 +++++---- .../settings/workspace/workspace-settings.tsx | 17 +- apps/client/src/pages/space/space-home.tsx | 27 ++- apps/client/src/pages/welcome.tsx | 23 -- .../core/auth/dto/create-admin-user.dto.ts | 11 +- .../src/core/auth/dto/create-user.dto.ts | 6 +- .../core/auth/dto/verify-user-token.dto.ts | 2 +- .../src/core/group/dto/create-group.dto.ts | 2 + .../src/core/space/dto/create-space.dto.ts | 2 + .../workspace/dto/create-workspace.dto.ts | 6 +- 38 files changed, 681 insertions(+), 612 deletions(-) delete mode 100644 apps/client/src/pages/welcome.tsx diff --git a/apps/client/src/components/common/recent-changes.tsx b/apps/client/src/components/common/recent-changes.tsx index de7f023..0f89c81 100644 --- a/apps/client/src/components/common/recent-changes.tsx +++ b/apps/client/src/components/common/recent-changes.tsx @@ -4,25 +4,25 @@ import { UnstyledButton, Badge, Table, - ScrollArea, ActionIcon, } from '@mantine/core'; -import { Link } from 'react-router-dom'; +import {Link} from 'react-router-dom'; import PageListSkeleton from '@/components/ui/page-list-skeleton.tsx'; -import { buildPageUrl } from '@/features/page/page.utils.ts'; -import { formattedDate } from '@/lib/time.ts'; -import { useRecentChangesQuery } from '@/features/page/queries/page-query.ts'; -import { IconFileDescription } from '@tabler/icons-react'; -import { getSpaceUrl } from '@/lib/config.ts'; +import {buildPageUrl} from '@/features/page/page.utils.ts'; +import {formattedDate} from '@/lib/time.ts'; +import {useRecentChangesQuery} from '@/features/page/queries/page-query.ts'; +import {IconFileDescription} from '@tabler/icons-react'; +import {getSpaceUrl} from '@/lib/config.ts'; interface Props { spaceId?: string; } -export default function RecentChanges({ spaceId }: Props) { - const { data: pages, isLoading, isError } = useRecentChangesQuery(spaceId); + +export default function RecentChanges({spaceId}: Props) { + const {data: pages, isLoading, isError} = useRecentChangesQuery(spaceId); if (isLoading) { - return ; + return ; } if (isError) { @@ -30,7 +30,7 @@ export default function RecentChanges({ spaceId }: Props) { } return pages && pages.items.length > 0 ? ( - + {pages.items.map((page) => ( @@ -43,7 +43,7 @@ export default function RecentChanges({ spaceId }: Props) { {page.icon || ( - + )} @@ -60,14 +60,14 @@ export default function RecentChanges({ spaceId }: Props) { variant="light" component={Link} to={getSpaceUrl(page?.space.slug)} - style={{ cursor: 'pointer' }} + style={{cursor: 'pointer'}} > {page?.space.name} )} - + {formattedDate(page.updatedAt)} @@ -75,7 +75,7 @@ export default function RecentChanges({ spaceId }: Props) { ))}
-
+ ) : ( No pages yet diff --git a/apps/client/src/components/layouts/global/app-header.tsx b/apps/client/src/components/layouts/global/app-header.tsx index c83bf4f..1eeb735 100644 --- a/apps/client/src/components/layouts/global/app-header.tsx +++ b/apps/client/src/components/layouts/global/app-header.tsx @@ -1,18 +1,18 @@ -import { Group, Text } from "@mantine/core"; +import {Group, Text, Tooltip} from "@mantine/core"; import classes from "./app-header.module.css"; import React from "react"; import TopMenu from "@/components/layouts/global/top-menu.tsx"; -import { Link } from "react-router-dom"; +import {Link} from "react-router-dom"; import APP_ROUTE from "@/lib/app-route.ts"; -import { useAtom } from "jotai/index"; +import {useAtom} from "jotai/index"; import { desktopSidebarAtom, mobileSidebarAtom, } from "@/components/layouts/global/hooks/atoms/sidebar-atom.ts"; -import { useToggleSidebar } from "@/components/layouts/global/hooks/hooks/use-toggle-sidebar.ts"; +import {useToggleSidebar} from "@/components/layouts/global/hooks/hooks/use-toggle-sidebar.ts"; import SidebarToggle from "@/components/ui/sidebar-toggle-button.tsx"; -const links = [{ link: APP_ROUTE.HOME, label: "Home" }]; +const links = [{link: APP_ROUTE.HOME, label: "Home"}]; export function AppHeader() { const [mobileOpened] = useAtom(mobileSidebarAtom); @@ -35,28 +35,33 @@ export function AppHeader() { {!isHomeRoute && ( <> - + - + + + + + + )} @@ -69,7 +74,7 @@ export function AppHeader() { - + diff --git a/apps/client/src/components/ui/sidebar-toggle-button.tsx b/apps/client/src/components/ui/sidebar-toggle-button.tsx index 0c17459..be89484 100644 --- a/apps/client/src/components/ui/sidebar-toggle-button.tsx +++ b/apps/client/src/components/ui/sidebar-toggle-button.tsx @@ -1,15 +1,9 @@ +import React from "react"; import { IconLayoutSidebarRightCollapse, - IconLayoutSidebarRightExpand, + IconLayoutSidebarRightExpand } from "@tabler/icons-react"; -import { - ActionIcon, - BoxProps, - ElementProps, - MantineColor, - MantineSize, -} from "@mantine/core"; -import React from "react"; +import { ActionIcon, BoxProps, ElementProps, MantineColor, MantineSize } from "@mantine/core"; export interface SidebarToggleProps extends BoxProps, ElementProps<"button"> { size?: MantineSize | `compact-${MantineSize}` | (string & {}); @@ -17,18 +11,18 @@ export interface SidebarToggleProps extends BoxProps, ElementProps<"button"> { opened?: boolean; } -export default function SidebarToggle({ - opened, - size = "sm", - ...others -}: SidebarToggleProps) { - return ( - - {opened ? ( - - ) : ( - - )} - - ); -} +const SidebarToggle = React.forwardRef( + ({ opened, size = "sm", ...others }, ref) => { + return ( + + {opened ? ( + + ) : ( + + )} + + ); + } +); + +export default SidebarToggle; diff --git a/apps/client/src/features/auth/components/invite-sign-up-form.tsx b/apps/client/src/features/auth/components/invite-sign-up-form.tsx index 81752f5..2690d00 100644 --- a/apps/client/src/features/auth/components/invite-sign-up-form.tsx +++ b/apps/client/src/features/auth/components/invite-sign-up-form.tsx @@ -19,7 +19,7 @@ import { useGetInvitationQuery } from "@/features/workspace/queries/workspace-qu import { useRedirectIfAuthenticated } from "@/features/auth/hooks/use-redirect-if-authenticated.ts"; const formSchema = z.object({ - name: z.string().min(2), + name: z.string().trim().min(1), password: z.string().min(8), }); diff --git a/apps/client/src/features/auth/components/setup-workspace-form.tsx b/apps/client/src/features/auth/components/setup-workspace-form.tsx index bb62918..6be94f5 100644 --- a/apps/client/src/features/auth/components/setup-workspace-form.tsx +++ b/apps/client/src/features/auth/components/setup-workspace-form.tsx @@ -15,8 +15,8 @@ import useAuth from "@/features/auth/hooks/use-auth"; import classes from "@/features/auth/components/auth.module.css"; const formSchema = z.object({ - workspaceName: z.string().min(2).max(60), - name: z.string().min(2).max(60), + workspaceName: z.string().trim().min(3).max(50), + name: z.string().min(1).max(50), email: z .string() .min(1, { message: "email is required" }) diff --git a/apps/client/src/features/group/components/create-group-form.tsx b/apps/client/src/features/group/components/create-group-form.tsx index 2773064..0c6df49 100644 --- a/apps/client/src/features/group/components/create-group-form.tsx +++ b/apps/client/src/features/group/components/create-group-form.tsx @@ -7,7 +7,7 @@ import { useNavigate } from "react-router-dom"; import { MultiUserSelect } from "@/features/group/components/multi-user-select.tsx"; const formSchema = z.object({ - name: z.string().min(2).max(50), + name: z.string().trim().min(2).max(50), description: z.string().max(500), }); diff --git a/apps/client/src/features/group/components/group-list.tsx b/apps/client/src/features/group/components/group-list.tsx index ae5d647..0da1d02 100644 --- a/apps/client/src/features/group/components/group-list.tsx +++ b/apps/client/src/features/group/components/group-list.tsx @@ -1,69 +1,72 @@ -import { Table, Group, Text, Anchor } from "@mantine/core"; -import { useGetGroupsQuery } from "@/features/group/queries/group-query"; +import {Table, Group, Text, Anchor} from "@mantine/core"; +import {useGetGroupsQuery} from "@/features/group/queries/group-query"; import React from "react"; -import { Link } from "react-router-dom"; -import { IconGroupCircle } from "@/components/icons/icon-people-circle.tsx"; +import {Link} from "react-router-dom"; +import {IconGroupCircle} from "@/components/icons/icon-people-circle.tsx"; export default function GroupList() { - const { data, isLoading } = useGetGroupsQuery(); + const {data, isLoading} = useGetGroupsQuery(); return ( <> {data && ( - - - - Group - Members - - - - - {data?.items.map((group, index) => ( - - - - - -
- - {group.name} - - - {group.description} - -
-
-
-
- - - - {group.memberCount} members - - + +
+ + + Group + Members - ))} - -
+ + + + {data?.items.map((group, index) => ( + + + + + +
+ + {group.name} + + + {group.description} + +
+
+
+
+ + + + {group.memberCount} members + + +
+ ))} +
+ + )} ); diff --git a/apps/client/src/features/group/components/group-members.tsx b/apps/client/src/features/group/components/group-members.tsx index 157f4d7..62b5714 100644 --- a/apps/client/src/features/group/components/group-members.tsx +++ b/apps/client/src/features/group/components/group-members.tsx @@ -1,20 +1,20 @@ -import { Group, Table, Text, Badge, Menu, ActionIcon } from "@mantine/core"; +import {Group, Table, Text, Badge, Menu, ActionIcon} from "@mantine/core"; import { useGroupMembersQuery, useRemoveGroupMemberMutation, } from "@/features/group/queries/group-query"; -import { useParams } from "react-router-dom"; +import {useParams} from "react-router-dom"; import React from "react"; -import { IconDots } from "@tabler/icons-react"; -import { modals } from "@mantine/modals"; -import { CustomAvatar } from "@/components/ui/custom-avatar.tsx"; +import {IconDots} from "@tabler/icons-react"; +import {modals} from "@mantine/modals"; +import {CustomAvatar} from "@/components/ui/custom-avatar.tsx"; import useUserRole from "@/hooks/use-user-role.tsx"; export default function GroupMembersList() { - const { groupId } = useParams(); - const { data, isLoading } = useGroupMembersQuery(groupId); + const {groupId} = useParams(); + const {data, isLoading} = useGroupMembersQuery(groupId); const removeGroupMember = useRemoveGroupMemberMutation(); - const { isAdmin } = useUserRole(); + const {isAdmin} = useUserRole(); const onRemove = async (userId: string) => { const memberToRemove = { @@ -34,72 +34,74 @@ export default function GroupMembersList() {
), centered: true, - labels: { confirm: "Delete", cancel: "Cancel" }, - confirmProps: { color: "red" }, + labels: {confirm: "Delete", cancel: "Cancel"}, + confirmProps: {color: "red"}, onConfirm: () => onRemove(userId), }); return ( <> {data && ( - - - - User - Status - - - - - - {data?.items.map((user, index) => ( - - - - -
- - {user.name} - - - {user.email} - -
-
-
- - - Active - - - - {isAdmin && ( - - - - - - - - - openRemoveModal(user.id)}> - Remove group member - - - - )} - + +
+ + + User + Status + - ))} - -
+ + + + {data?.items.map((user, index) => ( + + + + +
+ + {user.name} + + + {user.email} + +
+
+
+ + + Active + + + + {isAdmin && ( + + + + + + + + + openRemoveModal(user.id)}> + Remove group member + + + + )} + +
+ ))} +
+ + )} ); diff --git a/apps/client/src/features/group/queries/group-query.ts b/apps/client/src/features/group/queries/group-query.ts index b61314a..ac113f8 100644 --- a/apps/client/src/features/group/queries/group-query.ts +++ b/apps/client/src/features/group/queries/group-query.ts @@ -29,24 +29,22 @@ export function useGetGroupsQuery( export function useGroupQuery(groupId: string): UseQueryResult { return useQuery({ - queryKey: ['groups', groupId], + queryKey: ['group', groupId], queryFn: () => getGroupById(groupId), enabled: !!groupId, }); } -export function useGroupMembersQuery(groupId: string) { - return useQuery({ - queryKey: ['groupMembers', groupId], - queryFn: () => getGroupMembers(groupId), - enabled: !!groupId, - }); -} - export function useCreateGroupMutation() { + const queryClient = useQueryClient(); + return useMutation>({ mutationFn: (data) => createGroup(data), onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: ['groups'], + }); + notifications.show({ message: 'Group created successfully' }); }, onError: () => { @@ -96,6 +94,14 @@ export function useDeleteGroupMutation() { }); } +export function useGroupMembersQuery(groupId: string) { + return useQuery({ + queryKey: ['groupMembers', groupId], + queryFn: () => getGroupMembers(groupId), + enabled: !!groupId, + }); +} + export function useAddGroupMemberMutation() { const queryClient = useQueryClient(); diff --git a/apps/client/src/features/space/components/create-space-form.tsx b/apps/client/src/features/space/components/create-space-form.tsx index 29fe46f..15049c1 100644 --- a/apps/client/src/features/space/components/create-space-form.tsx +++ b/apps/client/src/features/space/components/create-space-form.tsx @@ -8,9 +8,10 @@ import { computeSpaceSlug } from "@/lib"; import { getSpaceUrl } from "@/lib/config.ts"; const formSchema = z.object({ - name: z.string().min(2).max(50), + name: z.string().trim().min(2).max(50), slug: z .string() + .trim() .min(2) .max(50) .regex( diff --git a/apps/client/src/features/space/components/settings-modal.tsx b/apps/client/src/features/space/components/settings-modal.tsx index e2f4ed7..cac9cda 100644 --- a/apps/client/src/features/space/components/settings-modal.tsx +++ b/apps/client/src/features/space/components/settings-modal.tsx @@ -1,10 +1,10 @@ -import { Modal, Tabs, rem, Group, ScrollArea } from "@mantine/core"; +import {Modal, Tabs, rem, Group, ScrollArea, Text} from "@mantine/core"; import SpaceMembersList from "@/features/space/components/space-members.tsx"; import AddSpaceMembersModal from "@/features/space/components/add-space-members-modal.tsx"; -import React, { useMemo } from "react"; +import React, {useMemo} from "react"; import SpaceDetails from "@/features/space/components/space-details.tsx"; -import { useSpaceQuery } from "@/features/space/queries/space-query.ts"; -import { useSpaceAbility } from "@/features/space/permissions/use-space-ability.ts"; +import {useSpaceQuery} from "@/features/space/queries/space-query.ts"; +import {useSpaceAbility} from "@/features/space/permissions/use-space-ability.ts"; import { SpaceCaslAction, SpaceCaslSubject, @@ -17,11 +17,11 @@ interface SpaceSettingsModalProps { } export default function SpaceSettingsModal({ - spaceId, - opened, - onClose, -}: SpaceSettingsModalProps) { - const { data: space, isLoading } = useSpaceQuery(spaceId); + spaceId, + opened, + onClose, + }: SpaceSettingsModalProps) { + const {data: space, isLoading} = useSpaceQuery(spaceId); const spaceRules = space?.membership?.permissions; const spaceAbility = useMemo(() => useSpaceAbility(spaceRules), [spaceRules]); @@ -37,14 +37,16 @@ export default function SpaceSettingsModal({ xOffset={0} mah={400} > - - + + - {space?.name} - + + {space?.name} + + -
+
@@ -55,34 +57,32 @@ export default function SpaceSettingsModal({ - - - - + + + - - - {spaceAbility.can( - SpaceCaslAction.Manage, - SpaceCaslSubject.Member, - ) && } - + + + {spaceAbility.can( + SpaceCaslAction.Manage, + SpaceCaslSubject.Member, + ) && } + - - - + +
diff --git a/apps/client/src/features/space/components/space-list.tsx b/apps/client/src/features/space/components/space-list.tsx index 48253fd..cb271b6 100644 --- a/apps/client/src/features/space/components/space-list.tsx +++ b/apps/client/src/features/space/components/space-list.tsx @@ -1,13 +1,13 @@ -import { Table, Group, Text, Avatar } from "@mantine/core"; -import React, { useState } from "react"; -import { useGetSpacesQuery } from "@/features/space/queries/space-query.ts"; +import {Table, Group, Text, Avatar} from "@mantine/core"; +import React, {useState} from "react"; +import {useGetSpacesQuery} from "@/features/space/queries/space-query.ts"; import SpaceSettingsModal from "@/features/space/components/settings-modal.tsx"; -import { useDisclosure } from "@mantine/hooks"; -import { formatMemberCount } from "@/lib"; +import {useDisclosure} from "@mantine/hooks"; +import {formatMemberCount} from "@/lib"; export default function SpaceList() { - const { data, isLoading } = useGetSpacesQuery(); - const [opened, { open, close }] = useDisclosure(false); + const {data, isLoading} = useGetSpacesQuery(); + const [opened, {open, close}] = useDisclosure(false); const [selectedSpaceId, setSelectedSpaceId] = useState(null); const handleClick = (spaceId: string) => { @@ -18,44 +18,48 @@ export default function SpaceList() { return ( <> {data && ( - - - - Space - Members - - - - - {data?.items.map((space, index) => ( - handleClick(space.id)} - > - - - -
- - {space.name} - - - {space.description} - -
-
-
- - {formatMemberCount(space.memberCount)} + +
+ + + Space + Members - ))} - -
+ + + + {data?.items.map((space, index) => ( + handleClick(space.id)} + > + + + +
+ + {space.name} + + + {space.description} + +
+
+
+ + + {formatMemberCount(space.memberCount)} + +
+ ))} +
+ + )} {selectedSpaceId && ( diff --git a/apps/client/src/features/space/components/space-members.tsx b/apps/client/src/features/space/components/space-members.tsx index 763cedc..d6861dd 100644 --- a/apps/client/src/features/space/components/space-members.tsx +++ b/apps/client/src/features/space/components/space-members.tsx @@ -1,32 +1,34 @@ -import { Group, Table, Text, Menu, ActionIcon } from "@mantine/core"; +import {Group, Table, Text, Menu, ActionIcon} from "@mantine/core"; import React from "react"; -import { IconDots } from "@tabler/icons-react"; -import { modals } from "@mantine/modals"; -import { CustomAvatar } from "@/components/ui/custom-avatar.tsx"; +import {IconDots} from "@tabler/icons-react"; +import {modals} from "@mantine/modals"; +import {CustomAvatar} from "@/components/ui/custom-avatar.tsx"; import { useChangeSpaceMemberRoleMutation, useRemoveSpaceMemberMutation, useSpaceMembersQuery, } from "@/features/space/queries/space-query.ts"; -import { IconGroupCircle } from "@/components/icons/icon-people-circle.tsx"; -import { IRemoveSpaceMember } from "@/features/space/types/space.types.ts"; +import {IconGroupCircle} from "@/components/icons/icon-people-circle.tsx"; +import {IRemoveSpaceMember} from "@/features/space/types/space.types.ts"; import RoleSelectMenu from "@/components/ui/role-select-menu.tsx"; import { getSpaceRoleLabel, spaceRoleData, } from "@/features/space/types/space-role-data.ts"; -import { formatMemberCount } from "@/lib"; +import {formatMemberCount} from "@/lib"; type MemberType = "user" | "group"; + interface SpaceMembersProps { spaceId: string; readOnly?: boolean; } + export default function SpaceMembersList({ - spaceId, - readOnly, -}: SpaceMembersProps) { - const { data, isLoading } = useSpaceMembersQuery(spaceId); + spaceId, + readOnly, + }: SpaceMembersProps) { + const {data, isLoading} = useSpaceMembersQuery(spaceId); const removeSpaceMember = useRemoveSpaceMemberMutation(); const changeSpaceMemberRoleMutation = useChangeSpaceMemberRoleMutation(); @@ -85,99 +87,101 @@ export default function SpaceMembersList({ ), centered: true, - labels: { confirm: "Remove", cancel: "Cancel" }, - confirmProps: { color: "red" }, + labels: {confirm: "Remove", cancel: "Cancel"}, + confirmProps: {color: "red"}, onConfirm: () => onRemove(memberId, type), }); return ( <> {data && ( - - - - Member - Role - - - - - - {data?.items.map((member, index) => ( - - - - {member.type === "user" && ( - - )} - - {member.type === "group" && } - -
- - {member?.name} - - - {member.type == "user" && member?.email} - - {member.type == "group" && - `Group - ${formatMemberCount(member?.memberCount)}`} - -
-
-
- - - - handleRoleChange( - member.id, - member.type, - newRole, - member.role, - ) - } - disabled={readOnly} - /> - - - - {!readOnly && ( - - - - - - - - - - openRemoveModal(member.id, member.type) - } - > - Remove space member - - - - )} - + +
+ + + Member + Role + - ))} - -
+ + + + {data?.items.map((member, index) => ( + + + + {member.type === "user" && ( + + )} + + {member.type === "group" && } + +
+ + {member?.name} + + + {member.type == "user" && member?.email} + + {member.type == "group" && + `Group - ${formatMemberCount(member?.memberCount)}`} + +
+
+
+ + + + handleRoleChange( + member.id, + member.type, + newRole, + member.role, + ) + } + disabled={readOnly} + /> + + + + {!readOnly && ( + + + + + + + + + + openRemoveModal(member.id, member.type) + } + > + Remove space member + + + + )} + +
+ ))} +
+ + )} ); diff --git a/apps/client/src/features/space/queries/space-query.ts b/apps/client/src/features/space/queries/space-query.ts index b7c6945..e1fa7a1 100644 --- a/apps/client/src/features/space/queries/space-query.ts +++ b/apps/client/src/features/space/queries/space-query.ts @@ -36,7 +36,7 @@ export function useGetSpacesQuery( export function useSpaceQuery(spaceId: string): UseQueryResult { return useQuery({ - queryKey: ['spaces', spaceId], + queryKey: ['space', spaceId], queryFn: () => getSpaceById(spaceId), enabled: !!spaceId, staleTime: 5 * 60 * 1000, @@ -65,7 +65,7 @@ export function useGetSpaceBySlugQuery( spaceId: string ): UseQueryResult { return useQuery({ - queryKey: ['spaces', spaceId], + queryKey: ['space', spaceId], queryFn: () => getSpaceById(spaceId), enabled: !!spaceId, staleTime: 5 * 60 * 1000, @@ -111,7 +111,7 @@ export function useDeleteSpaceMutation() { if (variables.slug) { queryClient.removeQueries({ - queryKey: ['spaces', variables.slug], + queryKey: ['space', variables.slug], exact: true, }); } diff --git a/apps/client/src/features/workspace/components/members/components/workspace-invites-table.tsx b/apps/client/src/features/workspace/components/members/components/workspace-invites-table.tsx index a7fbb9c..6517cd5 100644 --- a/apps/client/src/features/workspace/components/members/components/workspace-invites-table.tsx +++ b/apps/client/src/features/workspace/components/members/components/workspace-invites-table.tsx @@ -1,62 +1,64 @@ -import { Group, Table, Avatar, Text, Alert } from "@mantine/core"; -import { useWorkspaceInvitationsQuery } from "@/features/workspace/queries/workspace-query.ts"; +import {Group, Table, Avatar, Text, Alert} from "@mantine/core"; +import {useWorkspaceInvitationsQuery} from "@/features/workspace/queries/workspace-query.ts"; import React from "react"; -import { getUserRoleLabel } from "@/features/workspace/types/user-role-data.ts"; +import {getUserRoleLabel} from "@/features/workspace/types/user-role-data.ts"; import InviteActionMenu from "@/features/workspace/components/members/components/invite-action-menu.tsx"; -import { IconInfoCircle } from "@tabler/icons-react"; -import { formattedDate } from "@/lib/time.ts"; +import {IconInfoCircle} from "@tabler/icons-react"; +import {formattedDate, timeAgo} from "@/lib/time.ts"; import useUserRole from "@/hooks/use-user-role.tsx"; export default function WorkspaceInvitesTable() { - const { data, isLoading } = useWorkspaceInvitationsQuery({ + const {data, isLoading} = useWorkspaceInvitationsQuery({ limit: 100, }); - const { isAdmin } = useUserRole(); + const {isAdmin} = useUserRole(); return ( <> - }> + }> Invited members who are yet to accept their invitation will appear here. {data && ( <> - - - - Email - Role - Date - - - - - {data?.items.map((invitation, index) => ( - - - - -
- - {invitation.email} - -
-
-
- - {getUserRoleLabel(invitation.role)} - - {formattedDate(invitation.createdAt)} - - - {isAdmin && ( - - )} - + +
+ + + Email + Role + Date - ))} - -
+ + + + {data?.items.map((invitation, index) => ( + + + + +
+ + {invitation.email} + +
+
+
+ + {getUserRoleLabel(invitation.role)} + + {timeAgo(invitation.createdAt)} + + + {isAdmin && ( + + )} + +
+ ))} +
+ + )} diff --git a/apps/client/src/features/workspace/components/members/components/workspace-members-table.tsx b/apps/client/src/features/workspace/components/members/components/workspace-members-table.tsx index c99ce6b..58f2ce8 100644 --- a/apps/client/src/features/workspace/components/members/components/workspace-members-table.tsx +++ b/apps/client/src/features/workspace/components/members/components/workspace-members-table.tsx @@ -1,9 +1,9 @@ -import { Group, Table, Text, Badge } from "@mantine/core"; +import {Group, Table, Text, Badge} from "@mantine/core"; import { useChangeMemberRoleMutation, useWorkspaceMembersQuery, } from "@/features/workspace/queries/workspace-query.ts"; -import { CustomAvatar } from "@/components/ui/custom-avatar.tsx"; +import {CustomAvatar} from "@/components/ui/custom-avatar.tsx"; import React from "react"; import RoleSelectMenu from "@/components/ui/role-select-menu.tsx"; import { @@ -11,14 +11,14 @@ import { userRoleData, } from "@/features/workspace/types/user-role-data.ts"; import useUserRole from "@/hooks/use-user-role.tsx"; -import { UserRole } from "@/lib/types.ts"; +import {UserRole} from "@/lib/types.ts"; export default function WorkspaceMembersTable() { - const { data, isLoading } = useWorkspaceMembersQuery({ limit: 100 }); + const {data, isLoading} = useWorkspaceMembersQuery({limit: 100}); const changeMemberRoleMutation = useChangeMemberRoleMutation(); - const { isAdmin, isOwner } = useUserRole(); + const {isAdmin, isOwner} = useUserRole(); - const assignableUserRoles = isOwner ? userRoleData : userRoleData.filter((role) => role.value !== UserRole.OWNER); + const assignableUserRoles = isOwner ? userRoleData : userRoleData.filter((role) => role.value !== UserRole.OWNER); const handleRoleChange = async ( userId: string, @@ -40,50 +40,52 @@ export default function WorkspaceMembersTable() { return ( <> {data && ( - - - - User - Status - Role - - - - - {data?.items.map((user, index) => ( - - - - -
- - {user.name} - - - {user.email} - -
-
-
- - - Active - - - - - handleRoleChange(user.id, user.role, newRole) - } - disabled={!isAdmin} - /> - + +
+ + + User + Status + Role - ))} - -
+ + + + {data?.items.map((user, index) => ( + + + + +
+ + {user.name} + + + {user.email} + +
+
+
+ + + Active + + + + + handleRoleChange(user.id, user.role, newRole) + } + disabled={!isAdmin} + /> + +
+ ))} +
+ + )} ); diff --git a/apps/client/src/lib/config.ts b/apps/client/src/lib/config.ts index 64c7c4e..f6ff3fb 100644 --- a/apps/client/src/lib/config.ts +++ b/apps/client/src/lib/config.ts @@ -6,6 +6,10 @@ declare global { } } +export function getAppName(): string{ + return 'Docmost'; +} + export function getAppUrl(): string { //let appUrl = window.CONFIG?.APP_URL || process.env.APP_URL; diff --git a/apps/client/src/pages/auth/forgot-password.tsx b/apps/client/src/pages/auth/forgot-password.tsx index 0872fe3..94826c1 100644 --- a/apps/client/src/pages/auth/forgot-password.tsx +++ b/apps/client/src/pages/auth/forgot-password.tsx @@ -1,11 +1,12 @@ import { ForgotPasswordForm } from "@/features/auth/components/forgot-password-form"; +import { getAppName } from "@/lib/config"; import { Helmet } from "react-helmet-async"; export default function ForgotPassword() { return ( <> - Forgot Password - Docmost + Forgot Password - {getAppName()} diff --git a/apps/client/src/pages/auth/invite-signup.tsx b/apps/client/src/pages/auth/invite-signup.tsx index c113bb9..cb0057c 100644 --- a/apps/client/src/pages/auth/invite-signup.tsx +++ b/apps/client/src/pages/auth/invite-signup.tsx @@ -1,12 +1,13 @@ import { Helmet } from "react-helmet-async"; import { InviteSignUpForm } from "@/features/auth/components/invite-sign-up-form.tsx"; +import {getAppName} from "@/lib/config.ts"; export default function InviteSignup() { return ( <> - - Invitation Signup - Docmost - + + Invitation Signuo - {getAppName()} + ); diff --git a/apps/client/src/pages/auth/login.tsx b/apps/client/src/pages/auth/login.tsx index 68ba2c7..c3f8cc9 100644 --- a/apps/client/src/pages/auth/login.tsx +++ b/apps/client/src/pages/auth/login.tsx @@ -1,11 +1,12 @@ import { LoginForm } from "@/features/auth/components/login-form"; import { Helmet } from "react-helmet-async"; +import {getAppName} from "@/lib/config.ts"; export default function LoginPage() { return ( <> - Login - Docmost + Login - {getAppName()} diff --git a/apps/client/src/pages/auth/password-reset.tsx b/apps/client/src/pages/auth/password-reset.tsx index 0f37086..a01c681 100644 --- a/apps/client/src/pages/auth/password-reset.tsx +++ b/apps/client/src/pages/auth/password-reset.tsx @@ -4,6 +4,7 @@ import { Link, useSearchParams } from "react-router-dom"; import { useVerifyUserTokenQuery } from "@/features/auth/queries/auth-query"; import { Button, Container, Group, Text } from "@mantine/core"; import APP_ROUTE from "@/lib/app-route"; +import {getAppName} from "@/lib/config.ts"; export default function PasswordReset() { const [searchParams] = useSearchParams(); @@ -21,7 +22,7 @@ export default function PasswordReset() { return ( <> - Password Reset - Docmost + Password Reset - {getAppName()} @@ -45,7 +46,7 @@ export default function PasswordReset() { return ( <> - Password Reset - Docmost + Password Reset - {getAppName()} diff --git a/apps/client/src/pages/auth/setup-workspace.tsx b/apps/client/src/pages/auth/setup-workspace.tsx index 8f1ba9b..009ae10 100644 --- a/apps/client/src/pages/auth/setup-workspace.tsx +++ b/apps/client/src/pages/auth/setup-workspace.tsx @@ -3,6 +3,7 @@ import { SetupWorkspaceForm } from "@/features/auth/components/setup-workspace-f import { Helmet } from "react-helmet-async"; import React, { useEffect } from "react"; import { useNavigate } from "react-router-dom"; +import {getAppName} from "@/lib/config.ts"; export default function SetupWorkspace() { const { @@ -32,7 +33,7 @@ export default function SetupWorkspace() { return ( <> - Setup Workspace - Docmost + Setup Workspace - {getAppName()} diff --git a/apps/client/src/pages/dashboard/home.tsx b/apps/client/src/pages/dashboard/home.tsx index fc99cb1..0a201ef 100644 --- a/apps/client/src/pages/dashboard/home.tsx +++ b/apps/client/src/pages/dashboard/home.tsx @@ -1,15 +1,22 @@ -import { Container, Space } from "@mantine/core"; +import {Container, Space} from "@mantine/core"; import HomeTabs from "@/features/home/components/home-tabs"; import SpaceGrid from "@/features/space/components/space-grid.tsx"; +import {getAppName} from "@/lib/config.ts"; +import {Helmet} from "react-helmet-async"; export default function Home() { - return ( - - + return ( + <> + + Home - {getAppName()} + + + - + - - - ); + + + + ); } diff --git a/apps/client/src/pages/settings/account/account-preferences.tsx b/apps/client/src/pages/settings/account/account-preferences.tsx index d312e18..91bad82 100644 --- a/apps/client/src/pages/settings/account/account-preferences.tsx +++ b/apps/client/src/pages/settings/account/account-preferences.tsx @@ -1,15 +1,20 @@ import SettingsTitle from "@/components/settings/settings-title.tsx"; import AccountTheme from "@/features/user/components/account-theme.tsx"; import PageWidthPref from "@/features/user/components/page-width-pref.tsx"; -import { Divider } from "@mantine/core"; +import {Divider} from "@mantine/core"; +import {getAppName} from "@/lib/config.ts"; +import {Helmet} from "react-helmet-async"; export default function AccountPreferences() { - return ( - <> - - - - - - ); + return ( + <> + + Preferences - {getAppName()} + + + + + + + ); } diff --git a/apps/client/src/pages/settings/account/account-settings.tsx b/apps/client/src/pages/settings/account/account-settings.tsx index c115d25..044adc0 100644 --- a/apps/client/src/pages/settings/account/account-settings.tsx +++ b/apps/client/src/pages/settings/account/account-settings.tsx @@ -4,10 +4,15 @@ import ChangePassword from "@/features/user/components/change-password"; import { Divider } from "@mantine/core"; import AccountAvatar from "@/features/user/components/account-avatar"; import SettingsTitle from "@/components/settings/settings-title.tsx"; +import {getAppName} from "@/lib/config.ts"; +import {Helmet} from "react-helmet-async"; export default function AccountSettings() { return ( <> + + My Profile - {getAppName()} + diff --git a/apps/client/src/pages/settings/group/group-info.tsx b/apps/client/src/pages/settings/group/group-info.tsx index 7a22e7d..e5a079a 100644 --- a/apps/client/src/pages/settings/group/group-info.tsx +++ b/apps/client/src/pages/settings/group/group-info.tsx @@ -1,13 +1,18 @@ import SettingsTitle from "@/components/settings/settings-title.tsx"; import GroupMembersList from "@/features/group/components/group-members"; import GroupDetails from "@/features/group/components/group-details"; +import {getAppName} from "@/lib/config.ts"; +import {Helmet} from "react-helmet-async"; export default function GroupInfo() { - return ( - <> - - - - - ); + return ( + <> + + Manage Group - {getAppName()} + + + + + + ); } diff --git a/apps/client/src/pages/settings/group/groups.tsx b/apps/client/src/pages/settings/group/groups.tsx index 719233f..c3fb201 100644 --- a/apps/client/src/pages/settings/group/groups.tsx +++ b/apps/client/src/pages/settings/group/groups.tsx @@ -3,12 +3,17 @@ import SettingsTitle from "@/components/settings/settings-title.tsx"; import { Group } from "@mantine/core"; import CreateGroupModal from "@/features/group/components/create-group-modal"; import useUserRole from "@/hooks/use-user-role.tsx"; +import {getAppName} from "@/lib/config.ts"; +import {Helmet} from "react-helmet-async"; export default function Groups() { const { isAdmin } = useUserRole(); return ( <> + + Groups - {getAppName()} + diff --git a/apps/client/src/pages/settings/space/spaces.tsx b/apps/client/src/pages/settings/space/spaces.tsx index 3cfc4e7..6eb2cc7 100644 --- a/apps/client/src/pages/settings/space/spaces.tsx +++ b/apps/client/src/pages/settings/space/spaces.tsx @@ -1,21 +1,26 @@ import SettingsTitle from "@/components/settings/settings-title.tsx"; import SpaceList from "@/features/space/components/space-list.tsx"; import useUserRole from "@/hooks/use-user-role.tsx"; -import { Group } from "@mantine/core"; +import {Group} from "@mantine/core"; import CreateSpaceModal from "@/features/space/components/create-space-modal.tsx"; +import {getAppName} from "@/lib/config.ts"; +import {Helmet} from "react-helmet-async"; export default function Spaces() { - const { isAdmin } = useUserRole(); + const {isAdmin} = useUserRole(); - return ( - <> - + return ( + <> + + Spaces - {getAppName()} + + - - {isAdmin && } - + + {isAdmin && } + - - - ); + + + ); } diff --git a/apps/client/src/pages/settings/workspace/workspace-members.tsx b/apps/client/src/pages/settings/workspace/workspace-members.tsx index 6ae01aa..5cdd861 100644 --- a/apps/client/src/pages/settings/workspace/workspace-members.tsx +++ b/apps/client/src/pages/settings/workspace/workspace-members.tsx @@ -1,62 +1,67 @@ import WorkspaceInviteModal from "@/features/workspace/components/members/components/workspace-invite-modal"; -import { Group, SegmentedControl, Space, Text } from "@mantine/core"; +import {Group, SegmentedControl, Space, Text} from "@mantine/core"; import WorkspaceMembersTable from "@/features/workspace/components/members/components/workspace-members-table"; import SettingsTitle from "@/components/settings/settings-title.tsx"; -import { useEffect, useState } from "react"; -import { useNavigate, useSearchParams } from "react-router-dom"; +import {useEffect, useState} from "react"; +import {useNavigate, useSearchParams} from "react-router-dom"; import WorkspaceInvitesTable from "@/features/workspace/components/members/components/workspace-invites-table.tsx"; import useUserRole from "@/hooks/use-user-role.tsx"; +import {getAppName} from "@/lib/config.ts"; +import {Helmet} from "react-helmet-async"; export default function WorkspaceMembers() { - const [segmentValue, setSegmentValue] = useState("members"); - const [searchParams] = useSearchParams(); - const { isAdmin } = useUserRole(); - const navigate = useNavigate(); + const [segmentValue, setSegmentValue] = useState("members"); + const [searchParams] = useSearchParams(); + const {isAdmin} = useUserRole(); + const navigate = useNavigate(); - useEffect(() => { - const currentTab = searchParams.get("tab"); - if (currentTab === "invites") { - setSegmentValue(currentTab); - } - }, [searchParams.get("tab")]); + useEffect(() => { + const currentTab = searchParams.get("tab"); + if (currentTab === "invites") { + setSegmentValue(currentTab); + } + }, [searchParams.get("tab")]); - const handleSegmentChange = (value: string) => { - setSegmentValue(value); - if (value === "invites") { - navigate(`?tab=${value}`); - } else { - navigate(""); - } - }; + const handleSegmentChange = (value: string) => { + setSegmentValue(value); + if (value === "invites") { + navigate(`?tab=${value}`); + } else { + navigate(""); + } + }; - return ( - <> - + return ( + <> + + Members - {getAppName()} + + - {/* */} - {/* */} + {/* */} + {/* */} - - + + - {isAdmin && } - + {isAdmin && } + - + - {segmentValue === "invites" ? ( - - ) : ( - - )} - - ); + {segmentValue === "invites" ? ( + + ) : ( + + )} + + ); } diff --git a/apps/client/src/pages/settings/workspace/workspace-settings.tsx b/apps/client/src/pages/settings/workspace/workspace-settings.tsx index 66e93eb..cdc361c 100644 --- a/apps/client/src/pages/settings/workspace/workspace-settings.tsx +++ b/apps/client/src/pages/settings/workspace/workspace-settings.tsx @@ -1,11 +1,16 @@ import SettingsTitle from "@/components/settings/settings-title.tsx"; import WorkspaceNameForm from "@/features/workspace/components/settings/components/workspace-name-form"; +import {getAppName} from "@/lib/config.ts"; +import {Helmet} from "react-helmet-async"; export default function WorkspaceSettings() { - return ( - <> - - - - ); + return ( + <> + + Workspace Settings - {getAppName()} + + + + + ); } diff --git a/apps/client/src/pages/space/space-home.tsx b/apps/client/src/pages/space/space-home.tsx index 86ffe5f..0b5622f 100644 --- a/apps/client/src/pages/space/space-home.tsx +++ b/apps/client/src/pages/space/space-home.tsx @@ -1,15 +1,22 @@ -import { Container } from "@mantine/core"; +import {Container} from "@mantine/core"; import SpaceHomeTabs from "@/features/space/components/space-home-tabs.tsx"; -import { useParams } from "react-router-dom"; -import { useGetSpaceBySlugQuery } from "@/features/space/queries/space-query.ts"; +import {useParams} from "react-router-dom"; +import {useGetSpaceBySlugQuery} from "@/features/space/queries/space-query.ts"; +import {getAppName} from "@/lib/config.ts"; +import {Helmet} from "react-helmet-async"; export default function SpaceHome() { - const { spaceSlug } = useParams(); - const { data: space } = useGetSpaceBySlugQuery(spaceSlug); + const {spaceSlug} = useParams(); + const {data: space} = useGetSpaceBySlugQuery(spaceSlug); - return ( - - {space && } - - ); + return ( + <> + + {space?.name || 'Overview'} - {getAppName()} + + + {space && } + + + ); } diff --git a/apps/client/src/pages/welcome.tsx b/apps/client/src/pages/welcome.tsx deleted file mode 100644 index 6c0d0e5..0000000 --- a/apps/client/src/pages/welcome.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { Title, Text, Stack } from '@mantine/core'; -import { ThemeToggle } from '@/components/theme-toggle'; - -export function Welcome() { - return ( - - - <Text - inherit - variant="gradient" - component="span" - gradient={{ from: 'pink', to: 'yellow' }} - > - Welcome - </Text> - - - Welcome to something new and interesting. - - - - ); -} diff --git a/apps/server/src/core/auth/dto/create-admin-user.dto.ts b/apps/server/src/core/auth/dto/create-admin-user.dto.ts index c85cd3c..fb6b4cb 100644 --- a/apps/server/src/core/auth/dto/create-admin-user.dto.ts +++ b/apps/server/src/core/auth/dto/create-admin-user.dto.ts @@ -1,15 +1,18 @@ import { IsNotEmpty, IsString, MaxLength, MinLength } from 'class-validator'; import { CreateUserDto } from './create-user.dto'; +import {Transform, TransformFnParams} from "class-transformer"; export class CreateAdminUserDto extends CreateUserDto { @IsNotEmpty() - @MinLength(3) - @MaxLength(35) + @MinLength(1) + @MaxLength(50) + @Transform(({ value }: TransformFnParams) => value?.trim()) name: string; @IsNotEmpty() - @MinLength(4) - @MaxLength(35) + @MinLength(3) + @MaxLength(50) @IsString() + @Transform(({ value }: TransformFnParams) => value?.trim()) workspaceName: string; } diff --git a/apps/server/src/core/auth/dto/create-user.dto.ts b/apps/server/src/core/auth/dto/create-user.dto.ts index 5a224c0..359fb1c 100644 --- a/apps/server/src/core/auth/dto/create-user.dto.ts +++ b/apps/server/src/core/auth/dto/create-user.dto.ts @@ -6,12 +6,14 @@ import { MaxLength, MinLength, } from 'class-validator'; +import {Transform, TransformFnParams} from "class-transformer"; export class CreateUserDto { @IsOptional() - @MinLength(2) - @MaxLength(60) + @MinLength(1) + @MaxLength(50) @IsString() + @Transform(({ value }: TransformFnParams) => value?.trim()) name: string; @IsNotEmpty() diff --git a/apps/server/src/core/auth/dto/verify-user-token.dto.ts b/apps/server/src/core/auth/dto/verify-user-token.dto.ts index f789a3d..59600c5 100644 --- a/apps/server/src/core/auth/dto/verify-user-token.dto.ts +++ b/apps/server/src/core/auth/dto/verify-user-token.dto.ts @@ -1,4 +1,4 @@ -import { IsString, MinLength } from 'class-validator'; +import { IsString } from 'class-validator'; export class VerifyUserTokenDto { @IsString() diff --git a/apps/server/src/core/group/dto/create-group.dto.ts b/apps/server/src/core/group/dto/create-group.dto.ts index c49a270..2efdad3 100644 --- a/apps/server/src/core/group/dto/create-group.dto.ts +++ b/apps/server/src/core/group/dto/create-group.dto.ts @@ -7,11 +7,13 @@ import { MaxLength, MinLength, } from 'class-validator'; +import {Transform, TransformFnParams} from "class-transformer"; export class CreateGroupDto { @MinLength(2) @MaxLength(50) @IsString() + @Transform(({ value }: TransformFnParams) => value?.trim()) name: string; @IsOptional() diff --git a/apps/server/src/core/space/dto/create-space.dto.ts b/apps/server/src/core/space/dto/create-space.dto.ts index 0977323..bd7e668 100644 --- a/apps/server/src/core/space/dto/create-space.dto.ts +++ b/apps/server/src/core/space/dto/create-space.dto.ts @@ -5,11 +5,13 @@ import { MaxLength, MinLength, } from 'class-validator'; +import {Transform, TransformFnParams} from "class-transformer"; export class CreateSpaceDto { @MinLength(2) @MaxLength(50) @IsString() + @Transform(({ value }: TransformFnParams) => value?.trim()) name: string; @IsOptional() diff --git a/apps/server/src/core/workspace/dto/create-workspace.dto.ts b/apps/server/src/core/workspace/dto/create-workspace.dto.ts index c8ba193..7ad04e3 100644 --- a/apps/server/src/core/workspace/dto/create-workspace.dto.ts +++ b/apps/server/src/core/workspace/dto/create-workspace.dto.ts @@ -1,15 +1,17 @@ -import { IsOptional, IsString, MaxLength, MinLength } from 'class-validator'; +import {IsAlphanumeric, IsOptional, IsString, MaxLength, MinLength} from 'class-validator'; +import {Transform, TransformFnParams} from "class-transformer"; export class CreateWorkspaceDto { @MinLength(4) @MaxLength(64) @IsString() + @Transform(({ value }: TransformFnParams) => value?.trim()) name: string; @IsOptional() @MinLength(4) @MaxLength(30) - @IsString() + @IsAlphanumeric() hostname?: string; @IsOptional() From d4b219d608a756ef755d1f01f4f7b50d079c6b04 Mon Sep 17 00:00:00 2001 From: James Choi Date: Mon, 14 Oct 2024 17:13:36 +0900 Subject: [PATCH 11/71] add `COPY patches` to Dockerfile (#400) --- Dockerfile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Dockerfile b/Dockerfile index c2cca54..d6c740a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -30,6 +30,9 @@ COPY --from=builder /app/packages/editor-ext/package.json /app/packages/editor-e COPY --from=builder /app/package.json /app/package.json COPY --from=builder /app/pnpm*.yaml /app/ +# Copy patches +COPY --from=builder /app/patches /app/patches + RUN npm install -g pnpm RUN chown -R node:node /app From b57be9c7366bc74ebd701846b5a187edd438e3ec Mon Sep 17 00:00:00 2001 From: Philipinho <16838612+Philipinho@users.noreply.github.com> Date: Mon, 14 Oct 2024 12:29:11 +0100 Subject: [PATCH 12/71] fix: rename edit -> save --- apps/client/src/features/group/components/edit-group-form.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/client/src/features/group/components/edit-group-form.tsx b/apps/client/src/features/group/components/edit-group-form.tsx index 62f923e..036295e 100644 --- a/apps/client/src/features/group/components/edit-group-form.tsx +++ b/apps/client/src/features/group/components/edit-group-form.tsx @@ -79,7 +79,7 @@ export function EditGroupForm({ onClose }: EditGroupFormProps) { - + From 978fadd6b90043d9cc9e1ca56b969500b4b44e73 Mon Sep 17 00:00:00 2001 From: Philip Okugbe Date: Sat, 26 Oct 2024 15:48:40 +0100 Subject: [PATCH 13/71] fix: improve sidebar page tree syncing (#407) * sync node deletion * tree sync improvements * fix cache bug * fix debounced page title * fix --- .../src/features/editor/title-editor.tsx | 11 +-- .../page/tree/components/space-tree.tsx | 2 +- .../page/tree/hooks/use-tree-mutation.ts | 49 +++++++++-- .../src/features/page/tree/utils/utils.ts | 22 +++++ .../src/features/websocket/types/types.ts | 37 ++++++++- .../websocket/use-query-subscription.ts | 11 ++- .../src/features/websocket/use-tree-socket.ts | 81 +++++++++++-------- 7 files changed, 163 insertions(+), 50 deletions(-) diff --git a/apps/client/src/features/editor/title-editor.tsx b/apps/client/src/features/editor/title-editor.tsx index 1bd4eb9..a02e597 100644 --- a/apps/client/src/features/editor/title-editor.tsx +++ b/apps/client/src/features/editor/title-editor.tsx @@ -11,7 +11,6 @@ import { titleEditorAtom, } from "@/features/editor/atoms/editor-atoms"; import { - usePageQuery, useUpdatePageMutation, } from "@/features/page/queries/page-query"; import { useDebouncedValue } from "@mantine/hooks"; @@ -21,7 +20,7 @@ import { updateTreeNodeName } from "@/features/page/tree/utils"; import { useQueryEmit } from "@/features/websocket/use-query-emit.ts"; import { History } from "@tiptap/extension-history"; import { buildPageUrl } from "@/features/page/page.utils.ts"; -import { useNavigate, useParams } from "react-router-dom"; +import { useNavigate } from "react-router-dom"; export interface TitleEditorProps { pageId: string; @@ -39,14 +38,15 @@ export function TitleEditor({ editable, }: TitleEditorProps) { const [debouncedTitleState, setDebouncedTitleState] = useState(null); - const [debouncedTitle] = useDebouncedValue(debouncedTitleState, 1000); + const [debouncedTitle] = useDebouncedValue(debouncedTitleState, 500); const updatePageMutation = useUpdatePageMutation(); const pageEditor = useAtomValue(pageEditorAtom); const [, setTitleEditor] = useAtom(titleEditorAtom); const [treeData, setTreeData] = useAtom(treeDataAtom); const emit = useQueryEmit(); - const navigate = useNavigate(); + const [activePageId, setActivePageId] = useState(pageId); + const titleEditor = useEditor({ extensions: [ @@ -74,6 +74,7 @@ export function TitleEditor({ onUpdate({ editor }) { const currentTitle = editor.getText(); setDebouncedTitleState(currentTitle); + setActivePageId(pageId); }, editable: editable, content: title, @@ -85,7 +86,7 @@ export function TitleEditor({ }, [title]); useEffect(() => { - if (debouncedTitle !== null) { + if (debouncedTitle !== null && activePageId === pageId) { updatePageMutation.mutate({ pageId: pageId, title: debouncedTitle, diff --git a/apps/client/src/features/page/tree/components/space-tree.tsx b/apps/client/src/features/page/tree/components/space-tree.tsx index f78485a..67e0d0e 100644 --- a/apps/client/src/features/page/tree/components/space-tree.tsx +++ b/apps/client/src/features/page/tree/components/space-tree.tsx @@ -207,7 +207,7 @@ export default function SpaceTree({ spaceId, readOnly }: SpaceTreeProps) { overscanCount={10} dndRootElement={rootElement.current} onToggle={() => { - setOpenTreeNodes(treeApiRef.current.openState); + setOpenTreeNodes(treeApiRef.current?.openState); }} initialOpenState={openTreeNodes} > diff --git a/apps/client/src/features/page/tree/hooks/use-tree-mutation.ts b/apps/client/src/features/page/tree/hooks/use-tree-mutation.ts index 0cc41a9..e75bf47 100644 --- a/apps/client/src/features/page/tree/hooks/use-tree-mutation.ts +++ b/apps/client/src/features/page/tree/hooks/use-tree-mutation.ts @@ -21,6 +21,7 @@ import { generateJitteredKeyBetween } from "fractional-indexing-jittered"; import { SpaceTreeNode } from "@/features/page/tree/types.ts"; import { buildPageUrl } from "@/features/page/page.utils.ts"; import { getSpaceUrl } from "@/lib/config.ts"; +import { useQueryEmit } from "@/features/websocket/use-query-emit.ts"; export function useTreeMutation(spaceId: string) { const [data, setData] = useAtom(treeDataAtom); @@ -31,6 +32,8 @@ export function useTreeMutation(spaceId: string) { const movePageMutation = useMovePageMutation(); const navigate = useNavigate(); const { spaceSlug } = useParams(); + const { pageSlug } = useParams(); + const emit = useQueryEmit(); const onCreate: CreateHandler = async ({ parentId, index, type }) => { const payload: { spaceId: string; parentPageId?: string } = { @@ -69,6 +72,17 @@ export function useTreeMutation(spaceId: string) { tree.create({ parentId, index, data }); setData(tree.data); + setTimeout(() => { + emit({ + operation: "addTreeNode", + payload: { + parentId, + index, + data + } + }); + }, 50); + const pageUrl = buildPageUrl( spaceSlug, createdPage.slugId, @@ -100,7 +114,7 @@ export function useTreeMutation(spaceId: string) { : tree.data; // if there is a parentId, tree.find(args.parentId).children returns a SimpleNode array - // we have to access the node differently viq currentTreeData[args.index]?.data?.position + // we have to access the node differently via currentTreeData[args.index]?.data?.position // this makes it possible to correctly sort children of a parent node that is not the root const afterPosition = @@ -147,11 +161,13 @@ export function useTreeMutation(spaceId: string) { if (childrenCount === 0) { tree.update({ id: previousParent.id, - changes: { ...previousParent.data, hasChildren: false } as any, + changes: { ... previousParent.data, hasChildren: false } as any, }); } } + //console.log() + setData(tree.data); const payload: IMovePage = { @@ -162,6 +178,13 @@ export function useTreeMutation(spaceId: string) { try { movePageMutation.mutateAsync(payload); + + setTimeout(() => { + emit({ + operation: "moveTreeNode", + payload: { id: draggedNodeId, parentId: args.parentId, index: args.index, position: newPosition }, + }); + }, 50); } catch (error) { console.error("Error moving page:", error); } @@ -182,12 +205,26 @@ export function useTreeMutation(spaceId: string) { try { await deletePageMutation.mutateAsync(args.ids[0]); - if (tree.find(args.ids[0])) { - tree.drop({ id: args.ids[0] }); - setData(tree.data); + const node = tree.find(args.ids[0]); + if (!node) { + return; } - navigate(getSpaceUrl(spaceSlug)); + tree.drop({ id: args.ids[0] }); + setData(tree.data); + + // navigate only if the current url is same as the deleted page + if (pageSlug && node.data.slugId === pageSlug.split('-')[1]) { + navigate(getSpaceUrl(spaceSlug)); + } + + setTimeout(() => { + emit({ + operation: "deleteTreeNode", + payload: { node: node.data } + }); + }, 50); + } catch (error) { console.error("Failed to delete page:", error); } diff --git a/apps/client/src/features/page/tree/utils/utils.ts b/apps/client/src/features/page/tree/utils/utils.ts index b00be65..0dfe8ed 100644 --- a/apps/client/src/features/page/tree/utils/utils.ts +++ b/apps/client/src/features/page/tree/utils/utils.ts @@ -100,6 +100,28 @@ export const updateTreeNodeIcon = ( }); }; +export const deleteTreeNode = ( + nodes: SpaceTreeNode[], + nodeId: string, +): SpaceTreeNode[] => { + return nodes + .map((node) => { + if (node.id === nodeId) { + return null; + } + + if (node.children && node.children.length > 0) { + return { + ...node, + children: deleteTreeNode(node.children, nodeId), + }; + } + return node; + }) + .filter((node) => node !== null); +}; + + export function buildTreeWithChildren(items: SpaceTreeNode[]): SpaceTreeNode[] { const nodeMap = {}; let result: SpaceTreeNode[] = []; diff --git a/apps/client/src/features/websocket/types/types.ts b/apps/client/src/features/websocket/types/types.ts index fc2754b..583a9d0 100644 --- a/apps/client/src/features/websocket/types/types.ts +++ b/apps/client/src/features/websocket/types/types.ts @@ -1,3 +1,5 @@ +import { SpaceTreeNode } from "@/features/page/tree/types.ts"; + export type InvalidateEvent = { operation: "invalidate"; entity: Array; @@ -11,4 +13,37 @@ export type UpdateEvent = { payload: Partial; }; -export type WebSocketEvent = InvalidateEvent | UpdateEvent; +export type DeleteEvent = { + operation: "deleteOne"; + entity: Array; + id: string; + payload?: Partial; +}; + +export type AddTreeNodeEvent = { + operation: "addTreeNode"; + payload: { + parentId: string; + index: number; + data: SpaceTreeNode; + }; +}; + +export type MoveTreeNodeEvent = { + operation: "moveTreeNode"; + payload: { + id: string; + parentId: string; + index: number; + position: string; + } +}; + +export type DeleteTreeNodeEvent = { + operation: "deleteTreeNode"; + payload: { + node: SpaceTreeNode + } +}; + +export type WebSocketEvent = InvalidateEvent | UpdateEvent | DeleteEvent | AddTreeNodeEvent | MoveTreeNodeEvent | DeleteTreeNodeEvent; diff --git a/apps/client/src/features/websocket/use-query-subscription.ts b/apps/client/src/features/websocket/use-query-subscription.ts index 1ce9f36..576b677 100644 --- a/apps/client/src/features/websocket/use-query-subscription.ts +++ b/apps/client/src/features/websocket/use-query-subscription.ts @@ -30,10 +30,13 @@ export const useQuerySubscription = () => { queryKeyId = data.id; } - queryClient.setQueryData([...data.entity, queryKeyId], { - ...queryClient.getQueryData([...data.entity, queryKeyId]), - ...data.payload, - }); + // only update if data was already in cache + if(queryClient.getQueryData([...data.entity, queryKeyId])){ + queryClient.setQueryData([...data.entity, queryKeyId], { + ...queryClient.getQueryData([...data.entity, queryKeyId]), + ...data.payload, + }); + } /* queryClient.setQueriesData( diff --git a/apps/client/src/features/websocket/use-tree-socket.ts b/apps/client/src/features/websocket/use-tree-socket.ts index 2fa4fb9..3c759a9 100644 --- a/apps/client/src/features/websocket/use-tree-socket.ts +++ b/apps/client/src/features/websocket/use-tree-socket.ts @@ -2,17 +2,15 @@ import { useEffect, useRef } from "react"; import { socketAtom } from "@/features/websocket/atoms/socket-atom.ts"; import { useAtom } from "jotai"; import { treeDataAtom } from "@/features/page/tree/atoms/tree-data-atom.ts"; -import { - updateTreeNodeIcon, - updateTreeNodeName, -} from "@/features/page/tree/utils"; import { WebSocketEvent } from "@/features/websocket/types"; import { SpaceTreeNode } from "@/features/page/tree/types.ts"; +import { useQueryClient } from "@tanstack/react-query"; +import { SimpleTree } from "react-arborist"; export const useTreeSocket = () => { const [socket] = useAtom(socketAtom); const [treeData, setTreeData] = useAtom(treeDataAtom); - + const queryClient = useQueryClient(); const initialTreeData = useRef(treeData); useEffect(() => { @@ -20,42 +18,59 @@ export const useTreeSocket = () => { }, [treeData]); useEffect(() => { - socket?.on("message", (event) => { - const data: WebSocketEvent = event; + socket?.on("message", (event: WebSocketEvent) => { const initialData = initialTreeData.current; - switch (data.operation) { - case "invalidate": - // nothing to do here - break; + const treeApi = new SimpleTree(initialData); + + switch (event.operation) { case "updateOne": - // Get the initial value of treeData - if (initialData && initialData.length > 0) { - let newTreeData: SpaceTreeNode[]; - - if (data.entity[0] === "pages") { - if (data.payload?.title !== undefined) { - newTreeData = updateTreeNodeName( - initialData, - data.id, - data.payload.title, - ); + if (event.entity[0] === "pages") { + if (treeApi.find(event.id)) { + if (event.payload?.title) { + treeApi.update({ id: event.id, changes: { name: event.payload.title } }); } - - if (data.payload?.icon !== undefined) { - newTreeData = updateTreeNodeIcon( - initialData, - data.id, - data.payload.icon, - ); - } - - if (newTreeData && newTreeData.length > 0) { - setTreeData(newTreeData); + if (event.payload?.icon) { + treeApi.update({ id: event.id, changes: { icon: event.payload.icon } }); } + setTreeData(treeApi.data); } } break; + case 'addTreeNode': + if (treeApi.find(event.payload.data.id)) return; + + treeApi.create({ parentId: event.payload.parentId, index: event.payload.index, data: event.payload.data }); + setTreeData(treeApi.data); + + break; + case 'moveTreeNode': + // move node + treeApi.move({ + id: event.payload.id, + parentId: event.payload.parentId, + index: event.payload.index + }); + + // update node position + treeApi.update({ + id: event.payload.id, + changes: { + position: event.payload.position, + } + }); + + setTreeData(treeApi.data); + + break; + case "deleteTreeNode": + treeApi.drop({ id: event.payload.node.id }); + setTreeData(treeApi.data); + + queryClient.invalidateQueries({ + queryKey: ['pages', event.payload.node.slugId].filter(Boolean), + }); + break; } }); }, [socket]); From ab70cee278862dd8e4e64bcc7777af7dd34208ed Mon Sep 17 00:00:00 2001 From: Philip Okugbe Date: Tue, 29 Oct 2024 18:13:20 +0000 Subject: [PATCH 14/71] feat: third-party embeds (#423) * wip * Add more providers * icons * unify embed providers (Youtube) * fix case * YT music * remove redundant code --- apps/client/package.json | 2 +- .../src/components/icons/airtable-icon.tsx | 32 +++++ .../src/components/icons/figma-icon.tsx | 23 ++++ .../src/components/icons/framer-icon.tsx | 17 +++ .../components/icons/google-drive-icon.tsx | 24 ++++ apps/client/src/components/icons/index.ts | 10 ++ .../client/src/components/icons/loom-icon.tsx | 19 +++ .../client/src/components/icons/miro-icon.tsx | 18 +++ .../src/components/icons/typeform-icon.tsx | 18 +++ .../src/components/icons/vimeo-icon.tsx | 19 +++ .../src/components/icons/youtube-icon.tsx | 19 +++ .../editor/components/embed/embed-view.tsx | 111 ++++++++++++++++ .../editor/components/embed/providers.ts | 109 ++++++++++++++++ .../components/slash-menu/menu-items.ts | 95 +++++++++++++- .../features/editor/extensions/extensions.ts | 6 + apps/client/src/lib/utils.ts | 4 + .../src/collaboration/collaboration.util.ts | 30 +++-- packages/editor-ext/src/index.ts | 1 + packages/editor-ext/src/lib/embed.ts | 122 ++++++++++++++++++ packages/editor-ext/src/lib/link.ts | 19 --- pnpm-lock.yaml | 10 +- 21 files changed, 667 insertions(+), 41 deletions(-) create mode 100644 apps/client/src/components/icons/airtable-icon.tsx create mode 100644 apps/client/src/components/icons/figma-icon.tsx create mode 100644 apps/client/src/components/icons/framer-icon.tsx create mode 100644 apps/client/src/components/icons/google-drive-icon.tsx create mode 100644 apps/client/src/components/icons/index.ts create mode 100644 apps/client/src/components/icons/loom-icon.tsx create mode 100644 apps/client/src/components/icons/miro-icon.tsx create mode 100644 apps/client/src/components/icons/typeform-icon.tsx create mode 100644 apps/client/src/components/icons/vimeo-icon.tsx create mode 100644 apps/client/src/components/icons/youtube-icon.tsx create mode 100644 apps/client/src/features/editor/components/embed/embed-view.tsx create mode 100644 apps/client/src/features/editor/components/embed/providers.ts create mode 100644 packages/editor-ext/src/lib/embed.ts diff --git a/apps/client/package.json b/apps/client/package.json index f849241..c5f6ece 100644 --- a/apps/client/package.json +++ b/apps/client/package.json @@ -9,9 +9,9 @@ "preview": "vite preview" }, "dependencies": { - "@docmost/editor-ext": "workspace:*", "@casl/ability": "^6.7.1", "@casl/react": "^4.0.0", + "@docmost/editor-ext": "workspace:*", "@emoji-mart/data": "^1.2.1", "@emoji-mart/react": "^1.1.1", "@excalidraw/excalidraw": "^0.17.6", diff --git a/apps/client/src/components/icons/airtable-icon.tsx b/apps/client/src/components/icons/airtable-icon.tsx new file mode 100644 index 0000000..8ea0473 --- /dev/null +++ b/apps/client/src/components/icons/airtable-icon.tsx @@ -0,0 +1,32 @@ +import { rem } from '@mantine/core'; + +interface Props { + size?: number | string; +} + +export function AirtableIcon({ size }: Props) { + return ( + + + + + + + ); +} diff --git a/apps/client/src/components/icons/figma-icon.tsx b/apps/client/src/components/icons/figma-icon.tsx new file mode 100644 index 0000000..2ddd1ce --- /dev/null +++ b/apps/client/src/components/icons/figma-icon.tsx @@ -0,0 +1,23 @@ +import { rem } from '@mantine/core'; + +interface Props { + size?: number | string; +} + +export function FigmaIcon({ size }: Props) { + return ( + + + + + + + + + + ); +} diff --git a/apps/client/src/components/icons/framer-icon.tsx b/apps/client/src/components/icons/framer-icon.tsx new file mode 100644 index 0000000..4b76a0f --- /dev/null +++ b/apps/client/src/components/icons/framer-icon.tsx @@ -0,0 +1,17 @@ +import { rem } from '@mantine/core'; + +interface Props { + size?: number | string; +} + +export function FramerIcon({ size }: Props) { + return ( + + + + ); +} diff --git a/apps/client/src/components/icons/google-drive-icon.tsx b/apps/client/src/components/icons/google-drive-icon.tsx new file mode 100644 index 0000000..f19ceca --- /dev/null +++ b/apps/client/src/components/icons/google-drive-icon.tsx @@ -0,0 +1,24 @@ +import { rem } from '@mantine/core'; + +interface Props { + size?: number | string; +} + +export function GoogleDriveIcon({ size }: Props) { + return ( + + + + + + + + + ); +} diff --git a/apps/client/src/components/icons/index.ts b/apps/client/src/components/icons/index.ts new file mode 100644 index 0000000..089bf20 --- /dev/null +++ b/apps/client/src/components/icons/index.ts @@ -0,0 +1,10 @@ +export { AirtableIcon } from "./airtable-icon.tsx"; +export { FigmaIcon } from "./figma-icon.tsx"; +export { TypeformIcon } from "./typeform-icon.tsx"; +export { VimeoIcon } from "./vimeo-icon.tsx"; +export { MiroIcon } from "./miro-icon.tsx"; +export { GoogleDriveIcon } from "./google-drive-icon.tsx"; +export { FramerIcon } from "./framer-icon.tsx"; +export { LoomIcon } from "./loom-icon.tsx"; +export { YoutubeIcon } from "./youtube-icon.tsx"; + diff --git a/apps/client/src/components/icons/loom-icon.tsx b/apps/client/src/components/icons/loom-icon.tsx new file mode 100644 index 0000000..9899d70 --- /dev/null +++ b/apps/client/src/components/icons/loom-icon.tsx @@ -0,0 +1,19 @@ +import { rem } from '@mantine/core'; + +interface Props { + size?: number | string; +} + +export function LoomIcon({ size }: Props) { + return ( + + + + ); +} diff --git a/apps/client/src/components/icons/miro-icon.tsx b/apps/client/src/components/icons/miro-icon.tsx new file mode 100644 index 0000000..9d07898 --- /dev/null +++ b/apps/client/src/components/icons/miro-icon.tsx @@ -0,0 +1,18 @@ +import { rem } from '@mantine/core'; + +interface Props { + size?: number | string; +} + +export function MiroIcon({ size }: Props) { + return ( + + + + ); +} diff --git a/apps/client/src/components/icons/typeform-icon.tsx b/apps/client/src/components/icons/typeform-icon.tsx new file mode 100644 index 0000000..9d4b20a --- /dev/null +++ b/apps/client/src/components/icons/typeform-icon.tsx @@ -0,0 +1,18 @@ +import { rem } from '@mantine/core'; + +interface Props { + size?: number | string; +} + +export function TypeformIcon({ size }: Props) { + return ( + + + + ); +} diff --git a/apps/client/src/components/icons/vimeo-icon.tsx b/apps/client/src/components/icons/vimeo-icon.tsx new file mode 100644 index 0000000..a9c5c53 --- /dev/null +++ b/apps/client/src/components/icons/vimeo-icon.tsx @@ -0,0 +1,19 @@ +import { rem } from '@mantine/core'; + +interface Props { + size?: number | string; +} + +export function VimeoIcon({ size }: Props) { + return ( + + + + ); +} diff --git a/apps/client/src/components/icons/youtube-icon.tsx b/apps/client/src/components/icons/youtube-icon.tsx new file mode 100644 index 0000000..2011191 --- /dev/null +++ b/apps/client/src/components/icons/youtube-icon.tsx @@ -0,0 +1,19 @@ +import { rem } from '@mantine/core'; + +interface Props { + size?: number | string; +} + +export function YoutubeIcon({ size }: Props) { + return ( + + + + ); +} diff --git a/apps/client/src/features/editor/components/embed/embed-view.tsx b/apps/client/src/features/editor/components/embed/embed-view.tsx new file mode 100644 index 0000000..57b8de0 --- /dev/null +++ b/apps/client/src/features/editor/components/embed/embed-view.tsx @@ -0,0 +1,111 @@ +import { NodeViewProps, NodeViewWrapper } from "@tiptap/react"; +import { useMemo } from "react"; +import clsx from "clsx"; +import { ActionIcon, AspectRatio, Button, Card, FocusTrap, Group, Popover, Text, TextInput } from "@mantine/core"; +import { IconEdit } from "@tabler/icons-react"; +import { z } from "zod"; +import { useForm, zodResolver } from "@mantine/form"; +import { + getEmbedProviderById, + getEmbedUrlAndProvider +} from "@/features/editor/components/embed/providers.ts"; +import { notifications } from '@mantine/notifications'; + +const schema = z.object({ + url: z + .string().trim().url({ message: 'please enter a valid url' }), +}); + +export default function EmbedView(props: NodeViewProps) { + const { node, selected, updateAttributes } = props; + const { src, provider } = node.attrs; + + const embedUrl = useMemo(() => { + if (src) { + return getEmbedUrlAndProvider(src).embedUrl; + } + return null; + }, [src]); + + const embedForm = useForm<{ url: string }>({ + initialValues: { + url: "", + }, + validate: zodResolver(schema), + }); + + async function onSubmit(data: { url: string }) { + if (provider) { + const embedProvider = getEmbedProviderById(provider); + if (embedProvider.regex.test(data.url)) { + updateAttributes({ src: data.url }); + } else { + notifications.show({ + message: `Invalid ${provider} embed link`, + position: 'top-right', + color: 'red' + }); + } + } + } + + return ( + + {embedUrl ? ( + <> + + + + + + ) : ( + + + +
+ + + + + + Embed {getEmbedProviderById(provider).name} + +
+
+
+ +
+ + + + + + + +
+
+
+ )} +
+ ); +} diff --git a/apps/client/src/features/editor/components/embed/providers.ts b/apps/client/src/features/editor/components/embed/providers.ts new file mode 100644 index 0000000..6d92850 --- /dev/null +++ b/apps/client/src/features/editor/components/embed/providers.ts @@ -0,0 +1,109 @@ +export interface IEmbedProvider { + id: string; + name: string; + regex: RegExp; + getEmbedUrl: (match: RegExpMatchArray, url?: string) => string; +} + +export const embedProviders: IEmbedProvider[] = [ + { + id: 'loom', + name: 'Loom', + regex: /^https?:\/\/(?:www\.)?loom\.com\/(?:share|embed)\/([\da-zA-Z]+)\/?/, + getEmbedUrl: (match) => { + return `https://loom.com/embed/${match[1]}`; + } + }, + { + id: 'airtable', + name: 'Airtable', + regex: /^https:\/\/(www.)?airtable.com\/([a-zA-Z0-9]{2,})\/.*/, + getEmbedUrl: (match, url: string) => { + const path = url.split('airtable.com/'); + return `https://airtable.com/embed/${path[1]}`; + } + }, + { + id: 'figma', + name: 'Figma', + regex: /^https:\/\/[\w\.-]+\.?figma.com\/(file|proto|board|design|slides|deck)\/([0-9a-zA-Z]{22,128})/, + getEmbedUrl: (match, url: string) => { + return `https://www.figma.com/embed?url=${url}&embed_host=docmost`; + } + }, + { + 'id': 'typeform', + name: 'Typeform', + regex: /^(https?:)?(\/\/)?[\w\.]+\.typeform\.com\/to\/.+/, + getEmbedUrl: (match, url: string) => { + return url; + } + }, + { + id: 'miro', + name: 'Miro', + regex: /^https:\/\/(www\.)?miro\.com\/app\/board\/([\w-]+=)/, + getEmbedUrl: (match) => { + return `https://miro.com/app/live-embed/${match[2]}?embedMode=view_only_without_ui&autoplay=true&embedSource=docmost`; + } + }, + { + id: 'youtube', + name: 'YouTube', + regex: /^((?:https?:)?\/\/)?((?:www|m|music)\.)?((?:youtube\.com|youtu.be))(\/(?:[\w\-]+\?v=|embed\/|v\/)?)([\w\-]+)(\S+)?$/, + getEmbedUrl: (match) => { + return `https://www.youtube-nocookie.com/embed/${match[5]}`; + } + }, + { + id: 'vimeo', + name: 'Vimeo', + regex: /^(https:)?\/\/(?:www\.|player\.)?vimeo.com\/(?:channels\/(?:\w+\/)?|groups\/([^/]*)\/videos\/|album\/(\d+)\/video\/|video\/|)(\d+)/, + getEmbedUrl: (match) => { + return `https://player.vimeo.com/video/${match[4]}`; + } + }, + { + id: 'framer', + name: 'Framer', + regex: /^https:\/\/(www\.)?framer\.com\/embed\/([\w-]+)/, + getEmbedUrl: (match, url: string) => { + return url; + } + }, + { + id: 'gdrive', + name: 'Google Drive', + regex: /^((?:https?:)?\/\/)?((?:www|m)\.)?(drive\.google\.com)\/file\/d\/([a-zA-Z0-9_-]+)\/.*$/, + getEmbedUrl: (match) => { + return `https://drive.google.com/file/d/${match[4]}/preview`; + } + }, +]; + +export function getEmbedProviderById(id: string) { + return embedProviders.find(provider => provider.id.toLowerCase() === id.toLowerCase()); +} + +export interface IEmbedResult { + embedUrl: string; + provider: string; +} + +export function getEmbedUrlAndProvider(url: string): IEmbedResult { + for (const provider of embedProviders) { + const match = url.match(provider.regex); + if (match) { + return { + embedUrl: provider.getEmbedUrl(match, url), + provider: provider.name.toLowerCase() + }; + } + } + return { + embedUrl: url, + provider: 'iframe', + }; +} + + diff --git a/apps/client/src/features/editor/components/slash-menu/menu-items.ts b/apps/client/src/features/editor/components/slash-menu/menu-items.ts index 00d993d..c0a2b34 100644 --- a/apps/client/src/features/editor/components/slash-menu/menu-items.ts +++ b/apps/client/src/features/editor/components/slash-menu/menu-items.ts @@ -29,6 +29,16 @@ import { uploadAttachmentAction } from "@/features/editor/components/attachment/ import IconExcalidraw from "@/components/icons/icon-excalidraw"; import IconMermaid from "@/components/icons/icon-mermaid"; import IconDrawio from "@/components/icons/icon-drawio"; +import { + AirtableIcon, + FigmaIcon, + FramerIcon, + GoogleDriveIcon, + LoomIcon, + MiroIcon, + TypeformIcon, + VimeoIcon, YoutubeIcon +} from "@/components/icons"; const CommandGroups: SlashMenuGroupedItemsType = { basic: [ @@ -343,7 +353,7 @@ const CommandGroups: SlashMenuGroupedItemsType = { day: "numeric", }); - return editor + editor .chain() .focus() .deleteRange(range) @@ -351,6 +361,87 @@ const CommandGroups: SlashMenuGroupedItemsType = { .run(); }, }, + { + title: "Airtable", + description: "Embed Airtable", + searchTerms: ["airtable"], + icon: AirtableIcon, + command: ({ editor, range }: CommandProps) => { + editor.chain().focus().deleteRange(range).setEmbed({ provider: 'airtable' }).run(); + }, + }, + { + title: "Loom", + description: "Embed Loom video", + searchTerms: ["loom"], + icon: LoomIcon, + command: ({ editor, range }: CommandProps) => { + editor.chain().focus().deleteRange(range).setEmbed({ provider: 'loom' }).run(); + }, + }, + { + title: "Figma", + description: "Embed Figma files", + searchTerms: ["figma"], + icon: FigmaIcon, + command: ({ editor, range }: CommandProps) => { + editor.chain().focus().deleteRange(range).setEmbed({ provider: 'figma' }).run(); + }, + }, + { + title: "Typeform", + description: "Embed Typeform", + searchTerms: ["typeform"], + icon: TypeformIcon, + command: ({ editor, range }: CommandProps) => { + editor.chain().focus().deleteRange(range).setEmbed({ provider: 'typeform' }).run(); + }, + }, + { + title: "Miro", + description: "Embed Miro board", + searchTerms: ["miro"], + icon: MiroIcon, + command: ({ editor, range }: CommandProps) => { + editor.chain().focus().deleteRange(range).setEmbed({ provider: 'miro' }).run(); + }, + }, + { + title: "YouTube", + description: "Embed YouTube video", + searchTerms: ["youtube", "yt"], + icon: YoutubeIcon, + command: ({ editor, range }: CommandProps) => { + editor.chain().focus().deleteRange(range).setEmbed({ provider: 'youtube' }).run(); + }, + }, + { + title: "Vimeo", + description: "Embed Vimeo video", + searchTerms: ["vimeo"], + icon: VimeoIcon, + command: ({ editor, range }: CommandProps) => { + editor.chain().focus().deleteRange(range).setEmbed({ provider: 'vimeo' }).run(); + }, + }, + { + title: "Framer", + description: "Embed Framer prototype", + searchTerms: ["framer"], + icon: FramerIcon, + command: ({ editor, range }: CommandProps) => { + editor.chain().focus().deleteRange(range).setEmbed({ provider: 'framer' }).run(); + }, + }, + { + title: "Google Drive", + description: "Embed Google Drive content", + searchTerms: ["google drive", "gdrive"], + icon: GoogleDriveIcon, + command: ({ editor, range }: CommandProps) => { + editor.chain().focus().deleteRange(range).setEmbed({ provider: 'gdrive' }).run(); + }, + }, ], }; @@ -362,7 +453,7 @@ export const getSuggestionItems = ({ const search = query.toLowerCase(); const filteredGroups: SlashMenuGroupedItemsType = {}; - const fuzzyMatch = (query, target) => { + const fuzzyMatch = (query: string, target: string) => { let queryIndex = 0; target = target.toLowerCase(); for (let char of target) { diff --git a/apps/client/src/features/editor/extensions/extensions.ts b/apps/client/src/features/editor/extensions/extensions.ts index 3399873..573902d 100644 --- a/apps/client/src/features/editor/extensions/extensions.ts +++ b/apps/client/src/features/editor/extensions/extensions.ts @@ -35,6 +35,7 @@ import { CustomCodeBlock, Drawio, Excalidraw, + Embed } from "@docmost/editor-ext"; import { randomElement, @@ -53,6 +54,7 @@ import AttachmentView from "@/features/editor/components/attachment/attachment-v import CodeBlockView from "@/features/editor/components/code-block/code-block-view.tsx"; import DrawioView from "../components/drawio/drawio-view"; import ExcalidrawView from "@/features/editor/components/excalidraw/excalidraw-view.tsx"; +import EmbedView from "@/features/editor/components/embed/embed-view.tsx"; import plaintext from "highlight.js/lib/languages/plaintext"; import powershell from "highlight.js/lib/languages/powershell"; import elixir from "highlight.js/lib/languages/elixir"; @@ -149,6 +151,7 @@ export const mainExtensions = [ DetailsSummary, DetailsContent, Youtube.configure({ + addPasteHandler: false, controls: true, nocookie: true, }), @@ -179,6 +182,9 @@ export const mainExtensions = [ Excalidraw.configure({ view: ExcalidrawView, }), + Embed.configure({ + view: EmbedView, + }) ] as any; type CollabExtensions = (provider: HocuspocusProvider, user: IUser) => any[]; diff --git a/apps/client/src/lib/utils.ts b/apps/client/src/lib/utils.ts index 9ce7d35..077c9d4 100644 --- a/apps/client/src/lib/utils.ts +++ b/apps/client/src/lib/utils.ts @@ -71,3 +71,7 @@ export function decodeBase64ToSvgString(base64Data: string): string { return decodeBase64(base64Data); } + +export function capitalizeFirstChar(string: string) { + return string.charAt(0).toUpperCase() + string.slice(1); +} \ No newline at end of file diff --git a/apps/server/src/collaboration/collaboration.util.ts b/apps/server/src/collaboration/collaboration.util.ts index bb956d9..0ceafe8 100644 --- a/apps/server/src/collaboration/collaboration.util.ts +++ b/apps/server/src/collaboration/collaboration.util.ts @@ -1,15 +1,15 @@ -import { StarterKit } from '@tiptap/starter-kit'; -import { TextAlign } from '@tiptap/extension-text-align'; -import { TaskList } from '@tiptap/extension-task-list'; -import { TaskItem } from '@tiptap/extension-task-item'; -import { Underline } from '@tiptap/extension-underline'; -import { Superscript } from '@tiptap/extension-superscript'; +import {StarterKit} from '@tiptap/starter-kit'; +import {TextAlign} from '@tiptap/extension-text-align'; +import {TaskList} from '@tiptap/extension-task-list'; +import {TaskItem} from '@tiptap/extension-task-item'; +import {Underline} from '@tiptap/extension-underline'; +import {Superscript} from '@tiptap/extension-superscript'; import SubScript from '@tiptap/extension-subscript'; -import { Highlight } from '@tiptap/extension-highlight'; -import { Typography } from '@tiptap/extension-typography'; -import { TextStyle } from '@tiptap/extension-text-style'; -import { Color } from '@tiptap/extension-color'; -import { Youtube } from '@tiptap/extension-youtube'; +import {Highlight} from '@tiptap/extension-highlight'; +import {Typography} from '@tiptap/extension-typography'; +import {TextStyle} from '@tiptap/extension-text-style'; +import {Color} from '@tiptap/extension-color'; +import {Youtube} from '@tiptap/extension-youtube'; import Table from '@tiptap/extension-table'; import TableHeader from '@tiptap/extension-table-header'; import { @@ -30,13 +30,14 @@ import { Attachment, Drawio, Excalidraw, + Embed } from '@docmost/editor-ext'; -import { generateText, JSONContent } from '@tiptap/core'; -import { generateHTML } from '../common/helpers/prosemirror/html'; +import {generateText, JSONContent} from '@tiptap/core'; +import {generateHTML} from '../common/helpers/prosemirror/html'; // @tiptap/html library works best for generating prosemirror json state but not HTML // see: https://github.com/ueberdosis/tiptap/issues/5352 // see:https://github.com/ueberdosis/tiptap/issues/4089 -import { generateJSON } from '@tiptap/html'; +import {generateJSON} from '@tiptap/html'; export const tiptapExtensions = [ StarterKit.configure({ @@ -72,6 +73,7 @@ export const tiptapExtensions = [ CustomCodeBlock, Drawio, Excalidraw, + Embed ] as any; export function jsonToHtml(tiptapJson: any) { diff --git a/packages/editor-ext/src/index.ts b/packages/editor-ext/src/index.ts index 80e2035..a9095ce 100644 --- a/packages/editor-ext/src/index.ts +++ b/packages/editor-ext/src/index.ts @@ -14,4 +14,5 @@ export * from "./lib/attachment"; export * from "./lib/custom-code-block" export * from "./lib/drawio"; export * from "./lib/excalidraw"; +export * from "./lib/embed"; diff --git a/packages/editor-ext/src/lib/embed.ts b/packages/editor-ext/src/lib/embed.ts new file mode 100644 index 0000000..04181fa --- /dev/null +++ b/packages/editor-ext/src/lib/embed.ts @@ -0,0 +1,122 @@ +import { Node, mergeAttributes } from '@tiptap/core'; +import { ReactNodeViewRenderer } from '@tiptap/react'; + +export interface EmbedOptions { + HTMLAttributes: Record; + view: any; +} +export interface EmbedAttributes { + src?: string; + provider: string; + align?: string; + width?: number; + height?: number; +} + +declare module '@tiptap/core' { + interface Commands { + embeds: { + setEmbed: (attributes?: EmbedAttributes) => ReturnType; + }; + } +} + +export const Embed = Node.create({ + name: 'embed', + inline: false, + group: 'block', + isolating: true, + atom: true, + defining: true, + draggable: true, + + addOptions() { + return { + HTMLAttributes: {}, + view: null, + }; + }, + addAttributes() { + return { + src: { + default: '', + parseHTML: (element) => element.getAttribute('data-src'), + renderHTML: (attributes: EmbedAttributes) => ({ + 'data-src': attributes.src, + }), + }, + provider: { + default: '', + parseHTML: (element) => element.getAttribute('data-provider'), + renderHTML: (attributes: EmbedAttributes) => ({ + 'data-provider': attributes.provider, + }), + }, + align: { + default: 'center', + parseHTML: (element) => element.getAttribute('data-align'), + renderHTML: (attributes: EmbedAttributes) => ({ + 'data-align': attributes.align, + }), + }, + width: { + default: 640, + parseHTML: (element) => element.getAttribute('data-width'), + renderHTML: (attributes: EmbedAttributes) => ({ + 'data-width': attributes.width, + }), + }, + height: { + default: 480, + parseHTML: (element) => element.getAttribute('data-height'), + renderHTML: (attributes: EmbedAttributes) => ({ + 'data-height': attributes.height, + }), + }, + }; + }, + + parseHTML() { + return [ + { + tag: `div[data-type="${this.name}"]`, + }, + ]; + }, + + renderHTML({ HTMLAttributes }) { + return [ + "div", + mergeAttributes( + { "data-type": this.name }, + this.options.HTMLAttributes, + HTMLAttributes, + ), + [ + "a", + { + href: HTMLAttributes["data-src"], + target: "blank", + }, + `${HTMLAttributes["data-src"]}`, + ], + ]; + }, + + addCommands() { + return { + setEmbed: + (attrs: EmbedAttributes) => + ({ commands }) => { + return commands.insertContent({ + type: 'embed', + attrs: attrs, + }); + }, + }; + }, + + addNodeView() { + return ReactNodeViewRenderer(this.options.view); + }, +}); diff --git a/packages/editor-ext/src/lib/link.ts b/packages/editor-ext/src/lib/link.ts index 7aa4aac..6fe3e5a 100644 --- a/packages/editor-ext/src/lib/link.ts +++ b/packages/editor-ext/src/lib/link.ts @@ -1,4 +1,3 @@ -import { mergeAttributes } from "@tiptap/core"; import TiptapLink from "@tiptap/extension-link"; import { Plugin } from "@tiptap/pm/state"; import { EditorView } from "@tiptap/pm/view"; @@ -6,24 +5,6 @@ import { EditorView } from "@tiptap/pm/view"; export const LinkExtension = TiptapLink.extend({ inclusive: false, - parseHTML() { - return [ - { - tag: 'a[href]:not([data-type="button"]):not([href *= "javascript:" i])', - }, - ]; - }, - - renderHTML({ HTMLAttributes }) { - return [ - "a", - mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, { - class: "link", - }), - 0, - ]; - }, - addProseMirrorPlugins() { const { editor } = this; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ab9aa93..b201c7e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3480,10 +3480,10 @@ packages: peerDependencies: '@tiptap/core': ^2.6.6 - '@tiptap/extension-paragraph@2.6.6': - resolution: {integrity: sha512-fD/onCr16UQWx+/xEmuFC2MccZZ7J5u4YaENh8LMnAnBXf78iwU7CAcmuc9rfAEO3qiLoYGXgLKiHlh2ZfD4wA==} + '@tiptap/extension-paragraph@2.8.0': + resolution: {integrity: sha512-XgxxNNbuBF48rAGwv7/s6as92/xjm/lTZIGTq9aG13ClUKFtgdel7C33SpUCcxg3cO2WkEyllXVyKUiauFZw/A==} peerDependencies: - '@tiptap/core': ^2.6.6 + '@tiptap/core': ^2.7.0 '@tiptap/extension-placeholder@2.6.6': resolution: {integrity: sha512-J0ZMvF93NsRrt+R7IQ3GhxNq32vq+88g25oV/YFJiwvC48HMu1tQB6kG1I3LJpu5b8lN+LnfANNqDOEhiBfjaA==} @@ -11543,7 +11543,7 @@ snapshots: dependencies: '@tiptap/core': 2.6.6(@tiptap/pm@2.6.6) - '@tiptap/extension-paragraph@2.6.6(@tiptap/core@2.6.6(@tiptap/pm@2.6.6))': + '@tiptap/extension-paragraph@2.8.0(@tiptap/core@2.6.6(@tiptap/pm@2.6.6))': dependencies: '@tiptap/core': 2.6.6(@tiptap/pm@2.6.6) @@ -11670,7 +11670,7 @@ snapshots: '@tiptap/extension-italic': 2.6.6(@tiptap/core@2.6.6(@tiptap/pm@2.6.6)) '@tiptap/extension-list-item': 2.6.6(@tiptap/core@2.6.6(@tiptap/pm@2.6.6)) '@tiptap/extension-ordered-list': 2.6.6(@tiptap/core@2.6.6(@tiptap/pm@2.6.6)) - '@tiptap/extension-paragraph': 2.6.6(@tiptap/core@2.6.6(@tiptap/pm@2.6.6)) + '@tiptap/extension-paragraph': 2.8.0(@tiptap/core@2.6.6(@tiptap/pm@2.6.6)) '@tiptap/extension-strike': 2.6.6(@tiptap/core@2.6.6(@tiptap/pm@2.6.6)) '@tiptap/extension-text': 2.6.6(@tiptap/core@2.6.6(@tiptap/pm@2.6.6)) '@tiptap/pm': 2.6.6 From a574d13f43ac58e0094096f14b2b476fc50bc2c2 Mon Sep 17 00:00:00 2001 From: Philipinho <16838612+Philipinho@users.noreply.github.com> Date: Tue, 29 Oct 2024 18:44:59 +0000 Subject: [PATCH 15/71] fix: email overflow --- apps/client/src/components/layouts/global/top-menu.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/client/src/components/layouts/global/top-menu.tsx b/apps/client/src/components/layouts/global/top-menu.tsx index cd7527c..2f7a0de 100644 --- a/apps/client/src/components/layouts/global/top-menu.tsx +++ b/apps/client/src/components/layouts/global/top-menu.tsx @@ -73,11 +73,11 @@ export default function TopMenu() { name={user.name} /> -
+
{user.name} - + {user.email}
From ba32e42ece50bf0d528ed3ea90e4c7ab0892fb4c Mon Sep 17 00:00:00 2001 From: Philipinho <16838612+Philipinho@users.noreply.github.com> Date: Tue, 29 Oct 2024 19:15:26 +0000 Subject: [PATCH 16/71] fix: filter out redundant group --- .../group/components/multi-group-select.tsx | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/apps/client/src/features/group/components/multi-group-select.tsx b/apps/client/src/features/group/components/multi-group-select.tsx index bb70a28..8e7e4af 100644 --- a/apps/client/src/features/group/components/multi-group-select.tsx +++ b/apps/client/src/features/group/components/multi-group-select.tsx @@ -39,21 +39,23 @@ export function MultiGroupSelect({ useEffect(() => { if (groups) { - const groupsData = groups?.items.map((group: IGroup) => { - return { - value: group.id, - label: group.name, - }; - }); + const groupsData = groups?.items + .filter((group: IGroup) => group.name.toLowerCase() !== 'everyone') + .map((group: IGroup) => { + return { + value: group.id, + label: group.name, + }; + }); - // Filter out existing users by their ids + // Filter out existing groups by their ids const filteredGroupData = groupsData.filter( - (user) => - !data.find((existingUser) => existingUser.value === user.value), + (group) => + !data.find((existingGroup) => existingGroup.value === group.value), ); // Combine existing data with new search data - setData((prevData) => [...prevData, ...filteredGroupData]); + setData((prevData) => [... prevData, ... filteredGroupData]); } }, [groups]); From 31feb38def78fc4ef5d7610fed804faa3f57805a Mon Sep 17 00:00:00 2001 From: Philipinho <16838612+Philipinho@users.noreply.github.com> Date: Tue, 29 Oct 2024 19:33:08 +0000 Subject: [PATCH 17/71] fix: sync color scheme with excalidraw --- .../editor/components/excalidraw/excalidraw-view.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/client/src/features/editor/components/excalidraw/excalidraw-view.tsx b/apps/client/src/features/editor/components/excalidraw/excalidraw-view.tsx index b32b702..f5e5b17 100644 --- a/apps/client/src/features/editor/components/excalidraw/excalidraw-view.tsx +++ b/apps/client/src/features/editor/components/excalidraw/excalidraw-view.tsx @@ -73,7 +73,7 @@ export default function ExcalidrawView(props: NodeViewProps) { elements: excalidrawAPI?.getSceneElements(), appState: { exportEmbedScene: true, - exportWithDarkMode: computedColorScheme == 'light' ? false : true, + exportWithDarkMode: computedColorScheme != 'light', }, files: excalidrawAPI?.getFiles(), }); @@ -147,6 +147,7 @@ export default function ExcalidrawView(props: NodeViewProps) { ...excalidrawData, scrollToContent: true, }} + theme={computedColorScheme} />
@@ -202,7 +203,7 @@ export default function ExcalidrawView(props: NodeViewProps) { - Double-click to edit excalidraw diagram + Double-click to edit Excalidraw diagram
From a52c86a180f7c712716ef74fa1de517fc7bd2875 Mon Sep 17 00:00:00 2001 From: Philipinho <16838612+Philipinho@users.noreply.github.com> Date: Tue, 29 Oct 2024 19:37:49 +0000 Subject: [PATCH 18/71] fix: add drawio dark mode support --- .../src/features/editor/components/drawio/drawio-view.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/client/src/features/editor/components/drawio/drawio-view.tsx b/apps/client/src/features/editor/components/drawio/drawio-view.tsx index a89869a..3750594 100644 --- a/apps/client/src/features/editor/components/drawio/drawio-view.tsx +++ b/apps/client/src/features/editor/components/drawio/drawio-view.tsx @@ -1,5 +1,5 @@ import { NodeViewProps, NodeViewWrapper } from '@tiptap/react'; -import { ActionIcon, Card, Image, Modal, Text } from '@mantine/core'; +import { ActionIcon, Card, Image, Modal, Text, useComputedColorScheme } from '@mantine/core'; import { useRef, useState } from 'react'; import { uploadFile } from '@/features/page/services/page-service.ts'; import { useDisclosure } from '@mantine/hooks'; @@ -21,6 +21,7 @@ export default function DrawioView(props: NodeViewProps) { const drawioRef = useRef(null); const [initialXML, setInitialXML] = useState(''); const [opened, { open, close }] = useDisclosure(false); + const computedColorScheme = useComputedColorScheme(); const handleOpen = async () => { if (!editor.isEditable) { @@ -87,7 +88,7 @@ export default function DrawioView(props: NodeViewProps) { ref={drawioRef} xml={initialXML} urlParameters={{ - ui: 'kennedy', + ui: computedColorScheme === 'light' ? 'kennedy' : 'dark', spin: true, libraries: true, saveAndExit: true, From 9e0fbae1debae93e7d93b6e0f24f87ca36affcfd Mon Sep 17 00:00:00 2001 From: Philipinho <16838612+Philipinho@users.noreply.github.com> Date: Tue, 29 Oct 2024 19:39:08 +0000 Subject: [PATCH 19/71] fix: save excalidraw diagram in light mode only --- .../features/editor/components/excalidraw/excalidraw-view.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/client/src/features/editor/components/excalidraw/excalidraw-view.tsx b/apps/client/src/features/editor/components/excalidraw/excalidraw-view.tsx index f5e5b17..2ca1961 100644 --- a/apps/client/src/features/editor/components/excalidraw/excalidraw-view.tsx +++ b/apps/client/src/features/editor/components/excalidraw/excalidraw-view.tsx @@ -73,7 +73,7 @@ export default function ExcalidrawView(props: NodeViewProps) { elements: excalidrawAPI?.getSceneElements(), appState: { exportEmbedScene: true, - exportWithDarkMode: computedColorScheme != 'light', + exportWithDarkMode: false, }, files: excalidrawAPI?.getFiles(), }); From 95159625aacd7eda2dfd4611d3f07bdbc9ea5a83 Mon Sep 17 00:00:00 2001 From: Philipinho <16838612+Philipinho@users.noreply.github.com> Date: Tue, 29 Oct 2024 19:50:44 +0000 Subject: [PATCH 20/71] v0.5.0 --- apps/client/package.json | 2 +- apps/server/package.json | 2 +- package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/client/package.json b/apps/client/package.json index c5f6ece..a8d2556 100644 --- a/apps/client/package.json +++ b/apps/client/package.json @@ -1,7 +1,7 @@ { "name": "client", "private": true, - "version": "0.4.1", + "version": "0.5.0", "scripts": { "dev": "vite", "build": "tsc && vite build", diff --git a/apps/server/package.json b/apps/server/package.json index 7f2b1f7..9d5ef37 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -1,6 +1,6 @@ { "name": "server", - "version": "0.4.1", + "version": "0.5.0", "description": "", "author": "", "private": true, diff --git a/package.json b/package.json index fb58de0..1a0a692 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "docmost", "homepage": "https://docmost.com", - "version": "0.4.1", + "version": "0.5.0", "private": true, "scripts": { "build": "nx run-many -t build", From d07338861b6c5f17011fcf0510fcd3e7d66dfc02 Mon Sep 17 00:00:00 2001 From: Philip Okugbe Date: Wed, 30 Oct 2024 11:41:23 +0000 Subject: [PATCH 21/71] fix: prevent default browser save behavior (#450) --- apps/client/src/features/editor/full-editor.tsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/apps/client/src/features/editor/full-editor.tsx b/apps/client/src/features/editor/full-editor.tsx index aa87703..ccdde5e 100644 --- a/apps/client/src/features/editor/full-editor.tsx +++ b/apps/client/src/features/editor/full-editor.tsx @@ -5,6 +5,7 @@ import PageEditor from "@/features/editor/page-editor"; import { Container } from "@mantine/core"; import { useAtom } from "jotai"; import { userAtom } from "@/features/user/atoms/current-user-atom.ts"; +import { useOs } from "@mantine/hooks"; const MemoizedTitleEditor = React.memo(TitleEditor); const MemoizedPageEditor = React.memo(PageEditor); @@ -25,12 +26,21 @@ export function FullEditor({ editable, }: FullEditorProps) { const [user] = useAtom(userAtom); + const os = useOs(); + const fullPageWidth = user.settings?.preferences?.fullPageWidth; + // intercept ctrl+s to prevent default browser save behavior + document.addEventListener("keydown", function (e) { + if (editable && os === 'macos' ? e.metaKey : e.ctrlKey && e.code.toLowerCase() === 'keys') { + e.preventDefault(); + } + }, false); + return ( From 4f1a97ceb94b722a3b376d386779563683903ebc Mon Sep 17 00:00:00 2001 From: Philip Okugbe Date: Wed, 30 Oct 2024 12:23:31 +0000 Subject: [PATCH 22/71] Revert "fix: prevent default browser save behavior (#450)" (#451) This reverts commit d07338861b6c5f17011fcf0510fcd3e7d66dfc02. --- apps/client/src/features/editor/full-editor.tsx | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/apps/client/src/features/editor/full-editor.tsx b/apps/client/src/features/editor/full-editor.tsx index ccdde5e..aa87703 100644 --- a/apps/client/src/features/editor/full-editor.tsx +++ b/apps/client/src/features/editor/full-editor.tsx @@ -5,7 +5,6 @@ import PageEditor from "@/features/editor/page-editor"; import { Container } from "@mantine/core"; import { useAtom } from "jotai"; import { userAtom } from "@/features/user/atoms/current-user-atom.ts"; -import { useOs } from "@mantine/hooks"; const MemoizedTitleEditor = React.memo(TitleEditor); const MemoizedPageEditor = React.memo(PageEditor); @@ -26,21 +25,12 @@ export function FullEditor({ editable, }: FullEditorProps) { const [user] = useAtom(userAtom); - const os = useOs(); - const fullPageWidth = user.settings?.preferences?.fullPageWidth; - // intercept ctrl+s to prevent default browser save behavior - document.addEventListener("keydown", function (e) { - if (editable && os === 'macos' ? e.metaKey : e.ctrlKey && e.code.toLowerCase() === 'keys') { - e.preventDefault(); - } - }, false); - return ( From e064e58f7917e385809cbce3838266f6d06ad191 Mon Sep 17 00:00:00 2001 From: ftibi93 Date: Fri, 1 Nov 2024 10:41:23 +0100 Subject: [PATCH 23/71] Fix sidebar responsivity (#453) * navbar height fix. has to be cleaned up * use parent height for tree * cleanups --- .../src/features/page/tree/components/space-tree.tsx | 2 +- .../src/features/page/tree/styles/tree.module.css | 8 +++++--- .../space/components/sidebar/space-sidebar.module.css | 10 ++++++++++ .../space/components/sidebar/space-sidebar.tsx | 2 +- 4 files changed, 17 insertions(+), 5 deletions(-) diff --git a/apps/client/src/features/page/tree/components/space-tree.tsx b/apps/client/src/features/page/tree/components/space-tree.tsx index 67e0d0e..ef67742 100644 --- a/apps/client/src/features/page/tree/components/space-tree.tsx +++ b/apps/client/src/features/page/tree/components/space-tree.tsx @@ -197,7 +197,7 @@ export default function SpaceTree({ spaceId, readOnly }: SpaceTreeProps) { disableEdit={readOnly} {...controllers} width={width} - height={height} + height={rootElement.current.clientHeight} ref={treeApiRef} openByDefault={false} disableMultiSelection={true} diff --git a/apps/client/src/features/page/tree/styles/tree.module.css b/apps/client/src/features/page/tree/styles/tree.module.css index de35ea7..0a258fb 100644 --- a/apps/client/src/features/page/tree/styles/tree.module.css +++ b/apps/client/src/features/page/tree/styles/tree.module.css @@ -3,10 +3,12 @@ } .treeContainer { - display: flex; - height: 68vh; - flex: 1; + height: 100%; min-width: 0; + + > div, > div > .tree { + height: 100% !important; + } } .node { diff --git a/apps/client/src/features/space/components/sidebar/space-sidebar.module.css b/apps/client/src/features/space/components/sidebar/space-sidebar.module.css index 9e26316..b4a9b17 100644 --- a/apps/client/src/features/space/components/sidebar/space-sidebar.module.css +++ b/apps/client/src/features/space/components/sidebar/space-sidebar.module.css @@ -18,6 +18,16 @@ } } +.sectionPages { + margin-bottom: 0; + overflow-y: hidden; + + .pages { + height: 100%; + padding-bottom: 26px; + } +} + .menuItems { padding-left: calc(var(--mantine-spacing-md) - var(--mantine-spacing-xs)); padding-right: calc(var(--mantine-spacing-md) - var(--mantine-spacing-xs)); diff --git a/apps/client/src/features/space/components/sidebar/space-sidebar.tsx b/apps/client/src/features/space/components/sidebar/space-sidebar.tsx index ffaf0a9..0c45d3f 100644 --- a/apps/client/src/features/space/components/sidebar/space-sidebar.tsx +++ b/apps/client/src/features/space/components/sidebar/space-sidebar.tsx @@ -134,7 +134,7 @@ export function SpaceSidebar() { -
+
Pages From b85b34d6b1c8da4b0873fb7b867ffc381ea58e86 Mon Sep 17 00:00:00 2001 From: Philip Okugbe Date: Fri, 1 Nov 2024 10:05:03 +0000 Subject: [PATCH 24/71] feat: resizable sidebar (#452) * feat: resizable sidebar * only expand space sidebar --- .../layouts/global/app-shell.module.css | 15 ++++++ .../layouts/global/global-app-shell.tsx | 50 +++++++++++++++++-- .../global/hooks/atoms/sidebar-atom.ts | 2 + apps/client/src/lib/jotai-helper.ts | 4 +- 4 files changed, 65 insertions(+), 6 deletions(-) diff --git a/apps/client/src/components/layouts/global/app-shell.module.css b/apps/client/src/components/layouts/global/app-shell.module.css index b6e88d9..ed36961 100644 --- a/apps/client/src/components/layouts/global/app-shell.module.css +++ b/apps/client/src/components/layouts/global/app-shell.module.css @@ -14,3 +14,18 @@ } } +.resizeHandle { + width: 3px; + cursor: col-resize; + position: absolute; + right: 0; + top: 0; + bottom: 0; + + &:hover, &:active { + width: 5px; + background: light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-5)) + } +} + + diff --git a/apps/client/src/components/layouts/global/global-app-shell.tsx b/apps/client/src/components/layouts/global/global-app-shell.tsx index 663ed44..ae26c98 100644 --- a/apps/client/src/components/layouts/global/global-app-shell.tsx +++ b/apps/client/src/components/layouts/global/global-app-shell.tsx @@ -1,12 +1,12 @@ import { AppShell, Container } from "@mantine/core"; -import React from "react"; +import React, { useCallback, useEffect, useRef, useState } from "react"; import { useLocation } from "react-router-dom"; import SettingsSidebar from "@/components/settings/settings-sidebar.tsx"; import { useAtom } from "jotai"; import { asideStateAtom, desktopSidebarAtom, - mobileSidebarAtom, + mobileSidebarAtom, sidebarWidthAtom, } from "@/components/layouts/global/hooks/atoms/sidebar-atom.ts"; import { SpaceSidebar } from "@/features/space/components/sidebar/space-sidebar.tsx"; import { AppHeader } from "@/components/layouts/global/app-header.tsx"; @@ -21,6 +21,45 @@ export default function GlobalAppShell({ const [mobileOpened] = useAtom(mobileSidebarAtom); const [desktopOpened] = useAtom(desktopSidebarAtom); const [{ isAsideOpen }] = useAtom(asideStateAtom); + const [sidebarWidth, setSidebarWidth] = useAtom(sidebarWidthAtom); + const [isResizing, setIsResizing] = useState(false); + const sidebarRef = useRef(null); + + const startResizing = React.useCallback((mouseDownEvent) => { + setIsResizing(true); + }, []); + + const stopResizing = React.useCallback(() => { + setIsResizing(false); + }, []); + + const resize = React.useCallback( + (mouseMoveEvent) => { + if (isResizing) { + const newWidth = mouseMoveEvent.clientX - sidebarRef.current.getBoundingClientRect().left; + if (newWidth < 220) { + setSidebarWidth(220); + return; + } + if (newWidth > 600) { + setSidebarWidth(600); + return; + } + setSidebarWidth(newWidth); + } + }, + [isResizing] + ); + + useEffect(() => { + //https://codesandbox.io/p/sandbox/kz9de + window.addEventListener("mousemove", resize); + window.addEventListener("mouseup", stopResizing); + return () => { + window.removeEventListener("mousemove", resize); + window.removeEventListener("mouseup", stopResizing); + }; + }, [resize, stopResizing]); const location = useLocation(); const isSettingsRoute = location.pathname.startsWith("/settings"); @@ -33,7 +72,7 @@ export default function GlobalAppShell({ header={{ height: 45 }} navbar={ !isHomeRoute && { - width: 300, + width: isSpaceRoute ? sidebarWidth : 300, breakpoint: "sm", collapsed: { mobile: !mobileOpened, @@ -54,7 +93,10 @@ export default function GlobalAppShell({ {!isHomeRoute && ( - + e.preventDefault()} + > +
{isSpaceRoute && } {isSettingsRoute && } diff --git a/apps/client/src/components/layouts/global/hooks/atoms/sidebar-atom.ts b/apps/client/src/components/layouts/global/hooks/atoms/sidebar-atom.ts index fed366e..f71fc6f 100644 --- a/apps/client/src/components/layouts/global/hooks/atoms/sidebar-atom.ts +++ b/apps/client/src/components/layouts/global/hooks/atoms/sidebar-atom.ts @@ -19,3 +19,5 @@ export const asideStateAtom = atom({ tab: "", isAsideOpen: false, }); + +export const sidebarWidthAtom = atomWithWebStorage('sidebarWidth', 300); diff --git a/apps/client/src/lib/jotai-helper.ts b/apps/client/src/lib/jotai-helper.ts index 06d2525..252a3bf 100644 --- a/apps/client/src/lib/jotai-helper.ts +++ b/apps/client/src/lib/jotai-helper.ts @@ -2,9 +2,9 @@ import { atom } from "jotai"; export function atomWithWebStorage(key: string, initialValue: Value, storage = localStorage) { const storedValue = localStorage.getItem(key); - const isString = typeof initialValue === "string"; + const isStringOrInt = typeof initialValue === "string" || typeof initialValue === "number"; - const storageValue = storedValue ? isString ? storedValue : storedValue === "true" : undefined; + const storageValue = storedValue ? isStringOrInt ? storedValue : storedValue === "true" : undefined; const baseAtom = atom(storageValue ?? initialValue); return atom( From f7426a0b4507e535948707a05eea2535cf5f30f6 Mon Sep 17 00:00:00 2001 From: Philipinho <16838612+Philipinho@users.noreply.github.com> Date: Fri, 1 Nov 2024 10:09:52 +0000 Subject: [PATCH 25/71] fix: use clsx --- .../src/features/space/components/sidebar/space-sidebar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/client/src/features/space/components/sidebar/space-sidebar.tsx b/apps/client/src/features/space/components/sidebar/space-sidebar.tsx index 0c45d3f..1244828 100644 --- a/apps/client/src/features/space/components/sidebar/space-sidebar.tsx +++ b/apps/client/src/features/space/components/sidebar/space-sidebar.tsx @@ -134,7 +134,7 @@ export function SpaceSidebar() {
-
+
Pages From 4e7af507c6af1e07c8744da1e9eac22f01e52143 Mon Sep 17 00:00:00 2001 From: Philipinho <16838612+Philipinho@users.noreply.github.com> Date: Wed, 6 Nov 2024 19:29:12 -0800 Subject: [PATCH 26/71] fix tree dnd --- .../client/src/components/layouts/global/global-app-shell.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/client/src/components/layouts/global/global-app-shell.tsx b/apps/client/src/components/layouts/global/global-app-shell.tsx index ae26c98..4740027 100644 --- a/apps/client/src/components/layouts/global/global-app-shell.tsx +++ b/apps/client/src/components/layouts/global/global-app-shell.tsx @@ -93,9 +93,7 @@ export default function GlobalAppShell({ {!isHomeRoute && ( - e.preventDefault()} - > +
{isSpaceRoute && } {isSettingsRoute && } From da9c9710501bb6edffb359797d8250f8879e1798 Mon Sep 17 00:00:00 2001 From: ftibi93 Date: Wed, 13 Nov 2024 20:15:37 +0100 Subject: [PATCH 27/71] fix breadcrumb clipping (#457) --- .../features/page/components/breadcrumbs/breadcrumb.module.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/client/src/features/page/components/breadcrumbs/breadcrumb.module.css b/apps/client/src/features/page/components/breadcrumbs/breadcrumb.module.css index ad3ff78..cf3637b 100644 --- a/apps/client/src/features/page/components/breadcrumbs/breadcrumb.module.css +++ b/apps/client/src/features/page/components/breadcrumbs/breadcrumb.module.css @@ -1,11 +1,11 @@ .breadcrumbs { - flex: 1 1 auto; display: flex; align-items: center; overflow: hidden; a { color: var(--mantine-color-default-color); + line-height: inherit; } .mantine-Breadcrumbs-breadcrumb { From d01ced078ba352d7277b7ced402697257e07cf75 Mon Sep 17 00:00:00 2001 From: Philipinho <16838612+Philipinho@users.noreply.github.com> Date: Wed, 13 Nov 2024 11:36:55 -0800 Subject: [PATCH 28/71] * Reduce code block font-size * Make inline code more distinctive --- apps/client/src/features/editor/styles/code.css | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/client/src/features/editor/styles/code.css b/apps/client/src/features/editor/styles/code.css index 059b81d..b8be352 100644 --- a/apps/client/src/features/editor/styles/code.css +++ b/apps/client/src/features/editor/styles/code.css @@ -25,7 +25,7 @@ color: inherit; padding: 0; background: none; - font-size: inherit; + font-size: var(--mantine-font-size-sm); } /* Code styling */ @@ -103,12 +103,12 @@ @mixin where-light { background-color: var(--code-bg, var(--mantine-color-gray-1)); - color: var(--mantine-color-black); + color: var(--mantine-color-pink-7); } @mixin where-dark { background-color: var(--mantine-color-dark-8); - color: var(--mantine-color-gray-4); + color: var(--mantine-color-pink-7); } } } From 8194c7d42da38ae9341fabbb3ee2e71faa76f1ee Mon Sep 17 00:00:00 2001 From: Philip Okugbe Date: Wed, 13 Nov 2024 12:00:25 -0800 Subject: [PATCH 29/71] fix: focus editor on bottom click (#484) --- apps/client/src/features/editor/page-editor.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/client/src/features/editor/page-editor.tsx b/apps/client/src/features/editor/page-editor.tsx index 5030468..2349e4f 100644 --- a/apps/client/src/features/editor/page-editor.tsx +++ b/apps/client/src/features/editor/page-editor.tsx @@ -97,8 +97,8 @@ export default function PageEditor({ pageId, editable }: PageEditorProps) { }, [remoteProvider, localProvider]); const extensions = [ - ...mainExtensions, - ...collabExtensions(remoteProvider, currentUser.user), + ... mainExtensions, + ... collabExtensions(remoteProvider, currentUser.user), ]; const editor = useEditor( @@ -184,6 +184,7 @@ export default function PageEditor({ pageId, editable }: PageEditorProps) { )}
)} +
editor.commands.focus('end')} style={{ paddingBottom: '20vh' }}>
) : ( From dd0319a14d7ada01e3e38f0273fd24d23cee4197 Mon Sep 17 00:00:00 2001 From: Philip Okugbe Date: Wed, 20 Nov 2024 05:36:36 -0800 Subject: [PATCH 30/71] fix: index imported content (#495) --- apps/server/src/integrations/import/import.service.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/server/src/integrations/import/import.service.ts b/apps/server/src/integrations/import/import.service.ts index cdc8c75..a4eb805 100644 --- a/apps/server/src/integrations/import/import.service.ts +++ b/apps/server/src/integrations/import/import.service.ts @@ -4,7 +4,7 @@ import { MultipartFile } from '@fastify/multipart'; import { sanitize } from 'sanitize-filename-ts'; import * as path from 'path'; import { - htmlToJson, + htmlToJson, jsonToText, tiptapExtensions, } from '../../collaboration/collaboration.util'; import { InjectKysely } from 'nestjs-kysely'; @@ -72,6 +72,7 @@ export class ImportService { slugId: generateSlugId(), title: pageTitle, content: prosemirrorJson, + textContent: jsonToText(prosemirrorJson), ydoc: await this.createYdoc(prosemirrorJson), position: pagePosition, spaceId: spaceId, From a1b6ac7f3eafede615a9766a7b4a1468eec682da Mon Sep 17 00:00:00 2001 From: Philip Okugbe Date: Wed, 27 Nov 2024 02:32:12 +0000 Subject: [PATCH 31/71] fix: close space selection popover onClickOutside (#485) --- .../src/features/space/components/sidebar/switch-space.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/apps/client/src/features/space/components/sidebar/switch-space.tsx b/apps/client/src/features/space/components/sidebar/switch-space.tsx index d428dfe..8009af0 100644 --- a/apps/client/src/features/space/components/sidebar/switch-space.tsx +++ b/apps/client/src/features/space/components/sidebar/switch-space.tsx @@ -12,7 +12,6 @@ interface SwitchSpaceProps { } export function SwitchSpace({ spaceName, spaceSlug }: SwitchSpaceProps) { - const [opened, { close, open, toggle }] = useDisclosure(false); const navigate = useNavigate(); const handleSelect = (value: string) => { @@ -28,7 +27,6 @@ export function SwitchSpace({ spaceName, spaceSlug }: SwitchSpaceProps) { position="bottom" withArrow shadow="md" - opened={opened} > + + + + + + ); +} + +interface ExportFormatSelection { + format: ExportFormat; + onChange: (value: string) => void; +} +function ExportFormatSelection({ format, onChange }: ExportFormatSelection) { + return ( + ); } diff --git a/apps/client/src/components/common/recent-changes.tsx b/apps/client/src/components/common/recent-changes.tsx index 0f89c81..75935fa 100644 --- a/apps/client/src/components/common/recent-changes.tsx +++ b/apps/client/src/components/common/recent-changes.tsx @@ -8,17 +8,19 @@ import { } from '@mantine/core'; import {Link} from 'react-router-dom'; import PageListSkeleton from '@/components/ui/page-list-skeleton.tsx'; -import {buildPageUrl} from '@/features/page/page.utils.ts'; -import {formattedDate} from '@/lib/time.ts'; -import {useRecentChangesQuery} from '@/features/page/queries/page-query.ts'; -import {IconFileDescription} from '@tabler/icons-react'; -import {getSpaceUrl} from '@/lib/config.ts'; +import { buildPageUrl } from '@/features/page/page.utils.ts'; +import { formattedDate } from '@/lib/time.ts'; +import { useRecentChangesQuery } from '@/features/page/queries/page-query.ts'; +import { IconFileDescription } from '@tabler/icons-react'; +import { getSpaceUrl } from '@/lib/config.ts'; +import { useTranslation } from "react-i18next"; interface Props { spaceId?: string; } export default function RecentChanges({spaceId}: Props) { + const { t } = useTranslation(); const {data: pages, isLoading, isError} = useRecentChangesQuery(spaceId); if (isLoading) { @@ -26,7 +28,7 @@ export default function RecentChanges({spaceId}: Props) { } if (isError) { - return Failed to fetch recent pages; + return {t("Failed to fetch recent pages")}; } return pages && pages.items.length > 0 ? ( @@ -48,7 +50,7 @@ export default function RecentChanges({spaceId}: Props) { )} - {page.title || 'Untitled'} + {page.title || t("Untitled")} @@ -78,7 +80,7 @@ export default function RecentChanges({spaceId}: Props) { ) : ( - No pages yet + {t("No pages yet")} ); } diff --git a/apps/client/src/components/layouts/global/app-header.tsx b/apps/client/src/components/layouts/global/app-header.tsx index 1eeb735..04b2b09 100644 --- a/apps/client/src/components/layouts/global/app-header.tsx +++ b/apps/client/src/components/layouts/global/app-header.tsx @@ -11,10 +11,12 @@ import { } from "@/components/layouts/global/hooks/atoms/sidebar-atom.ts"; import {useToggleSidebar} from "@/components/layouts/global/hooks/hooks/use-toggle-sidebar.ts"; import SidebarToggle from "@/components/ui/sidebar-toggle-button.tsx"; +import { useTranslation } from "react-i18next"; const links = [{link: APP_ROUTE.HOME, label: "Home"}]; export function AppHeader() { + const { t } = useTranslation(); const [mobileOpened] = useAtom(mobileSidebarAtom); const toggleMobile = useToggleSidebar(mobileSidebarAtom); @@ -25,7 +27,7 @@ export function AppHeader() { const items = links.map((link) => ( - {link.label} + {t(link.label)} )); @@ -35,10 +37,10 @@ export function AppHeader() { {!isHomeRoute && ( <> - + - + - {title} + {t(title)} - Workspace + {t("Workspace")} } > - Workspace settings + {t("Workspace settings")} } > - Manage members + {t("Manage members")} - Account + {t("Account")} } > - My profile + {t("My profile")} } > - My preferences + {t("My preferences")} }> - Logout + {t("Logout")} diff --git a/apps/client/src/components/settings/settings-sidebar.tsx b/apps/client/src/components/settings/settings-sidebar.tsx index e4ee279..8ffb7c4 100644 --- a/apps/client/src/components/settings/settings-sidebar.tsx +++ b/apps/client/src/components/settings/settings-sidebar.tsx @@ -11,6 +11,7 @@ import { } from "@tabler/icons-react"; import { Link, useLocation, useNavigate } from "react-router-dom"; import classes from "./settings.module.css"; +import { useTranslation } from "react-i18next"; interface DataItem { label: string; @@ -51,6 +52,7 @@ const groupedData: DataGroup[] = [ ]; export default function SettingsSidebar() { + const { t } = useTranslation(); const location = useLocation(); const [active, setActive] = useState(location.pathname); const navigate = useNavigate(); @@ -62,7 +64,7 @@ export default function SettingsSidebar() { const menuItems = groupedData.map((group) => (
- {group.heading} + {t(group.heading)} {group.items.map((item) => ( - {item.label} + {t(item.label)} ))}
@@ -89,7 +91,7 @@ export default function SettingsSidebar() { > - Settings + {t("Settings")}
{menuItems} diff --git a/apps/client/src/components/ui/error-404.tsx b/apps/client/src/components/ui/error-404.tsx index 52d4b83..8c8846a 100644 --- a/apps/client/src/components/ui/error-404.tsx +++ b/apps/client/src/components/ui/error-404.tsx @@ -2,21 +2,24 @@ import { Title, Text, Button, Container, Group } from "@mantine/core"; import classes from "./error-404.module.css"; import { Link } from "react-router-dom"; import { Helmet } from "react-helmet-async"; +import { useTranslation } from "react-i18next"; export function Error404() { + const { t } = useTranslation(); + return ( <> - 404 page not found - Docmost + {t("404 page not found")} - Docmost - 404 Page Not Found + {t("404 page not found")} - Sorry, we can't find the page you are looking for. + {t("Sorry, we can't find the page you are looking for.")} diff --git a/apps/client/src/components/ui/role-select-menu.tsx b/apps/client/src/components/ui/role-select-menu.tsx index 0d83cb7..2184383 100644 --- a/apps/client/src/components/ui/role-select-menu.tsx +++ b/apps/client/src/components/ui/role-select-menu.tsx @@ -2,6 +2,7 @@ import React, { forwardRef } from "react"; import { IconCheck, IconChevronDown } from "@tabler/icons-react"; import { Group, Text, Menu, Button } from "@mantine/core"; import { IRoleData } from "@/lib/types.ts"; +import { useTranslation } from "react-i18next"; interface RoleButtonProps extends React.ComponentPropsWithoutRef<"button"> { name: string; @@ -36,10 +37,12 @@ export default function RoleSelectMenu({ onChange, disabled, }: RoleMenuProps) { + const { t } = useTranslation(); + return ( - + @@ -50,9 +53,9 @@ export default function RoleSelectMenu({ >
- {item.label} + {t(item.label)} - {item.description} + {t(item.description)}
{item.label === roleName && } diff --git a/apps/client/src/features/auth/components/forgot-password-form.tsx b/apps/client/src/features/auth/components/forgot-password-form.tsx index 74714e5..f42f2e0 100644 --- a/apps/client/src/features/auth/components/forgot-password-form.tsx +++ b/apps/client/src/features/auth/components/forgot-password-form.tsx @@ -6,6 +6,7 @@ import { IForgotPassword } from "@/features/auth/types/auth.types"; import { Box, Button, Container, Text, TextInput, Title } from "@mantine/core"; import classes from "./auth.module.css"; import { useRedirectIfAuthenticated } from "@/features/auth/hooks/use-redirect-if-authenticated.ts"; +import { useTranslation } from "react-i18next"; const formSchema = z.object({ email: z @@ -15,6 +16,7 @@ const formSchema = z.object({ }); export function ForgotPasswordForm() { + const { t } = useTranslation(); const { forgotPassword, isLoading } = useAuth(); const [isTokenSent, setIsTokenSent] = useState(false); useRedirectIfAuthenticated(); @@ -36,7 +38,7 @@ export function ForgotPasswordForm() { - Forgot password + {t("Forgot password")}
@@ -53,14 +55,15 @@ export function ForgotPasswordForm() { {isTokenSent && ( - A password reset link has been sent to your email. Please check - your inbox. + {t( + "A password reset link has been sent to your email. Please check your inbox.", + )} )} {!isTokenSent && ( )}
diff --git a/apps/client/src/features/auth/components/invite-sign-up-form.tsx b/apps/client/src/features/auth/components/invite-sign-up-form.tsx index 2690d00..a135c60 100644 --- a/apps/client/src/features/auth/components/invite-sign-up-form.tsx +++ b/apps/client/src/features/auth/components/invite-sign-up-form.tsx @@ -17,6 +17,7 @@ import useAuth from "@/features/auth/hooks/use-auth"; import classes from "@/features/auth/components/auth.module.css"; import { useGetInvitationQuery } from "@/features/workspace/queries/workspace-query.ts"; import { useRedirectIfAuthenticated } from "@/features/auth/hooks/use-redirect-if-authenticated.ts"; +import { useTranslation } from "react-i18next"; const formSchema = z.object({ name: z.string().trim().min(1), @@ -26,6 +27,7 @@ const formSchema = z.object({ type FormValues = z.infer; export function InviteSignUpForm() { + const { t } = useTranslation(); const params = useParams(); const [searchParams] = useSearchParams(); @@ -55,7 +57,7 @@ export function InviteSignUpForm() { } if (isError) { - return
invalid invitation link
; + return
{t("invalid invitation link")}
; } if (!invitation) { @@ -66,7 +68,7 @@ export function InviteSignUpForm() { - Join the workspace + {t("Join the workspace")} @@ -74,8 +76,8 @@ export function InviteSignUpForm() { @@ -83,7 +85,7 @@ export function InviteSignUpForm() { diff --git a/apps/client/src/features/auth/components/login-form.tsx b/apps/client/src/features/auth/components/login-form.tsx index 8d55206..794bf46 100644 --- a/apps/client/src/features/auth/components/login-form.tsx +++ b/apps/client/src/features/auth/components/login-form.tsx @@ -9,13 +9,13 @@ import { Button, PasswordInput, Box, - Anchor, } from "@mantine/core"; import classes from "./auth.module.css"; import { useRedirectIfAuthenticated } from "@/features/auth/hooks/use-redirect-if-authenticated.ts"; import { Link, useNavigate } from "react-router-dom"; import APP_ROUTE from "@/lib/app-route.ts"; +import { useTranslation } from "react-i18next"; const formSchema = z.object({ email: z @@ -26,6 +26,7 @@ const formSchema = z.object({ }); export function LoginForm() { + const { t } = useTranslation(); const { signIn, isLoading } = useAuth(); useRedirectIfAuthenticated(); @@ -45,29 +46,29 @@ export function LoginForm() { - Login + {t("Login")}
@@ -77,7 +78,7 @@ export function LoginForm() { underline="never" size="sm" > - Forgot your password? + {t("Forgot your password?")}
diff --git a/apps/client/src/features/auth/components/password-reset-form.tsx b/apps/client/src/features/auth/components/password-reset-form.tsx index 9896406..13d0307 100644 --- a/apps/client/src/features/auth/components/password-reset-form.tsx +++ b/apps/client/src/features/auth/components/password-reset-form.tsx @@ -12,6 +12,7 @@ import { } from "@mantine/core"; import classes from "./auth.module.css"; import { useRedirectIfAuthenticated } from "@/features/auth/hooks/use-redirect-if-authenticated.ts"; +import { useTranslation } from "react-i18next"; const formSchema = z.object({ newPassword: z @@ -24,6 +25,7 @@ interface PasswordResetFormProps { } export function PasswordResetForm({ resetToken }: PasswordResetFormProps) { + const { t } = useTranslation(); const { passwordReset, isLoading } = useAuth(); useRedirectIfAuthenticated(); @@ -37,28 +39,28 @@ export function PasswordResetForm({ resetToken }: PasswordResetFormProps) { async function onSubmit(data: IPasswordReset) { await passwordReset({ token: resetToken, - newPassword: data.newPassword - }) + newPassword: data.newPassword, + }); } return ( - Password reset + {t("Password reset")}
diff --git a/apps/client/src/features/auth/components/setup-workspace-form.tsx b/apps/client/src/features/auth/components/setup-workspace-form.tsx index 6be94f5..7bff251 100644 --- a/apps/client/src/features/auth/components/setup-workspace-form.tsx +++ b/apps/client/src/features/auth/components/setup-workspace-form.tsx @@ -13,6 +13,7 @@ import { import { ISetupWorkspace } from "@/features/auth/types/auth.types"; import useAuth from "@/features/auth/hooks/use-auth"; import classes from "@/features/auth/components/auth.module.css"; +import { useTranslation } from "react-i18next"; const formSchema = z.object({ workspaceName: z.string().trim().min(3).max(50), @@ -25,6 +26,7 @@ const formSchema = z.object({ }); export function SetupWorkspaceForm() { + const { t } = useTranslation(); const { setupWorkspace, isLoading } = useAuth(); // useRedirectIfAuthenticated(); @@ -46,15 +48,15 @@ export function SetupWorkspaceForm() { - Create workspace + {t("Create workspace")}
diff --git a/apps/client/src/features/comment/components/comment-actions.tsx b/apps/client/src/features/comment/components/comment-actions.tsx index b045a3b..9249a07 100644 --- a/apps/client/src/features/comment/components/comment-actions.tsx +++ b/apps/client/src/features/comment/components/comment-actions.tsx @@ -1,4 +1,5 @@ -import { Button, Group } from '@mantine/core'; +import { Button, Group } from "@mantine/core"; +import { useTranslation } from "react-i18next"; type CommentActionsProps = { onSave: () => void; @@ -6,9 +7,13 @@ type CommentActionsProps = { }; function CommentActions({ onSave, isLoading }: CommentActionsProps) { + const { t } = useTranslation(); + return ( - + ); } diff --git a/apps/client/src/features/comment/components/comment-dialog.tsx b/apps/client/src/features/comment/components/comment-dialog.tsx index 1a3020f..2e27f65 100644 --- a/apps/client/src/features/comment/components/comment-dialog.tsx +++ b/apps/client/src/features/comment/components/comment-dialog.tsx @@ -14,6 +14,7 @@ import { useCreateCommentMutation } from "@/features/comment/queries/comment-que import { asideStateAtom } from "@/components/layouts/global/hooks/atoms/sidebar-atom"; import { useEditor } from "@tiptap/react"; import { CustomAvatar } from "@/components/ui/custom-avatar.tsx"; +import { useTranslation } from "react-i18next"; interface CommentDialogProps { editor: ReturnType; @@ -21,6 +22,7 @@ interface CommentDialogProps { } function CommentDialog({ editor, pageId }: CommentDialogProps) { + const { t } = useTranslation(); const [comment, setComment] = useState(""); const [, setShowCommentPopup] = useAtom(showCommentPopupAtom); const [, setActiveCommentId] = useAtom(activeCommentIdAtom); @@ -107,7 +109,7 @@ function CommentDialog({ editor, pageId }: CommentDialogProps) { diff --git a/apps/client/src/features/comment/components/comment-editor.tsx b/apps/client/src/features/comment/components/comment-editor.tsx index 62e1b13..850d6b9 100644 --- a/apps/client/src/features/comment/components/comment-editor.tsx +++ b/apps/client/src/features/comment/components/comment-editor.tsx @@ -7,6 +7,7 @@ import classes from "./comment.module.css"; import { useFocusWithin } from "@mantine/hooks"; import clsx from "clsx"; import { forwardRef, useEffect, useImperativeHandle } from "react"; +import { useTranslation } from "react-i18next"; interface CommentEditorProps { defaultContent?: any; @@ -27,6 +28,7 @@ const CommentEditor = forwardRef( }: CommentEditorProps, ref, ) => { + const { t } = useTranslation(); const { ref: focusRef, focused } = useFocusWithin(); const commentEditor = useEditor({ @@ -36,7 +38,7 @@ const CommentEditor = forwardRef( dropcursor: false, }), Placeholder.configure({ - placeholder: placeholder || "Reply...", + placeholder: placeholder || t("Reply..."), }), Underline, Link, diff --git a/apps/client/src/features/comment/components/comment-list-item.tsx b/apps/client/src/features/comment/components/comment-list-item.tsx index d4f9172..e27bfa7 100644 --- a/apps/client/src/features/comment/components/comment-list-item.tsx +++ b/apps/client/src/features/comment/components/comment-list-item.tsx @@ -24,7 +24,6 @@ function CommentListItem({ comment }: CommentListItemProps) { const { hovered, ref } = useHover(); const [isEditing, setIsEditing] = useState(false); const [isLoading, setIsLoading] = useState(false); - const editor = useAtomValue(pageEditorAtom); const [content, setContent] = useState(comment.content); const updateCommentMutation = useUpdateCommentMutation(); diff --git a/apps/client/src/features/comment/components/comment-list.tsx b/apps/client/src/features/comment/components/comment-list.tsx index ccac53a..3296d5e 100644 --- a/apps/client/src/features/comment/components/comment-list.tsx +++ b/apps/client/src/features/comment/components/comment-list.tsx @@ -6,7 +6,6 @@ import { useCommentsQuery, useCreateCommentMutation, } from "@/features/comment/queries/comment-query"; - import CommentEditor from "@/features/comment/components/comment-editor"; import CommentActions from "@/features/comment/components/comment-actions"; import { useFocusWithin } from "@mantine/hooks"; @@ -14,8 +13,10 @@ import { IComment } from "@/features/comment/types/comment.types.ts"; import { usePageQuery } from "@/features/page/queries/page-query.ts"; import { IPagination } from "@/lib/types.ts"; import { extractPageSlugId } from "@/lib"; +import { useTranslation } from "react-i18next"; function CommentList() { + const { t } = useTranslation(); const { pageSlug } = useParams(); const { data: page } = usePageQuery({ pageId: extractPageSlugId(pageSlug) }); const { @@ -79,11 +80,11 @@ function CommentList() { } if (isError) { - return
Error loading comments.
; + return
{t("Error loading comments.")}
; } if (!comments || comments.items.length === 0) { - return <>No comments yet.; + return <>{t("No comments yet.")}; } return ( diff --git a/apps/client/src/features/comment/components/comment-menu.tsx b/apps/client/src/features/comment/components/comment-menu.tsx index 2f5fa75..051c415 100644 --- a/apps/client/src/features/comment/components/comment-menu.tsx +++ b/apps/client/src/features/comment/components/comment-menu.tsx @@ -1,6 +1,7 @@ -import { ActionIcon, Menu } from '@mantine/core'; -import { IconDots, IconEdit, IconTrash } from '@tabler/icons-react'; -import { modals } from '@mantine/modals'; +import { ActionIcon, Menu } from "@mantine/core"; +import { IconDots, IconEdit, IconTrash } from "@tabler/icons-react"; +import { modals } from "@mantine/modals"; +import { useTranslation } from "react-i18next"; type CommentMenuProps = { onEditComment: () => void; @@ -8,34 +9,35 @@ type CommentMenuProps = { }; function CommentMenu({ onEditComment, onDeleteComment }: CommentMenuProps) { + const { t } = useTranslation(); //@ts-ignore const openDeleteModal = () => modals.openConfirmModal({ - title: 'Are you sure you want to delete this comment?', + title: t("Are you sure you want to delete this comment?"), centered: true, - labels: { confirm: 'Delete', cancel: 'Cancel' }, - confirmProps: { color: 'red' }, + labels: { confirm: t("Delete"), cancel: t("Cancel") }, + confirmProps: { color: "red" }, onConfirm: onDeleteComment, }); return ( - + - }> - Edit comment + }> + {t("Edit comment")} - } - onClick={openDeleteModal} + } + onClick={openDeleteModal} > - Delete comment + {t("Delete comment")} diff --git a/apps/client/src/features/comment/components/resolve-comment.tsx b/apps/client/src/features/comment/components/resolve-comment.tsx index f6b206f..9d7c4e7 100644 --- a/apps/client/src/features/comment/components/resolve-comment.tsx +++ b/apps/client/src/features/comment/components/resolve-comment.tsx @@ -1,34 +1,44 @@ -import { ActionIcon } from '@mantine/core'; -import { IconCircleCheck } from '@tabler/icons-react'; -import { modals } from '@mantine/modals'; -import { useResolveCommentMutation } from '@/features/comment/queries/comment-query'; +import { ActionIcon } from "@mantine/core"; +import { IconCircleCheck } from "@tabler/icons-react"; +import { modals } from "@mantine/modals"; +import { useResolveCommentMutation } from "@/features/comment/queries/comment-query"; +import { useTranslation } from "react-i18next"; function ResolveComment({ commentId, pageId, resolvedAt }) { + const { t } = useTranslation(); const resolveCommentMutation = useResolveCommentMutation(); + const isResolved = resolvedAt != null; - const iconColor = isResolved ? 'green' : 'gray'; + const iconColor = isResolved ? "green" : "gray"; //@ts-ignore const openConfirmModal = () => modals.openConfirmModal({ - title: 'Are you sure you want to resolve this comment thread?', + title: t("Are you sure you want to resolve this comment thread?"), centered: true, - labels: { confirm: 'Confirm', cancel: 'Cancel' }, + labels: { confirm: t("Confirm"), cancel: t("Cancel") }, onConfirm: handleResolveToggle, }); const handleResolveToggle = async () => { try { - await resolveCommentMutation.mutateAsync({ commentId, resolved: !isResolved }); + await resolveCommentMutation.mutateAsync({ + commentId, + resolved: !isResolved, + }); //TODO: remove comment mark // Remove comment thread from state on resolve } catch (error) { - console.error('Failed to toggle resolved state:', error); + console.error("Failed to toggle resolved state:", error); } }; return ( - + ); diff --git a/apps/client/src/features/comment/queries/comment-query.ts b/apps/client/src/features/comment/queries/comment-query.ts index 7eb1ae0..85b2e54 100644 --- a/apps/client/src/features/comment/queries/comment-query.ts +++ b/apps/client/src/features/comment/queries/comment-query.ts @@ -18,6 +18,7 @@ import { } from "@/features/comment/types/comment.types"; import { notifications } from "@mantine/notifications"; import { IPagination } from "@/lib/types.ts"; +import { useTranslation } from "react-i18next"; export const RQ_KEY = (pageId: string) => ["comments", pageId]; @@ -33,6 +34,7 @@ export function useCommentsQuery( export function useCreateCommentMutation() { const queryClient = useQueryClient(); + const { t } = useTranslation(); return useMutation>({ mutationFn: (data) => createComment(data), @@ -45,28 +47,37 @@ export function useCreateCommentMutation() { //} queryClient.refetchQueries({ queryKey: RQ_KEY(data.pageId) }); - notifications.show({ message: "Comment created successfully" }); + notifications.show({ message: t("Comment created successfully") }); }, onError: (error) => { - notifications.show({ message: "Error creating comment", color: "red" }); + notifications.show({ + message: t("Error creating comment"), + color: "red", + }); }, }); } export function useUpdateCommentMutation() { + const { t } = useTranslation(); + return useMutation>({ mutationFn: (data) => updateComment(data), onSuccess: (data) => { - notifications.show({ message: "Comment updated successfully" }); + notifications.show({ message: t("Comment updated successfully") }); }, onError: (error) => { - notifications.show({ message: "Failed to update comment", color: "red" }); + notifications.show({ + message: t("Failed to update comment"), + color: "red", + }); }, }); } export function useDeleteCommentMutation(pageId?: string) { const queryClient = useQueryClient(); + const { t } = useTranslation(); return useMutation({ mutationFn: (commentId: string) => deleteComment(commentId), @@ -86,16 +97,20 @@ export function useDeleteCommentMutation(pageId?: string) { }); } - notifications.show({ message: "Comment deleted successfully" }); + notifications.show({ message: t("Comment deleted successfully") }); }, onError: (error) => { - notifications.show({ message: "Failed to delete comment", color: "red" }); + notifications.show({ + message: t("Failed to delete comment"), + color: "red", + }); }, }); } export function useResolveCommentMutation() { const queryClient = useQueryClient(); + const { t } = useTranslation(); return useMutation({ mutationFn: (data: IResolveComment) => resolveComment(data), @@ -114,11 +129,11 @@ export function useResolveCommentMutation() { queryClient.setQueryData(RQ_KEY(data.pageId), updatedComments); }*/ - notifications.show({ message: "Comment resolved successfully" }); + notifications.show({ message: t("Comment resolved successfully") }); }, onError: (error) => { notifications.show({ - message: "Failed to resolve comment", + message: t("Failed to resolve comment"), color: "red", }); }, diff --git a/apps/client/src/features/editor/components/attachment/upload-attachment-action.tsx b/apps/client/src/features/editor/components/attachment/upload-attachment-action.tsx index 2386361..5b208df 100644 --- a/apps/client/src/features/editor/components/attachment/upload-attachment-action.tsx +++ b/apps/client/src/features/editor/components/attachment/upload-attachment-action.tsx @@ -1,8 +1,9 @@ import { handleAttachmentUpload } from "@docmost/editor-ext"; import { uploadFile } from "@/features/page/services/page-service.ts"; import { notifications } from "@mantine/notifications"; -import {getFileUploadSizeLimit} from "@/lib/config.ts"; -import {formatBytes} from "@/lib"; +import { getFileUploadSizeLimit } from "@/lib/config.ts"; +import { formatBytes } from "@/lib"; +import i18n from "i18next"; export const uploadAttachmentAction = handleAttachmentUpload({ onUpload: async (file: File, pageId: string): Promise => { @@ -23,7 +24,9 @@ export const uploadAttachmentAction = handleAttachmentUpload({ if (file.size > getFileUploadSizeLimit()) { notifications.show({ color: "red", - message: `File exceeds the ${formatBytes(getFileUploadSizeLimit())} attachment limit`, + message: i18n.t("File exceeds the {{limit}} attachment limit", { + limit: formatBytes(getFileUploadSizeLimit()), + }), }); return false; } diff --git a/apps/client/src/features/editor/components/bubble-menu/bubble-menu.tsx b/apps/client/src/features/editor/components/bubble-menu/bubble-menu.tsx index 693e0c2..5ee16c3 100644 --- a/apps/client/src/features/editor/components/bubble-menu/bubble-menu.tsx +++ b/apps/client/src/features/editor/components/bubble-menu/bubble-menu.tsx @@ -26,6 +26,7 @@ import { useAtom } from "jotai"; import { v7 as uuid7 } from "uuid"; import { isCellSelection, isTextSelected } from "@docmost/editor-ext"; import { LinkSelector } from "@/features/editor/components/bubble-menu/link-selector.tsx"; +import { useTranslation } from "react-i18next"; export interface BubbleMenuItem { name: string; @@ -39,6 +40,7 @@ type EditorBubbleMenuProps = Omit & { }; export const EditorBubbleMenu: FC = (props) => { + const { t } = useTranslation(); const [showCommentPopup, setShowCommentPopup] = useAtom(showCommentPopupAtom); const [, setDraftCommentId] = useAtom(draftCommentIdAtom); const showCommentPopupRef = useRef(showCommentPopup); @@ -49,31 +51,31 @@ export const EditorBubbleMenu: FC = (props) => { const items: BubbleMenuItem[] = [ { - name: "bold", + name: "Bold", isActive: () => props.editor.isActive("bold"), command: () => props.editor.chain().focus().toggleBold().run(), icon: IconBold, }, { - name: "italic", + name: "Italic", isActive: () => props.editor.isActive("italic"), command: () => props.editor.chain().focus().toggleItalic().run(), icon: IconItalic, }, { - name: "underline", + name: "Underline", isActive: () => props.editor.isActive("underline"), command: () => props.editor.chain().focus().toggleUnderline().run(), icon: IconUnderline, }, { - name: "strike", + name: "Strike", isActive: () => props.editor.isActive("strike"), command: () => props.editor.chain().focus().toggleStrike().run(), icon: IconStrikethrough, }, { - name: "code", + name: "Code", isActive: () => props.editor.isActive("code"), command: () => props.editor.chain().focus().toggleCode().run(), icon: IconCode, @@ -81,7 +83,7 @@ export const EditorBubbleMenu: FC = (props) => { ]; const commentItem: BubbleMenuItem = { - name: "comment", + name: "Comment", isActive: () => props.editor.isActive("comment"), command: () => { const commentId = uuid7(); @@ -138,13 +140,13 @@ export const EditorBubbleMenu: FC = (props) => { {items.map((item, index) => ( - + = (props) => { variant="default" size="lg" radius="0" - aria-label={commentItem.name} + aria-label={t(commentItem.name)} style={{ border: "none" }} onClick={commentItem.command} > diff --git a/apps/client/src/features/editor/components/bubble-menu/color-selector.tsx b/apps/client/src/features/editor/components/bubble-menu/color-selector.tsx index 8402517..4b47cd2 100644 --- a/apps/client/src/features/editor/components/bubble-menu/color-selector.tsx +++ b/apps/client/src/features/editor/components/bubble-menu/color-selector.tsx @@ -10,6 +10,7 @@ import { Tooltip, } from "@mantine/core"; import { useEditor } from "@tiptap/react"; +import { useTranslation } from "react-i18next"; export interface BubbleColorMenuItem { name: string; @@ -106,6 +107,7 @@ export const ColorSelector: FC = ({ isOpen, setIsOpen, }) => { + const { t } = useTranslation(); const activeColorItem = TEXT_COLORS.find(({ color }) => editor.isActive("textStyle", { color }), ); @@ -117,7 +119,7 @@ export const ColorSelector: FC = ({ return ( - + = ({ {/* make mah responsive */} - - COLOR + + {t("Color")} @@ -155,7 +157,7 @@ export const ColorSelector: FC = ({ } onClick={() => { editor.commands.unsetColor(); - name !== "Default" && + name !== t("Default") && editor .chain() .focus() @@ -165,7 +167,7 @@ export const ColorSelector: FC = ({ }} style={{ border: "none" }} > - {name} + {t(name)} ))} diff --git a/apps/client/src/features/editor/components/bubble-menu/link-selector.tsx b/apps/client/src/features/editor/components/bubble-menu/link-selector.tsx index c9b9a08..67bb9f8 100644 --- a/apps/client/src/features/editor/components/bubble-menu/link-selector.tsx +++ b/apps/client/src/features/editor/components/bubble-menu/link-selector.tsx @@ -3,6 +3,7 @@ import { IconLink } from "@tabler/icons-react"; import { ActionIcon, Popover, Tooltip } from "@mantine/core"; import { useEditor } from "@tiptap/react"; import { LinkEditorPanel } from "@/features/editor/components/link/link-editor-panel.tsx"; +import { useTranslation } from "react-i18next"; interface LinkSelectorProps { editor: ReturnType; @@ -15,6 +16,7 @@ export const LinkSelector: FC = ({ isOpen, setIsOpen, }) => { + const { t } = useTranslation(); const onLink = useCallback( (url: string) => { setIsOpen(false); @@ -32,7 +34,7 @@ export const LinkSelector: FC = ({ withArrow > - + ; @@ -33,6 +34,8 @@ export const NodeSelector: FC = ({ isOpen, setIsOpen, }) => { + const { t } = useTranslation(); + const items: BubbleMenuItem[] = [ { name: "Text", @@ -114,7 +117,7 @@ export const NodeSelector: FC = ({ rightSection={} onClick={() => setIsOpen(!isOpen)} > - {activeItem?.name} + {t(activeItem?.name)} @@ -137,7 +140,7 @@ export const NodeSelector: FC = ({ }} style={{ border: "none" }} > - {item.name} + {t(item.name)} ))} diff --git a/apps/client/src/features/editor/components/callout/callout-menu.tsx b/apps/client/src/features/editor/components/callout/callout-menu.tsx index 1203ec6..56dea23 100644 --- a/apps/client/src/features/editor/components/callout/callout-menu.tsx +++ b/apps/client/src/features/editor/components/callout/callout-menu.tsx @@ -17,8 +17,10 @@ import { IconInfoCircleFilled, } from "@tabler/icons-react"; import { CalloutType } from "@docmost/editor-ext"; +import { useTranslation } from "react-i18next"; export function CalloutMenu({ editor }: EditorMenuProps) { + const { t } = useTranslation(); const shouldShow = useCallback( ({ state }: ShouldShowProps) => { if (!state) { @@ -71,11 +73,11 @@ export function CalloutMenu({ editor }: EditorMenuProps) { shouldShow={shouldShow} > - + setCalloutType("info")} size="lg" - aria-label="Info" + aria-label={t("Info")} variant={ editor.isActive("callout", { type: "info" }) ? "light" : "default" } @@ -84,11 +86,11 @@ export function CalloutMenu({ editor }: EditorMenuProps) { - + setCalloutType("success")} size="lg" - aria-label="Success" + aria-label={t("Success")} variant={ editor.isActive("callout", { type: "success" }) ? "light" @@ -99,11 +101,11 @@ export function CalloutMenu({ editor }: EditorMenuProps) { - + setCalloutType("warning")} size="lg" - aria-label="Warning" + aria-label={t("Warning")} variant={ editor.isActive("callout", { type: "warning" }) ? "light" @@ -114,11 +116,11 @@ export function CalloutMenu({ editor }: EditorMenuProps) { - + setCalloutType("danger")} size="lg" - aria-label="Danger" + aria-label={t("Danger")} variant={ editor.isActive("callout", { type: "danger" }) ? "light" diff --git a/apps/client/src/features/editor/components/code-block/code-block-view.tsx b/apps/client/src/features/editor/components/code-block/code-block-view.tsx index 8ee2493..ba25110 100644 --- a/apps/client/src/features/editor/components/code-block/code-block-view.tsx +++ b/apps/client/src/features/editor/components/code-block/code-block-view.tsx @@ -2,16 +2,17 @@ import { NodeViewContent, NodeViewProps, NodeViewWrapper } from '@tiptap/react'; import { ActionIcon, CopyButton, Group, Select, Tooltip } from '@mantine/core'; import { useEffect, useState } from 'react'; import { IconCheck, IconCopy } from '@tabler/icons-react'; -//import MermaidView from "@/features/editor/components/code-block/mermaid-view.tsx"; import classes from './code-block.module.css'; import React from 'react'; import { Suspense } from 'react'; +import { useTranslation } from "react-i18next"; const MermaidView = React.lazy( () => import('@/features/editor/components/code-block/mermaid-view.tsx') ); export default function CodeBlockView(props: NodeViewProps) { + const { t } = useTranslation(); const { node, updateAttributes, extension, editor, getPos } = props; const { language } = node.attrs; const [languageValue, setLanguageValue] = useState( @@ -61,7 +62,7 @@ export default function CodeBlockView(props: NodeViewProps) { {({ copied, copy }) => ( diff --git a/apps/client/src/features/editor/components/code-block/mermaid-view.tsx b/apps/client/src/features/editor/components/code-block/mermaid-view.tsx index 5fc4b68..3c3c538 100644 --- a/apps/client/src/features/editor/components/code-block/mermaid-view.tsx +++ b/apps/client/src/features/editor/components/code-block/mermaid-view.tsx @@ -3,6 +3,7 @@ import { useEffect, useState } from "react"; import mermaid from "mermaid"; import { v4 as uuidv4 } from "uuid"; import classes from "./code-block.module.css"; +import { t } from "i18next"; mermaid.initialize({ startOnLoad: false, @@ -29,11 +30,11 @@ export default function MermaidView({ props }: MermaidViewProps) { .catch((err) => { if (props.editor.isEditable) { setPreview( - `
Mermaid diagram error: ${err}
`, + `
${t("Mermaid diagram error:")} ${err}
`, ); } else { setPreview( - `
Invalid Mermaid Diagram
`, + `
${t("Invalid Mermaid diagram")}
`, ); } }); diff --git a/apps/client/src/features/editor/components/drawio/drawio-view.tsx b/apps/client/src/features/editor/components/drawio/drawio-view.tsx index 930958f..6f7d68c 100644 --- a/apps/client/src/features/editor/components/drawio/drawio-view.tsx +++ b/apps/client/src/features/editor/components/drawio/drawio-view.tsx @@ -1,25 +1,34 @@ -import { NodeViewProps, NodeViewWrapper } from '@tiptap/react'; -import { ActionIcon, Card, Image, Modal, Text, useComputedColorScheme } from '@mantine/core'; -import { useRef, useState } from 'react'; -import { uploadFile } from '@/features/page/services/page-service.ts'; -import { useDisclosure } from '@mantine/hooks'; -import { getDrawioUrl, getFileUrl } from '@/lib/config.ts'; +import { NodeViewProps, NodeViewWrapper } from "@tiptap/react"; +import { + ActionIcon, + Card, + Image, + Modal, + Text, + useComputedColorScheme, +} from "@mantine/core"; +import { useRef, useState } from "react"; +import { uploadFile } from "@/features/page/services/page-service.ts"; +import { useDisclosure } from "@mantine/hooks"; +import { getDrawioUrl, getFileUrl } from "@/lib/config.ts"; import { DrawIoEmbed, DrawIoEmbedRef, EventExit, EventSave, -} from 'react-drawio'; -import { IAttachment } from '@/lib/types'; -import { decodeBase64ToSvgString, svgStringToFile } from '@/lib/utils'; -import clsx from 'clsx'; -import { IconEdit } from '@tabler/icons-react'; +} from "react-drawio"; +import { IAttachment } from "@/lib/types"; +import { decodeBase64ToSvgString, svgStringToFile } from "@/lib/utils"; +import clsx from "clsx"; +import { IconEdit } from "@tabler/icons-react"; +import { useTranslation } from "react-i18next"; export default function DrawioView(props: NodeViewProps) { + const { t } = useTranslation(); const { node, updateAttributes, editor, selected } = props; const { src, title, width, attachmentId } = node.attrs; const drawioRef = useRef(null); - const [initialXML, setInitialXML] = useState(''); + const [initialXML, setInitialXML] = useState(""); const [opened, { open, close }] = useDisclosure(false); const computedColorScheme = useComputedColorScheme(); @@ -32,15 +41,15 @@ export default function DrawioView(props: NodeViewProps) { if (src) { const url = getFileUrl(src); const request = await fetch(url, { - credentials: 'include', - cache: 'no-store', + credentials: "include", + cache: "no-store", }); const blob = await request.blob(); const reader = new FileReader(); reader.readAsDataURL(blob); reader.onloadend = () => { - const base64data = (reader.result || '') as string; + const base64data = (reader.result || "") as string; setInitialXML(base64data); }; } @@ -54,7 +63,7 @@ export default function DrawioView(props: NodeViewProps) { const handleSave = async (data: EventSave) => { const svgString = decodeBase64ToSvgString(data.xml); - const fileName = 'diagram.drawio.svg'; + const fileName = "diagram.drawio.svg"; const drawioSVGFile = await svgStringToFile(svgString, fileName); const pageId = editor.storage?.pageId; @@ -81,15 +90,15 @@ export default function DrawioView(props: NodeViewProps) { - + -
+
{ // If the save is triggered by another event, then do nothing - if (data.parentEvent !== 'save') { + if (data.parentEvent !== "save") { return; } handleSave(data); @@ -116,7 +125,7 @@ export default function DrawioView(props: NodeViewProps) { {src ? ( -
+
e.detail === 2 && handleOpen()} radius="md" @@ -125,8 +134,8 @@ export default function DrawioView(props: NodeViewProps) { src={getFileUrl(src)} alt={title} className={clsx( - selected ? 'ProseMirror-selectednode' : '', - 'alignCenter' + selected ? "ProseMirror-selectednode" : "", + "alignCenter", )} /> @@ -137,7 +146,7 @@ export default function DrawioView(props: NodeViewProps) { color="gray" mx="xs" style={{ - position: 'absolute', + position: "absolute", top: 8, right: 8, }} @@ -152,20 +161,20 @@ export default function DrawioView(props: NodeViewProps) { onClick={(e) => e.detail === 2 && handleOpen()} p="xs" style={{ - display: 'flex', - justifyContent: 'center', - alignItems: 'center', + display: "flex", + justifyContent: "center", + alignItems: "center", }} withBorder - className={clsx(selected ? 'ProseMirror-selectednode' : '')} + className={clsx(selected ? "ProseMirror-selectednode" : "")} > -
+
- Double-click to edit drawio diagram + {t("Double-click to edit Draw.io diagram")}
diff --git a/apps/client/src/features/editor/components/embed/embed-view.tsx b/apps/client/src/features/editor/components/embed/embed-view.tsx index 57b8de0..dcc96e4 100644 --- a/apps/client/src/features/editor/components/embed/embed-view.tsx +++ b/apps/client/src/features/editor/components/embed/embed-view.tsx @@ -1,22 +1,37 @@ import { NodeViewProps, NodeViewWrapper } from "@tiptap/react"; import { useMemo } from "react"; import clsx from "clsx"; -import { ActionIcon, AspectRatio, Button, Card, FocusTrap, Group, Popover, Text, TextInput } from "@mantine/core"; +import { + ActionIcon, + AspectRatio, + Button, + Card, + FocusTrap, + Group, + Popover, + Text, + TextInput, +} from "@mantine/core"; import { IconEdit } from "@tabler/icons-react"; import { z } from "zod"; import { useForm, zodResolver } from "@mantine/form"; import { getEmbedProviderById, - getEmbedUrlAndProvider + getEmbedUrlAndProvider, } from "@/features/editor/components/embed/providers.ts"; -import { notifications } from '@mantine/notifications'; +import { notifications } from "@mantine/notifications"; +import { useTranslation } from "react-i18next"; +import i18n from "i18next"; const schema = z.object({ url: z - .string().trim().url({ message: 'please enter a valid url' }), + .string() + .trim() + .url({ message: i18n.t("Please enter a valid url") }), }); export default function EmbedView(props: NodeViewProps) { + const { t } = useTranslation(); const { node, selected, updateAttributes } = props; const { src, provider } = node.attrs; @@ -41,9 +56,9 @@ export default function EmbedView(props: NodeViewProps) { updateAttributes({ src: data.url }); } else { notifications.show({ - message: `Invalid ${provider} embed link`, - position: 'top-right', - color: 'red' + message: t("Invalid {{provider}} embed link", { provider: provider }), + position: "top-right", + color: "red", }); } } @@ -62,7 +77,6 @@ export default function EmbedView(props: NodeViewProps) { frameBorder="0" > - ) : ( @@ -71,20 +85,22 @@ export default function EmbedView(props: NodeViewProps) { radius="md" p="xs" style={{ - display: 'flex', - justifyContent: 'center', - alignItems: 'center', + display: "flex", + justifyContent: "center", + alignItems: "center", }} withBorder - className={clsx(selected ? 'ProseMirror-selectednode' : '')} + className={clsx(selected ? "ProseMirror-selectednode" : "")} > -
+
- Embed {getEmbedProviderById(provider).name} + {t("Embed {{provider}}", { + provider: getEmbedProviderById(provider).name, + })}
@@ -92,15 +108,18 @@ export default function EmbedView(props: NodeViewProps) {
- - +
diff --git a/apps/client/src/features/editor/components/excalidraw/excalidraw-view.tsx b/apps/client/src/features/editor/components/excalidraw/excalidraw-view.tsx index 2ca1961..e145918 100644 --- a/apps/client/src/features/editor/components/excalidraw/excalidraw-view.tsx +++ b/apps/client/src/features/editor/components/excalidraw/excalidraw-view.tsx @@ -1,4 +1,4 @@ -import { NodeViewProps, NodeViewWrapper } from '@tiptap/react'; +import { NodeViewProps, NodeViewWrapper } from "@tiptap/react"; import { ActionIcon, Button, @@ -7,27 +7,29 @@ import { Image, Text, useComputedColorScheme, -} from '@mantine/core'; -import { useState } from 'react'; -import { uploadFile } from '@/features/page/services/page-service.ts'; -import { svgStringToFile } from '@/lib'; -import { useDisclosure } from '@mantine/hooks'; -import { getFileUrl } from '@/lib/config.ts'; -import { ExcalidrawImperativeAPI } from '@excalidraw/excalidraw/types/types'; -import { IAttachment } from '@/lib/types'; -import ReactClearModal from 'react-clear-modal'; -import clsx from 'clsx'; -import { IconEdit } from '@tabler/icons-react'; -import { lazy } from 'react'; -import { Suspense } from 'react'; +} from "@mantine/core"; +import { useState } from "react"; +import { uploadFile } from "@/features/page/services/page-service.ts"; +import { svgStringToFile } from "@/lib"; +import { useDisclosure } from "@mantine/hooks"; +import { getFileUrl } from "@/lib/config.ts"; +import { ExcalidrawImperativeAPI } from "@excalidraw/excalidraw/types/types"; +import { IAttachment } from "@/lib/types"; +import ReactClearModal from "react-clear-modal"; +import clsx from "clsx"; +import { IconEdit } from "@tabler/icons-react"; +import { lazy } from "react"; +import { Suspense } from "react"; +import { useTranslation } from "react-i18next"; const Excalidraw = lazy(() => - import('@excalidraw/excalidraw').then((module) => ({ + import("@excalidraw/excalidraw").then((module) => ({ default: module.Excalidraw, - })) + })), ); export default function ExcalidrawView(props: NodeViewProps) { + const { t } = useTranslation(); const { node, updateAttributes, editor, selected } = props; const { src, title, width, attachmentId } = node.attrs; @@ -46,11 +48,11 @@ export default function ExcalidrawView(props: NodeViewProps) { if (src) { const url = getFileUrl(src); const request = await fetch(url, { - credentials: 'include', - cache: 'no-store', + credentials: "include", + cache: "no-store", }); - const { loadFromBlob } = await import('@excalidraw/excalidraw'); + const { loadFromBlob } = await import("@excalidraw/excalidraw"); const data = await loadFromBlob(await request.blob(), null, null); setExcalidrawData(data); @@ -67,7 +69,7 @@ export default function ExcalidrawView(props: NodeViewProps) { return; } - const { exportToSvg } = await import('@excalidraw/excalidraw'); + const { exportToSvg } = await import("@excalidraw/excalidraw"); const svg = await exportToSvg({ elements: excalidrawAPI?.getSceneElements(), @@ -83,10 +85,10 @@ export default function ExcalidrawView(props: NodeViewProps) { svgString = svgString.replace( /https:\/\/unpkg\.com\/@excalidraw\/excalidraw@undefined/g, - 'https://unpkg.com/@excalidraw/excalidraw@latest' + "https://unpkg.com/@excalidraw/excalidraw@latest", ); - const fileName = 'diagram.excalidraw.svg'; + const fileName = "diagram.excalidraw.svg"; const excalidrawSvgFile = await svgStringToFile(svgString, fileName); const pageId = editor.storage?.pageId; @@ -112,7 +114,7 @@ export default function ExcalidrawView(props: NodeViewProps) { @@ -132,14 +134,14 @@ export default function ExcalidrawView(props: NodeViewProps) { bg="var(--mantine-color-body)" p="xs" > - - -
+
setExcalidrawAPI(api)} @@ -154,7 +156,7 @@ export default function ExcalidrawView(props: NodeViewProps) { {src ? ( -
+
e.detail === 2 && handleOpen()} radius="md" @@ -163,8 +165,8 @@ export default function ExcalidrawView(props: NodeViewProps) { src={getFileUrl(src)} alt={title} className={clsx( - selected ? 'ProseMirror-selectednode' : '', - 'alignCenter' + selected ? "ProseMirror-selectednode" : "", + "alignCenter", )} /> @@ -175,7 +177,7 @@ export default function ExcalidrawView(props: NodeViewProps) { color="gray" mx="xs" style={{ - position: 'absolute', + position: "absolute", top: 8, right: 8, }} @@ -190,20 +192,20 @@ export default function ExcalidrawView(props: NodeViewProps) { onClick={(e) => e.detail === 2 && handleOpen()} p="xs" style={{ - display: 'flex', - justifyContent: 'center', - alignItems: 'center', + display: "flex", + justifyContent: "center", + alignItems: "center", }} withBorder - className={clsx(selected ? 'ProseMirror-selectednode' : '')} + className={clsx(selected ? "ProseMirror-selectednode" : "")} > -
+
- Double-click to edit Excalidraw diagram + {t("Double-click to edit Excalidraw diagram")}
diff --git a/apps/client/src/features/editor/components/image/image-menu.tsx b/apps/client/src/features/editor/components/image/image-menu.tsx index 3598599..abb1c1c 100644 --- a/apps/client/src/features/editor/components/image/image-menu.tsx +++ b/apps/client/src/features/editor/components/image/image-menu.tsx @@ -17,8 +17,10 @@ import { IconLayoutAlignRight, } from "@tabler/icons-react"; import { NodeWidthResize } from "@/features/editor/components/common/node-width-resize.tsx"; +import { useTranslation } from "react-i18next"; export function ImageMenu({ editor }: EditorMenuProps) { + const { t } = useTranslation(); const shouldShow = useCallback( ({ state }: ShouldShowProps) => { if (!state) { @@ -96,11 +98,11 @@ export function ImageMenu({ editor }: EditorMenuProps) { shouldShow={shouldShow} > - + - + - + => { @@ -23,7 +24,9 @@ export const uploadImageAction = handleImageUpload({ if (file.size > getFileUploadSizeLimit()) { notifications.show({ color: "red", - message: `File exceeds the ${formatBytes(getFileUploadSizeLimit())} attachment limit`, + message: i18n.t("File exceeds the {{limit}} attachment limit", { + limit: formatBytes(getFileUploadSizeLimit()), + }), }); return false; } diff --git a/apps/client/src/features/editor/components/link/link-editor-panel.tsx b/apps/client/src/features/editor/components/link/link-editor-panel.tsx index bed51d1..733455c 100644 --- a/apps/client/src/features/editor/components/link/link-editor-panel.tsx +++ b/apps/client/src/features/editor/components/link/link-editor-panel.tsx @@ -3,11 +3,13 @@ import { Button, Group, TextInput } from "@mantine/core"; import { IconLink } from "@tabler/icons-react"; import { useLinkEditorState } from "@/features/editor/components/link/use-link-editor-state.tsx"; import { LinkEditorPanelProps } from "@/features/editor/components/link/types.ts"; +import { useTranslation } from "react-i18next"; export const LinkEditorPanel = ({ onSetLink, initialUrl, }: LinkEditorPanelProps) => { + const { t } = useTranslation(); const state = useLinkEditorState({ onSetLink, initialUrl, @@ -20,12 +22,12 @@ export const LinkEditorPanel = ({ } variant="filled" - placeholder="Paste link" + placeholder={t("Paste link")} value={state.url} onChange={state.onChange} /> diff --git a/apps/client/src/features/editor/components/link/link-preview.tsx b/apps/client/src/features/editor/components/link/link-preview.tsx index 35cec27..fecd172 100644 --- a/apps/client/src/features/editor/components/link/link-preview.tsx +++ b/apps/client/src/features/editor/components/link/link-preview.tsx @@ -7,6 +7,7 @@ import { Flex, } from "@mantine/core"; import { IconLinkOff, IconPencil } from "@tabler/icons-react"; +import { useTranslation } from "react-i18next"; export type LinkPreviewPanelProps = { url: string; @@ -19,6 +20,8 @@ export const LinkPreviewPanel = ({ onEdit, url, }: LinkPreviewPanelProps) => { + const { t } = useTranslation(); + return ( <> @@ -42,13 +45,13 @@ export const LinkPreviewPanel = ({ - + - + diff --git a/apps/client/src/features/editor/components/math/math-block.tsx b/apps/client/src/features/editor/components/math/math-block.tsx index 7413499..a289fca 100644 --- a/apps/client/src/features/editor/components/math/math-block.tsx +++ b/apps/client/src/features/editor/components/math/math-block.tsx @@ -8,8 +8,10 @@ import classes from "./math.module.css"; import { v4 } from "uuid"; import { IconTrashX } from "@tabler/icons-react"; import { useDebouncedValue } from "@mantine/hooks"; +import { useTranslation } from "react-i18next"; export default function MathBlockView(props: NodeViewProps) { + const { t } = useTranslation(); const { node, updateAttributes, editor, getPos } = props; const mathResultContainer = useRef(null); const mathPreviewContainer = useRef(null); @@ -94,9 +96,9 @@ export default function MathBlockView(props: NodeViewProps) { >
{((isEditing && !preview?.trim().length) || (!isEditing && !node.attrs.text.trim().length)) && ( -
Empty equation
+
{t("Empty equation")}
)} - {error &&
Invalid equation
} + {error &&
{t("Invalid equation")}
} diff --git a/apps/client/src/features/editor/components/math/math-inline.tsx b/apps/client/src/features/editor/components/math/math-inline.tsx index a65e432..8d4897d 100644 --- a/apps/client/src/features/editor/components/math/math-inline.tsx +++ b/apps/client/src/features/editor/components/math/math-inline.tsx @@ -6,8 +6,10 @@ import { NodeViewProps, NodeViewWrapper } from "@tiptap/react"; import { Popover, Textarea } from "@mantine/core"; import classes from "./math.module.css"; import { v4 } from "uuid"; +import { useTranslation } from "react-i18next"; export default function MathInlineView(props: NodeViewProps) { + const { t } = useTranslation(); const { node, updateAttributes, editor, getPos } = props; const mathResultContainer = useRef(null); const mathPreviewContainer = useRef(null); @@ -84,9 +86,9 @@ export default function MathInlineView(props: NodeViewProps) { >
{((isEditing && !preview?.trim().length) || (!isEditing && !node.attrs.text.trim().length)) && ( -
Empty equation
+
{t("Empty equation")}
)} - {error &&
Invalid equation
} + {error &&
{t("Invalid equation")}
} diff --git a/apps/client/src/features/editor/components/slash-menu/command-list.tsx b/apps/client/src/features/editor/components/slash-menu/command-list.tsx index 8453f9f..ab1dcaf 100644 --- a/apps/client/src/features/editor/components/slash-menu/command-list.tsx +++ b/apps/client/src/features/editor/components/slash-menu/command-list.tsx @@ -13,6 +13,7 @@ import { } from "@mantine/core"; import classes from "./slash-menu.module.css"; import clsx from "clsx"; +import { useTranslation } from "react-i18next"; const CommandList = ({ items, @@ -25,6 +26,7 @@ const CommandList = ({ editor: any; range: any; }) => { + const { t } = useTranslation(); const [selectedIndex, setSelectedIndex] = useState(0); const viewportRef = useRef(null); @@ -104,18 +106,17 @@ const CommandList = ({
- {item.title} + {t(item.title)} - {item.description} + {t(item.description)}
diff --git a/apps/client/src/features/editor/components/table/table-cell-menu.tsx b/apps/client/src/features/editor/components/table/table-cell-menu.tsx index 721420f..e348ea6 100644 --- a/apps/client/src/features/editor/components/table/table-cell-menu.tsx +++ b/apps/client/src/features/editor/components/table/table-cell-menu.tsx @@ -13,9 +13,11 @@ import { IconRowRemove, IconSquareToggle, } from "@tabler/icons-react"; +import { useTranslation } from "react-i18next"; export const TableCellMenu = React.memo( ({ editor, appendTo }: EditorMenuProps): JSX.Element => { + const { t } = useTranslation(); const shouldShow = useCallback( ({ view, state, from }: ShouldShowProps) => { if (!state) { @@ -58,45 +60,45 @@ export const TableCellMenu = React.memo( shouldShow={shouldShow} > - + - + - + - + diff --git a/apps/client/src/features/editor/components/table/table-menu.tsx b/apps/client/src/features/editor/components/table/table-menu.tsx index e35b5bc..5321b90 100644 --- a/apps/client/src/features/editor/components/table/table-menu.tsx +++ b/apps/client/src/features/editor/components/table/table-menu.tsx @@ -21,9 +21,11 @@ import { IconTrashX, } from "@tabler/icons-react"; import { isCellSelection } from "@docmost/editor-ext"; +import { useTranslation } from "react-i18next"; export const TableMenu = React.memo( ({ editor }: EditorMenuProps): JSX.Element => { + const { t } = useTranslation(); const shouldShow = useCallback( ({ state }: ShouldShowProps) => { if (!state) { @@ -111,79 +113,80 @@ export const TableMenu = React.memo( shouldShow={shouldShow} > - + - + - + - + - + - + - + diff --git a/apps/client/src/features/editor/components/video/upload-video-action.tsx b/apps/client/src/features/editor/components/video/upload-video-action.tsx index 4173207..da96997 100644 --- a/apps/client/src/features/editor/components/video/upload-video-action.tsx +++ b/apps/client/src/features/editor/components/video/upload-video-action.tsx @@ -1,8 +1,9 @@ import { handleVideoUpload } from "@docmost/editor-ext"; import { uploadFile } from "@/features/page/services/page-service.ts"; import { notifications } from "@mantine/notifications"; -import {getFileUploadSizeLimit} from "@/lib/config.ts"; -import {formatBytes} from "@/lib"; +import { getFileUploadSizeLimit } from "@/lib/config.ts"; +import { formatBytes } from "@/lib"; +import i18n from "i18next"; export const uploadVideoAction = handleVideoUpload({ onUpload: async (file: File, pageId: string): Promise => { @@ -24,11 +25,12 @@ export const uploadVideoAction = handleVideoUpload({ if (file.size > getFileUploadSizeLimit()) { notifications.show({ color: "red", - message: `File exceeds the ${formatBytes(getFileUploadSizeLimit())} attachment limit`, + message: i18n.t("File exceeds the {{limit}} attachment limit", { + limit: formatBytes(getFileUploadSizeLimit()), + }), }); return false; } return true; }, }); - diff --git a/apps/client/src/features/editor/components/video/video-menu.tsx b/apps/client/src/features/editor/components/video/video-menu.tsx index 03705ad..0c671cd 100644 --- a/apps/client/src/features/editor/components/video/video-menu.tsx +++ b/apps/client/src/features/editor/components/video/video-menu.tsx @@ -17,8 +17,10 @@ import { IconLayoutAlignRight, } from "@tabler/icons-react"; import { NodeWidthResize } from "@/features/editor/components/common/node-width-resize.tsx"; +import { useTranslation } from "react-i18next"; export function VideoMenu({ editor }: EditorMenuProps) { + const { t } = useTranslation(); const shouldShow = useCallback( ({ state }: ShouldShowProps) => { if (!state) { @@ -96,11 +98,11 @@ export function VideoMenu({ editor }: EditorMenuProps) { shouldShow={shouldShow} > - + - + - + { if (node.type.name === "heading") { - return `Heading ${node.attrs.level}`; + return i18n.t("Heading {{level}}", { level: node.attrs.level }); } if (node.type.name === "detailsSummary") { - return "Toggle title"; + return i18n.t("Toggle title"); } if (node.type.name === "paragraph") { - return 'Write anything. Enter "/" for commands'; + return i18n.t('Write anything. Enter "/" for commands'); } }, includeChildren: true, @@ -184,7 +185,7 @@ export const mainExtensions = [ }), Embed.configure({ view: EmbedView, - }) + }), ] as any; type CollabExtensions = (provider: HocuspocusProvider, user: IUser) => any[]; diff --git a/apps/client/src/features/editor/title-editor.tsx b/apps/client/src/features/editor/title-editor.tsx index 038aac2..5d0a54f 100644 --- a/apps/client/src/features/editor/title-editor.tsx +++ b/apps/client/src/features/editor/title-editor.tsx @@ -19,6 +19,7 @@ import { useQueryEmit } from "@/features/websocket/use-query-emit.ts"; import { History } from "@tiptap/extension-history"; import { buildPageUrl } from "@/features/page/page.utils.ts"; import { useNavigate } from "react-router-dom"; +import { useTranslation } from "react-i18next"; export interface TitleEditorProps { pageId: string; @@ -35,6 +36,7 @@ export function TitleEditor({ spaceSlug, editable, }: TitleEditorProps) { + const { t } = useTranslation(); const [debouncedTitleState, setDebouncedTitleState] = useState(null); const [debouncedTitle] = useDebouncedValue(debouncedTitleState, 500); const { @@ -59,7 +61,7 @@ export function TitleEditor({ }), Text, Placeholder.configure({ - placeholder: "Untitled", + placeholder: t("Untitled"), showOnlyWhenEditable: false, }), History.configure({ diff --git a/apps/client/src/features/group/components/add-group-member-modal.tsx b/apps/client/src/features/group/components/add-group-member-modal.tsx index 8b05c59..a5abaa7 100644 --- a/apps/client/src/features/group/components/add-group-member-modal.tsx +++ b/apps/client/src/features/group/components/add-group-member-modal.tsx @@ -4,8 +4,10 @@ import React, { useState } from "react"; import { MultiUserSelect } from "@/features/group/components/multi-user-select.tsx"; import { useParams } from "react-router-dom"; import { useAddGroupMemberMutation } from "@/features/group/queries/group-query.ts"; +import { useTranslation } from "react-i18next"; export default function AddGroupMemberModal() { + const { t } = useTranslation(); const { groupId } = useParams(); const [opened, { open, close }] = useDisclosure(false); const [userIds, setUserIds] = useState([]); @@ -27,19 +29,19 @@ export default function AddGroupMemberModal() { return ( <> - + - + diff --git a/apps/client/src/features/group/components/create-group-form.tsx b/apps/client/src/features/group/components/create-group-form.tsx index 0c6df49..10d4375 100644 --- a/apps/client/src/features/group/components/create-group-form.tsx +++ b/apps/client/src/features/group/components/create-group-form.tsx @@ -5,6 +5,7 @@ import { useForm, zodResolver } from "@mantine/form"; import * as z from "zod"; import { useNavigate } from "react-router-dom"; import { MultiUserSelect } from "@/features/group/components/multi-user-select.tsx"; +import { useTranslation } from "react-i18next"; const formSchema = z.object({ name: z.string().trim().min(2).max(50), @@ -14,6 +15,7 @@ const formSchema = z.object({ type FormValues = z.infer; export function CreateGroupForm() { + const { t } = useTranslation(); const createGroupMutation = useCreateGroupMutation(); const [userIds, setUserIds] = useState([]); const navigate = useNavigate(); @@ -52,16 +54,16 @@ export function CreateGroupForm() {