feat: workspace default edit preference (#2319)

This commit is contained in:
Philip Okugbe
2026-06-30 01:10:51 +01:00
committed by GitHub
parent 96faff7175
commit 15bdf680d9
11 changed files with 157 additions and 3 deletions
@@ -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 (
<Group justify="space-between" wrap="nowrap" gap="xl">
<div>
<Text size="md">{t("Default page edit mode")}</Text>
<Text size="sm" c="dimmed">
{t(
"Choose the page edit mode new members start with. Existing members are not affected.",
)}
</Text>
</div>
<DefaultPageEditModeControl />
</Group>
);
}
function DefaultPageEditModeControl() {
const { t } = useTranslation();
const [workspace, setWorkspace] = useAtom(workspaceAtom);
const defaultPageEditMode =
workspace?.settings?.defaultPageEditMode ?? PageEditMode.Edit;
const [value, setValue] = useState<string>(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 (
<SegmentedControl
aria-label={t("Default page edit mode")}
value={value}
onChange={handleChange}
data={[
{ label: t("Edit"), value: PageEditMode.Edit },
{ label: t("Read"), value: PageEditMode.Read },
]}
/>
);
}
@@ -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 {
@@ -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() {
<ManageHostname />
</>
)}
<Divider my="md" />
<WorkspaceDefaultPageEditMode />
</>
);
}
@@ -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
@@ -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;
}
@@ -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
@@ -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,
@@ -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;
}
@@ -112,6 +112,7 @@ export class UserRepo {
async insertUser(
insertableUser: InsertableUser,
trx?: KyselyTransaction,
opts?: { pageEditMode?: string },
): Promise<User> {
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();
}
@@ -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();
}
}