From e9926d5cefa9ab87a06ed88a6498023022459320 Mon Sep 17 00:00:00 2001 From: Philipinho <16838612+Philipinho@users.noreply.github.com> Date: Tue, 28 Apr 2026 10:53:13 +0100 Subject: [PATCH] feat(base): seed inline-embed bases with two extra text columns and one row MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A freshly-created inline-embed used to render with only the primary "Title" column and zero rows — visually it looks more like a broken widget than a database, so users always had to do at least three manual setup steps before the embed conveyed its purpose. BaseService.create now accepts an optional `defaults` arg for `extraTextProperties` and `defaultRows`; both extras are inserted in the same transaction as the page/property/view so a half-built base can never slip out. The inline-embed controller passes { extraTextProperties: 2, defaultRows: 1 } so the embed lands as "Title + Text 1 + Text 2", with one empty row ready to type into. Standalone base creation goes through the same code path with no defaults, so its existing single-Title-column shape is unchanged. --- .../core/base/controllers/base.controller.ts | 20 ++++++-- .../src/core/base/services/base.service.ts | 51 ++++++++++++++++++- 2 files changed, 65 insertions(+), 6 deletions(-) diff --git a/apps/server/src/core/base/controllers/base.controller.ts b/apps/server/src/core/base/controllers/base.controller.ts index 7822e8c7e..83b79572d 100644 --- a/apps/server/src/core/base/controllers/base.controller.ts +++ b/apps/server/src/core/base/controllers/base.controller.ts @@ -177,10 +177,20 @@ export class BaseController { await this.pageAccessService.validateCanEdit(parent, user); - return this.baseService.create(user.id, parent.workspaceId, { - spaceId: parent.spaceId, - parentPageId: dto.parentPageId, - name: 'Untitled', - }); + // 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/services/base.service.ts b/apps/server/src/core/base/services/base.service.ts index f5f00e420..8c21554e5 100644 --- a/apps/server/src/core/base/services/base.service.ts +++ b/apps/server/src/core/base/services/base.service.ts @@ -7,6 +7,7 @@ import { KyselyDB } from '@docmost/db/types/kysely.types'; import { executeTx } from '@docmost/db/utils'; import { BaseRepo } from '@docmost/db/repos/base/base.repo'; import { BasePropertyRepo } from '@docmost/db/repos/base/base-property.repo'; +import { BaseRowRepo } from '@docmost/db/repos/base/base-row.repo'; import { BaseViewRepo } from '@docmost/db/repos/base/base-view.repo'; import { PageService } from '../../page/services/page.service'; import { PageRepo } from '@docmost/db/repos/page/page.repo'; @@ -22,12 +23,18 @@ export class BaseService { @InjectKysely() private readonly db: KyselyDB, private readonly baseRepo: BaseRepo, private readonly basePropertyRepo: BasePropertyRepo, + private readonly baseRowRepo: BaseRowRepo, private readonly baseViewRepo: BaseViewRepo, private readonly pageService: PageService, private readonly pageRepo: PageRepo, ) {} - async create(userId: string, workspaceId: string, dto: CreateBaseDto) { + async create( + userId: string, + workspaceId: string, + dto: CreateBaseDto, + defaults: { extraTextProperties?: number; defaultRows?: number } = {}, + ) { return executeTx(this.db, async (trx) => { const page = await this.pageService.create( userId, @@ -56,6 +63,28 @@ export class BaseService { trx, ); + // Extra default text properties (used by the inline-embed flow so + // a freshly-inserted database renders with a few visible columns + // instead of a single "Title" lane). Positions are chained off + // each other so they keep the requested order. + let lastPropertyPosition = firstPosition; + const extraTextProperties = defaults.extraTextProperties ?? 0; + for (let i = 0; i < extraTextProperties; i++) { + const next = generateJitteredKeyBetween(lastPropertyPosition, null); + await this.basePropertyRepo.insertProperty( + { + pageId: page.id, + name: `Text ${i + 1}`, + type: BasePropertyType.TEXT, + position: next, + isPrimary: false, + workspaceId, + }, + trx, + ); + lastPropertyPosition = next; + } + await this.baseViewRepo.insertView( { pageId: page.id, @@ -69,6 +98,26 @@ export class BaseService { { trx }, ); + // Default empty rows. Same flow rationale as extra properties — + // the inline-embed flow asks for one so the freshly-inserted + // database is interactive on first paint. + let lastRowPosition: string | null = null; + const defaultRows = defaults.defaultRows ?? 0; + for (let i = 0; i < defaultRows; i++) { + const next = generateJitteredKeyBetween(lastRowPosition, null); + await this.baseRowRepo.insertRow( + { + pageId: page.id, + workspaceId, + position: next, + cells: {}, + creatorId: userId, + }, + { trx }, + ); + lastRowPosition = next; + } + return this.baseRepo.findById(page.id, { includeProperties: true, includeViews: true,