feat(base): seed inline-embed bases with two extra text columns and one row

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.
This commit is contained in:
Philipinho
2026-04-28 10:53:13 +01:00
parent 4a25e787fa
commit e9926d5cef
2 changed files with 65 additions and 6 deletions
@@ -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 },
);
}
}
@@ -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,