diff --git a/apps/client/src/features/workspace/components/settings/components/workspace-default-page-edit-mode.tsx b/apps/client/src/features/workspace/components/settings/components/workspace-default-page-edit-mode.tsx new file mode 100644 index 000000000..65248d104 --- /dev/null +++ b/apps/client/src/features/workspace/components/settings/components/workspace-default-page-edit-mode.tsx @@ -0,0 +1,71 @@ +import { Group, Text, SegmentedControl } from "@mantine/core"; +import { useAtom } from "jotai"; +import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts"; +import { useState, useEffect } from "react"; +import { useTranslation } from "react-i18next"; +import { updateWorkspace } from "@/features/workspace/services/workspace-service.ts"; +import { notifications } from "@mantine/notifications"; +import { getApiErrorMessage } from "@/lib/api-error.ts"; +import { PageEditMode } from "@/features/user/types/user.types.ts"; + +export default function WorkspaceDefaultPageEditMode() { + const { t } = useTranslation(); + + return ( + +
+ {t("Default page edit mode")} + + {t( + "Choose the page edit mode new members start with. Existing members are not affected.", + )} + +
+ + +
+ ); +} + +function DefaultPageEditModeControl() { + const { t } = useTranslation(); + const [workspace, setWorkspace] = useAtom(workspaceAtom); + const defaultPageEditMode = + workspace?.settings?.defaultPageEditMode ?? PageEditMode.Edit; + const [value, setValue] = useState(defaultPageEditMode); + + const handleChange = async (newValue: string) => { + const prevValue = value; + setValue(newValue); + try { + const updatedWorkspace = await updateWorkspace({ + defaultPageEditMode: newValue, + }); + setWorkspace(updatedWorkspace); + } catch (err) { + setValue(prevValue); + notifications.show({ + message: getApiErrorMessage(err, t("Failed to update setting")), + color: "red", + }); + } + }; + + useEffect(() => { + if (defaultPageEditMode !== value) { + setValue(defaultPageEditMode); + } + }, [defaultPageEditMode, value]); + + return ( + + ); +} diff --git a/apps/client/src/features/workspace/types/workspace.types.ts b/apps/client/src/features/workspace/types/workspace.types.ts index eb927e789..dd7124dfb 100644 --- a/apps/client/src/features/workspace/types/workspace.types.ts +++ b/apps/client/src/features/workspace/types/workspace.types.ts @@ -29,6 +29,7 @@ export interface IWorkspace { restrictApiToAdmins?: boolean; allowMemberTemplates?: boolean; allowPersonalSpaces?: boolean; + defaultPageEditMode?: string; isScimEnabled?: boolean; } @@ -38,6 +39,7 @@ export interface IWorkspaceSettings { api?: IWorkspaceApiSettings; templates?: IWorkspaceTemplateSettings; spaces?: IWorkspaceSpaceSettings; + defaultPageEditMode?: string; } export interface IWorkspaceApiSettings { diff --git a/apps/client/src/pages/settings/workspace/workspace-settings.tsx b/apps/client/src/pages/settings/workspace/workspace-settings.tsx index e31b41b0f..1dc55485f 100644 --- a/apps/client/src/pages/settings/workspace/workspace-settings.tsx +++ b/apps/client/src/pages/settings/workspace/workspace-settings.tsx @@ -7,6 +7,7 @@ import { Helmet } from "react-helmet-async"; import ManageHostname from "@/ee/components/manage-hostname.tsx"; import { Divider } from "@mantine/core"; import AllowMemberTemplates from "@/ee/security/components/allow-member-templates.tsx"; +import WorkspaceDefaultPageEditMode from "@/features/workspace/components/settings/components/workspace-default-page-edit-mode.tsx"; import PersonalSpacesSetting from "@/ee/personal-space/components/personal-spaces-setting.tsx"; export default function WorkspaceSettings() { @@ -32,6 +33,9 @@ export default function WorkspaceSettings() { )} + + + ); } diff --git a/apps/server/src/core/auth/services/signup.service.ts b/apps/server/src/core/auth/services/signup.service.ts index ab683c977..75207e88d 100644 --- a/apps/server/src/core/auth/services/signup.service.ts +++ b/apps/server/src/core/auth/services/signup.service.ts @@ -4,6 +4,8 @@ import { WorkspaceService } from '../../workspace/services/workspace.service'; import { CreateWorkspaceDto } from '../../workspace/dto/create-workspace.dto'; import { CreateAdminUserDto } from '../dto/create-admin-user.dto'; import { UserRepo } from '@docmost/db/repos/user/user.repo'; +import { WorkspaceRepo } from '@docmost/db/repos/workspace/workspace.repo'; +import { getWorkspaceDefaultPageEditMode } from '../../workspace/workspace.util'; import { KyselyDB, KyselyTransaction } from '@docmost/db/types/kysely.types'; import { executeTx } from '@docmost/db/utils'; import { InjectKysely } from 'nestjs-kysely'; @@ -20,6 +22,7 @@ import { export class SignupService { constructor( private userRepo: UserRepo, + private workspaceRepo: WorkspaceRepo, private workspaceService: WorkspaceService, private groupUserRepo: GroupUserRepo, @InjectKysely() private readonly db: KyselyDB, @@ -46,12 +49,16 @@ export class SignupService { this.db, async (trx) => { // create user + const workspace = await this.workspaceRepo.findById(workspaceId, { + trx, + }); const user = await this.userRepo.insertUser( { ...createUserDto, workspaceId: workspaceId, }, trx, + { pageEditMode: getWorkspaceDefaultPageEditMode(workspace) }, ); // add user to workspace diff --git a/apps/server/src/core/workspace/dto/update-workspace.dto.ts b/apps/server/src/core/workspace/dto/update-workspace.dto.ts index 4d0a650b3..87e969e16 100644 --- a/apps/server/src/core/workspace/dto/update-workspace.dto.ts +++ b/apps/server/src/core/workspace/dto/update-workspace.dto.ts @@ -3,8 +3,10 @@ import { CreateWorkspaceDto } from './create-workspace.dto'; import { IsArray, IsBoolean, + IsIn, IsInt, IsOptional, + IsString, Min, } from 'class-validator'; @@ -61,4 +63,9 @@ export class UpdateWorkspaceDto extends PartialType(CreateWorkspaceDto) { @IsOptional() @IsBoolean() allowPersonalSpaces: boolean; + + @IsOptional() + @IsString() + @IsIn(['read', 'edit']) + defaultPageEditMode: string; } diff --git a/apps/server/src/core/workspace/services/workspace-invitation.service.ts b/apps/server/src/core/workspace/services/workspace-invitation.service.ts index 2bc3d2dd9..8e13b9dfc 100644 --- a/apps/server/src/core/workspace/services/workspace-invitation.service.ts +++ b/apps/server/src/core/workspace/services/workspace-invitation.service.ts @@ -41,7 +41,10 @@ import { AUDIT_SERVICE, IAuditService, } from '../../../integrations/audit/audit.service'; -import { isAdminActingOnOwner } from '../workspace.util'; +import { + getWorkspaceDefaultPageEditMode, + isAdminActingOnOwner, +} from '../workspace.util'; @Injectable() export class WorkspaceInvitationService { @@ -257,6 +260,7 @@ export class WorkspaceInvitationService { workspaceId: workspace.id, }, trx, + { pageEditMode: getWorkspaceDefaultPageEditMode(workspace) }, ); // add user to default group diff --git a/apps/server/src/core/workspace/services/workspace.service.ts b/apps/server/src/core/workspace/services/workspace.service.ts index 8f5e3c993..77ccb90c9 100644 --- a/apps/server/src/core/workspace/services/workspace.service.ts +++ b/apps/server/src/core/workspace/services/workspace.service.ts @@ -527,6 +527,20 @@ export class WorkspaceService { ); } + if (typeof updateWorkspaceDto.defaultPageEditMode !== 'undefined') { + const prev = settingsBefore?.defaultPageEditMode ?? null; + const next = updateWorkspaceDto.defaultPageEditMode.toLowerCase(); + if (prev !== next) { + before.defaultPageEditMode = prev; + after.defaultPageEditMode = next; + } + await this.workspaceRepo.updateDefaultPageEditMode( + workspaceId, + next, + trx, + ); + } + delete updateWorkspaceDto.restrictApiToAdmins; delete updateWorkspaceDto.aiSearch; delete updateWorkspaceDto.generativeAi; @@ -535,6 +549,7 @@ export class WorkspaceService { delete updateWorkspaceDto.allowMemberTemplates; delete updateWorkspaceDto.aiChat; delete updateWorkspaceDto.allowPersonalSpaces; + delete updateWorkspaceDto.defaultPageEditMode; await this.workspaceRepo.updateWorkspace( updateWorkspaceDto, diff --git a/apps/server/src/core/workspace/workspace.util.ts b/apps/server/src/core/workspace/workspace.util.ts index b22aa3ceb..0d385d04e 100644 --- a/apps/server/src/core/workspace/workspace.util.ts +++ b/apps/server/src/core/workspace/workspace.util.ts @@ -6,3 +6,18 @@ export function isAdminActingOnOwner( ): boolean { return authUserRole === UserRole.ADMIN && targetRole === UserRole.OWNER; } + +export type PageEditMode = 'read' | 'edit'; + +export function getWorkspaceDefaultPageEditMode( + workspace: { settings?: unknown } | null | undefined, +): PageEditMode | undefined { + const settings = (workspace?.settings ?? {}) as { + defaultPageEditMode?: unknown; + }; + const mode = settings.defaultPageEditMode; + if (mode === 'read' || mode === 'edit') { + return mode; + } + return undefined; +} diff --git a/apps/server/src/database/repos/user/user.repo.ts b/apps/server/src/database/repos/user/user.repo.ts index 6f9f51cf4..4d619e6c1 100644 --- a/apps/server/src/database/repos/user/user.repo.ts +++ b/apps/server/src/database/repos/user/user.repo.ts @@ -112,6 +112,7 @@ export class UserRepo { async insertUser( insertableUser: InsertableUser, trx?: KyselyTransaction, + opts?: { pageEditMode?: string }, ): Promise { const user: InsertableUser = { name: @@ -126,7 +127,17 @@ export class UserRepo { const db = dbOrTx(this.db, trx); return db .insertInto('users') - .values({ ...insertableUser, ...user }) + .values({ + ...insertableUser, + ...user, + ...(opts?.pageEditMode + ? { + settings: sql`${JSON.stringify({ + preferences: { pageEditMode: opts.pageEditMode }, + })}::text::jsonb`, + } + : {}), + }) .returning(this.baseFields) .executeTakeFirst(); } diff --git a/apps/server/src/database/repos/workspace/workspace.repo.ts b/apps/server/src/database/repos/workspace/workspace.repo.ts index 6bf2d447a..d0f9b5cee 100644 --- a/apps/server/src/database/repos/workspace/workspace.repo.ts +++ b/apps/server/src/database/repos/workspace/workspace.repo.ts @@ -271,4 +271,22 @@ export class WorkspaceRepo { .executeTakeFirst(); } + async updateDefaultPageEditMode( + workspaceId: string, + pageEditMode: string, + trx?: KyselyTransaction, + ) { + const db = dbOrTx(this.db, trx); + return db + .updateTable('workspaces') + .set({ + settings: sql`COALESCE(settings, '{}'::jsonb) + || jsonb_build_object('defaultPageEditMode', ${sql.lit(pageEditMode)})`, + updatedAt: new Date(), + }) + .where('id', '=', workspaceId) + .returning(this.baseFields) + .executeTakeFirst(); + } + } diff --git a/apps/server/src/ee b/apps/server/src/ee index 224d73f92..4669109a1 160000 --- a/apps/server/src/ee +++ b/apps/server/src/ee @@ -1 +1 @@ -Subproject commit 224d73f9256c4a80677d4452ad7ad8b68a387889 +Subproject commit 4669109a18f0c4d53c616e1f23fb8d5ec0b42f51