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 511f36b47..31a788004 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 @@ -529,7 +529,7 @@ const CommandGroups: SlashMenuGroupedItemsType = { .run(); try { - const res = await api.post<{ id: string }>("/bases/inline-embed", { + const res = await api.post<{ id: string }>("/bases/create", { parentPageId, }); diff --git a/apps/server/src/core/base/controllers/base.controller.ts b/apps/server/src/core/base/controllers/base.controller.ts index 072bafb9d..463b9c337 100644 --- a/apps/server/src/core/base/controllers/base.controller.ts +++ b/apps/server/src/core/base/controllers/base.controller.ts @@ -1,4 +1,5 @@ import { + BadRequestException, Body, Controller, ForbiddenException, @@ -19,7 +20,6 @@ import { UpdateBaseDto } from '../dto/update-base.dto'; import { BaseIdDto } from '../dto/base.dto'; import { ExportBaseCsvDto } from '../dto/export-base.dto'; import { ResolvePagesDto } from '../dto/resolve-pages.dto'; -import { InlineEmbedBaseDto } from '../dto/inline-embed-base.dto'; import { AuthUser } from '../../../common/decorators/auth-user.decorator'; import { AuthWorkspace } from '../../../common/decorators/auth-workspace.decorator'; import { JwtAuthGuard } from '../../../common/guards/jwt-auth.guard'; @@ -56,9 +56,38 @@ export class BaseController { @AuthUser() user: User, @AuthWorkspace() workspace: Workspace, ) { - // Bases are pages — use the same SpaceCaslSubject.Page check the - // page controller uses for its own create endpoint, so a single - // role definition (admin/writer/reader) governs both. + // One endpoint, two modes: + // - parentPageId set → inline embed inside that page. Derive + // spaceId/workspaceId from the parent and gate on Edit access + // to the parent (same check the editor uses for any other + // content the user adds while editing). Seed two extra text + // columns + one row so the freshly-inserted base looks like + // a typical database on first paint. + // - parentPageId unset → standalone base. Gate on space-level + // Create, Page (the same check the page controller uses for + // its own create endpoint). + if (dto.parentPageId) { + const parent = await this.pageRepo.findById(dto.parentPageId); + if (!parent) { + throw new NotFoundException('Parent page not found'); + } + await this.pageAccessService.validateCanEdit(parent, user); + + return this.baseService.create( + user.id, + parent.workspaceId, + { + ...dto, + spaceId: parent.spaceId, + }, + { extraTextProperties: 2, defaultRows: 1 }, + ); + } + + if (!dto.spaceId) { + throw new BadRequestException('spaceId or parentPageId is required'); + } + const ability = await this.spaceAbility.createForUser(user, dto.spaceId); if (ability.cannot(SpaceCaslAction.Create, SpaceCaslSubject.Page)) { throw new ForbiddenException(); @@ -169,33 +198,4 @@ export class BaseController { return { items }; } - @HttpCode(HttpStatus.OK) - @Post('inline-embed') - async createInlineEmbed( - @Body() dto: InlineEmbedBaseDto, - @AuthUser() user: User, - ) { - const parent = await this.pageRepo.findById(dto.parentPageId); - if (!parent) { - throw new NotFoundException('Parent page not found'); - } - - await this.pageAccessService.validateCanEdit(parent, user); - - // Inline embeds land mid-document and need to be visually meaningful - // on first paint — a single "Title" column with no rows looks broken. - // Seed two extra text columns and one empty row so the freshly- - // created base looks like a typical database (Title + Text 1 + Text 2, - // one ready-to-fill row). - return this.baseService.create( - user.id, - parent.workspaceId, - { - spaceId: parent.spaceId, - parentPageId: dto.parentPageId, - name: 'Untitled', - }, - { extraTextProperties: 2, defaultRows: 1 }, - ); - } } diff --git a/apps/server/src/core/base/dto/create-base.dto.ts b/apps/server/src/core/base/dto/create-base.dto.ts index 9ffc01595..b0d92a4c3 100644 --- a/apps/server/src/core/base/dto/create-base.dto.ts +++ b/apps/server/src/core/base/dto/create-base.dto.ts @@ -9,8 +9,16 @@ export class CreateBaseDto { @IsOptional() icon?: string; + // Either `spaceId` or `parentPageId` must be supplied. When + // parentPageId is given, the base is being inserted as an inline + // embed inside that page — spaceId/workspaceId are derived from + // the parent and the seeded shape includes a couple of default + // columns + one row. When only spaceId is supplied, the base is a + // standalone page in that space's tree. The controller validates + // the cross-field requirement and picks the right permission gate. @IsUUID() - spaceId!: string; + @IsOptional() + spaceId?: string; @IsUUID() @IsOptional() diff --git a/apps/server/src/core/base/dto/inline-embed-base.dto.ts b/apps/server/src/core/base/dto/inline-embed-base.dto.ts deleted file mode 100644 index 5286d22b9..000000000 --- a/apps/server/src/core/base/dto/inline-embed-base.dto.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { IsUUID } from 'class-validator'; - -export class InlineEmbedBaseDto { - @IsUUID() - parentPageId!: string; -}