mirror of
https://github.com/docmost/docmost.git
synced 2026-08-25 15:57:11 +08:00
feat(ee): personal spaces (#2298)
* feat(ee): personal spaces * pref * feat: on-demand only * error notification
This commit is contained in:
@@ -20,6 +20,7 @@ export const Feature = {
|
||||
VIEWER_COMMENTS: 'comment:viewer',
|
||||
TEMPLATES: 'templates',
|
||||
PDF_EXPORT: 'export:pdf',
|
||||
PERSONAL_SPACES: 'spaces:personal',
|
||||
DOCX_EXPORT: 'export:docx',
|
||||
} as const;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
IsAlphanumeric,
|
||||
IsOptional,
|
||||
IsString,
|
||||
Matches,
|
||||
MaxLength,
|
||||
MinLength,
|
||||
} from 'class-validator';
|
||||
@@ -20,6 +20,9 @@ export class CreateSpaceDto {
|
||||
|
||||
@MinLength(2)
|
||||
@MaxLength(100)
|
||||
@IsAlphanumeric()
|
||||
@Matches(/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/, {
|
||||
message:
|
||||
'Space slug must start with a letter or number and may contain hyphens and underscores',
|
||||
})
|
||||
slug: string;
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ export class SpaceService {
|
||||
workspaceId: string,
|
||||
createSpaceDto: CreateSpaceDto,
|
||||
trx?: KyselyTransaction,
|
||||
options?: { isPersonal?: boolean },
|
||||
): Promise<Space> {
|
||||
let space = null;
|
||||
|
||||
@@ -59,6 +60,7 @@ export class SpaceService {
|
||||
workspaceId,
|
||||
createSpaceDto,
|
||||
trx,
|
||||
options,
|
||||
);
|
||||
|
||||
await this.spaceMemberService.addUserToSpace(
|
||||
@@ -81,6 +83,7 @@ export class SpaceService {
|
||||
after: {
|
||||
name: space.name,
|
||||
slug: space.slug,
|
||||
...(space.isPersonal ? { isPersonal: true } : {}),
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -93,6 +96,7 @@ export class SpaceService {
|
||||
workspaceId: string,
|
||||
createSpaceDto: CreateSpaceDto,
|
||||
trx?: KyselyTransaction,
|
||||
options?: { isPersonal?: boolean },
|
||||
): Promise<Space> {
|
||||
const slugExists = await this.spaceRepo.slugExists(
|
||||
createSpaceDto.slug,
|
||||
@@ -112,6 +116,7 @@ export class SpaceService {
|
||||
creatorId: userId,
|
||||
workspaceId: workspaceId,
|
||||
slug: createSpaceDto.slug,
|
||||
isPersonal: options?.isPersonal ?? false,
|
||||
},
|
||||
trx,
|
||||
);
|
||||
|
||||
@@ -57,4 +57,8 @@ export class UpdateWorkspaceDto extends PartialType(CreateWorkspaceDto) {
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
allowMemberTemplates: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
allowPersonalSpaces: boolean;
|
||||
}
|
||||
|
||||
@@ -333,7 +333,8 @@ export class WorkspaceService {
|
||||
typeof updateWorkspaceDto.mcpEnabled !== 'undefined' ||
|
||||
typeof updateWorkspaceDto.restrictApiToAdmins !== 'undefined' ||
|
||||
typeof updateWorkspaceDto.allowMemberTemplates !== 'undefined' ||
|
||||
typeof updateWorkspaceDto.isScimEnabled !== 'undefined'
|
||||
typeof updateWorkspaceDto.isScimEnabled !== 'undefined' ||
|
||||
typeof updateWorkspaceDto.allowPersonalSpaces !== 'undefined'
|
||||
) {
|
||||
const ws = await this.db
|
||||
.selectFrom('workspaces')
|
||||
@@ -361,6 +362,18 @@ export class WorkspaceService {
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof updateWorkspaceDto.allowPersonalSpaces !== 'undefined') {
|
||||
if (
|
||||
!this.licenseCheckService.hasFeature(
|
||||
ws.licenseKey,
|
||||
Feature.PERSONAL_SPACES,
|
||||
ws.plan,
|
||||
)
|
||||
) {
|
||||
throw new ForbiddenException('This feature requires a valid license');
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
typeof updateWorkspaceDto.disablePublicSharing !== 'undefined' ||
|
||||
typeof updateWorkspaceDto.trashRetentionDays !== 'undefined' ||
|
||||
@@ -500,6 +513,20 @@ export class WorkspaceService {
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof updateWorkspaceDto.allowPersonalSpaces !== 'undefined') {
|
||||
const prev = settingsBefore?.spaces?.allowPersonal ?? false;
|
||||
if (prev !== updateWorkspaceDto.allowPersonalSpaces) {
|
||||
before.allowPersonalSpaces = prev;
|
||||
after.allowPersonalSpaces = updateWorkspaceDto.allowPersonalSpaces;
|
||||
}
|
||||
await this.workspaceRepo.updateSpaceSettings(
|
||||
workspaceId,
|
||||
'allowPersonal',
|
||||
updateWorkspaceDto.allowPersonalSpaces,
|
||||
trx,
|
||||
);
|
||||
}
|
||||
|
||||
delete updateWorkspaceDto.restrictApiToAdmins;
|
||||
delete updateWorkspaceDto.aiSearch;
|
||||
delete updateWorkspaceDto.generativeAi;
|
||||
@@ -507,6 +534,7 @@ export class WorkspaceService {
|
||||
delete updateWorkspaceDto.mcpEnabled;
|
||||
delete updateWorkspaceDto.allowMemberTemplates;
|
||||
delete updateWorkspaceDto.aiChat;
|
||||
delete updateWorkspaceDto.allowPersonalSpaces;
|
||||
|
||||
await this.workspaceRepo.updateWorkspace(
|
||||
updateWorkspaceDto,
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Kysely, sql } from 'kysely';
|
||||
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
await db.schema
|
||||
.alterTable('spaces')
|
||||
.addColumn('is_personal', 'boolean', (col) =>
|
||||
col.notNull().defaultTo(false),
|
||||
)
|
||||
.execute();
|
||||
|
||||
await sql`
|
||||
CREATE UNIQUE INDEX spaces_personal_creator_unique
|
||||
ON spaces (creator_id)
|
||||
WHERE is_personal = true AND deleted_at IS NULL
|
||||
`.execute(db);
|
||||
}
|
||||
|
||||
export async function down(db: Kysely<any>): Promise<void> {
|
||||
await db.schema
|
||||
.dropIndex('spaces_personal_creator_unique')
|
||||
.ifExists()
|
||||
.execute();
|
||||
await db.schema.alterTable('spaces').dropColumn('is_personal').execute();
|
||||
}
|
||||
@@ -57,6 +57,22 @@ export class SpaceRepo {
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
async findPersonalSpace(
|
||||
userId: string,
|
||||
workspaceId: string,
|
||||
trx?: KyselyTransaction,
|
||||
): Promise<Space | undefined> {
|
||||
const db = dbOrTx(this.db, trx);
|
||||
return db
|
||||
.selectFrom('spaces')
|
||||
.selectAll('spaces')
|
||||
.where('workspaceId', '=', workspaceId)
|
||||
.where('creatorId', '=', userId)
|
||||
.where('isPersonal', '=', true)
|
||||
.where('deletedAt', 'is', null)
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
async slugExists(
|
||||
slug: string,
|
||||
workspaceId: string,
|
||||
|
||||
@@ -251,4 +251,24 @@ export class WorkspaceRepo {
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
async updateSpaceSettings(
|
||||
workspaceId: string,
|
||||
prefKey: string,
|
||||
prefValue: string | boolean,
|
||||
trx?: KyselyTransaction,
|
||||
) {
|
||||
const db = dbOrTx(this.db, trx);
|
||||
return db
|
||||
.updateTable('workspaces')
|
||||
.set({
|
||||
settings: sql`COALESCE(settings, '{}'::jsonb)
|
||||
|| jsonb_build_object('spaces', COALESCE(settings->'spaces', '{}'::jsonb)
|
||||
|| jsonb_build_object('${sql.raw(prefKey)}', ${sql.lit(prefValue)}))`,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where('id', '=', workspaceId)
|
||||
.returning(this.baseFields)
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+1
@@ -322,6 +322,7 @@ export interface Spaces {
|
||||
deletedAt: Timestamp | null;
|
||||
description: string | null;
|
||||
id: Generated<string>;
|
||||
isPersonal: Generated<boolean>;
|
||||
logo: string | null;
|
||||
name: string | null;
|
||||
settings: Json | null;
|
||||
|
||||
+1
-1
Submodule apps/server/src/ee updated: 7afa4e9f2b...efd4d10dfd
Reference in New Issue
Block a user