import type { Disposable } from '@blockeditor/global/utils'; import { assertExists, Slot } from '@blockeditor/global/utils'; import type { EditorHost } from '@blockeditor/lit'; import type { BlockModel } from '@blockeditor/store'; import { Text, type Y } from '@blockeditor/store'; import { createUniComponentFromWebComponent } from '../../../_common/components/uni-component/uni-component.js'; import type { DatabaseBlockModel } from '../../database-model.js'; import type { InsertToPosition } from '../../types.js'; import { insertPositionToIndex } from '../../utils/insert.js'; import { checkboxColumnConfig } from '../columns/checkbox/cell-renderer.js'; import { dateColumnConfig } from '../columns/date/cell-renderer.js'; import { imageColumnConfig } from '../columns/image/cell-renderer.js'; import { linkColumnConfig } from '../columns/link/cell-renderer.js'; import type { ColumnConfig } from '../columns/manager.js'; import { columnManager } from '../columns/manager.js'; import { multiSelectColumnConfig } from '../columns/multi-select/cell-renderer.js'; import { multiSelectPureColumnConfig } from '../columns/multi-select/define.js'; import { numberColumnConfig } from '../columns/number/cell-renderer.js'; import { progressColumnConfig } from '../columns/progress/cell-renderer.js'; import { richTextColumnConfig } from '../columns/rich-text/cell-renderer.js'; import { selectColumnConfig } from '../columns/select/cell-renderer.js'; import { titleColumnConfig } from '../columns/title/cell-renderer.js'; import type { DatabaseBlockDatasourceConfig, DetailSlots } from './base.js'; import { BaseDataSource } from './base.js'; import { getIcon } from './block-icons.js'; import { BlockRenderer } from './block-renderer.js'; export class DatabaseBlockDatasource extends BaseDataSource { private _model: DatabaseBlockModel; private _batch = 0; get page() { return this._model.page; } constructor( private host: EditorHost, config: DatabaseBlockDatasourceConfig ) { super(); this._model = host.page.workspace .getPage(config.pageId) ?.getBlockById(config.blockId) as DatabaseBlockModel; this._model.childrenUpdated.pipe(this.slots.update); } public get rows(): string[] { return this._model.children.map(v => v.id); } public get properties(): string[] { return [...this._model.columns.map(column => column.id)]; } public slots = { update: new Slot(), }; private _runCapture() { if (this._batch) { return; } this._batch = requestAnimationFrame(() => { this.page.captureSync(); this._batch = 0; }); } public cellChangeValue( rowId: string, propertyId: string, value: unknown ): void { this._runCapture(); const type = this.propertyGetType(propertyId); if (type === 'title' && typeof value === 'string') { const text = this.getModelById(rowId)?.text; if (text) { text.replace(0, text.length, value); } this.slots.update.emit(); return; } else if (type === 'rich-text' && typeof value === 'string') { const cell = this._model.getCell(rowId, propertyId); const yText = cell?.value as Y.Text | undefined; if (yText) { const text = new Text(yText); text.replace(0, text.length, value); } return; } if (this._model.columns.find(v => v.id === propertyId)) { this._model.updateCell(rowId, { columnId: propertyId, value, }); this._model.applyColumnUpdate(); } } public cellGetValue(rowId: string, propertyId: string): unknown { if (propertyId === 'type') { const model = this.getModelById(rowId); if (!model) { return; } return getIcon(model); } const type = this.propertyGetType(propertyId); if (type === 'title') { const model = this.getModelById(rowId); if (model) { return model.text?.yText; } return; } return this._model.getCell(rowId, propertyId)?.value; } override cellGetExtra(rowId: string, propertyId: string): unknown { if (this.propertyGetType(propertyId) === 'title') { const model = this.getModelById(rowId); if (model) { return { result: this.host.renderModel(model), model, }; } } return super.cellGetExtra(rowId, propertyId); } private getModelById(rowId: string): BlockModel | undefined { return this._model.children[this._model.childMap.get(rowId) ?? -1]; } public override cellGetRenderValue( rowId: string, propertyId: string ): unknown { return this.cellGetValue(rowId, propertyId); } private newColumnName() { let i = 1; while (this._model.columns.find(column => column.name === `Column ${i}`)) { i++; } return `Column ${i}`; } public propertyAdd( insertToPosition: InsertToPosition, type?: string ): string { this.page.captureSync(); return this._model.addColumn( insertToPosition, columnManager .getColumn(type ?? multiSelectPureColumnConfig.type) .create(this.newColumnName()) ); } public propertyChangeData( propertyId: string, data: Record ): void { this._runCapture(); this._model.updateColumn(propertyId, () => ({ data })); this._model.applyColumnUpdate(); } public propertyChangeName(propertyId: string, name: string): void { this.page.captureSync(); this._model.updateColumn(propertyId, () => ({ name })); this._model.applyColumnUpdate(); } public propertyChangeType(propertyId: string, toType: string): void { const currentType = this.propertyGetType(propertyId); const currentData = this.propertyGetData(propertyId); const rows = this.rows; const currentCells = rows.map(rowId => this.cellGetValue(rowId, propertyId) ); const result = columnManager .getColumn(currentType) ?.convertCell(toType, currentData, currentCells) ?? { column: columnManager.getColumn(toType).defaultData(), cells: currentCells.map(() => undefined), }; this.page.captureSync(); this._model.updateColumn(propertyId, () => ({ type: toType, data: result.column, })); const cells: Record = {}; currentCells.forEach((value, i) => { if (value != null || result.cells[i] != null) { cells[rows[i]] = result.cells[i]; } }); this._model.updateCells(propertyId, cells); this._model.applyColumnUpdate(); } public propertyDelete(id: string): void { this.page.captureSync(); const index = this._model.findColumnIndex(id); if (index < 0) return; this.page.transact(() => { this._model.columns.splice(index, 1); }); this._model.applyColumnUpdate(); } public propertyGetData(propertyId: string): Record { return this._model.columns.find(v => v.id === propertyId)?.data ?? {}; } public override propertyGetReadonly(propertyId: string): boolean { if (propertyId === 'type') return true; return false; } public propertyGetName(propertyId: string): string { if (propertyId === 'type') { return 'Block Type'; } return this._model.columns.find(v => v.id === propertyId)?.name ?? ''; } public propertyGetType(propertyId: string): string { if (propertyId === 'type') { return 'image'; } return this._model.columns.find(v => v.id === propertyId)?.type ?? ''; } public propertyDuplicate(columnId: string): string { this.page.captureSync(); const currentSchema = this._model.getColumn(columnId); assertExists(currentSchema); const { id: copyId, ...nonIdProps } = currentSchema; const schema = { ...nonIdProps }; const id = this._model.addColumn( { before: false, id: columnId, }, schema ); this._model.copyCellsByColumn(copyId, id); this._model.applyColumnUpdate(); return id; } public rowAdd(insertPosition: InsertToPosition | number): string { this.page.captureSync(); const index = typeof insertPosition === 'number' ? insertPosition : insertPositionToIndex(insertPosition, this._model.children); return this.page.addBlock('affine:paragraph', {}, this._model.id, index); } public rowDelete(ids: string[]): void { this.page.captureSync(); this.page.updateBlock(this._model, { children: this._model.children.filter(v => !ids.includes(v.id)), }); } public override captureSync(): void { this.page.captureSync(); } public override propertyGetDefaultWidth(propertyId: string): number { if (this.propertyGetType(propertyId) === 'title') { return 260; } return super.propertyGetDefaultWidth(propertyId); } override onCellUpdate( rowId: string, propertyId: string, callback: () => void ): Disposable { if (this.propertyGetType(propertyId) === 'title') { this.getModelById(rowId)?.text?.yText.observe(callback); return { dispose: () => { this.getModelById(rowId)?.text?.yText.unobserve(callback); }, }; } return this._model.propsUpdated.on(callback); } // eslint-disable-next-line @typescript-eslint/no-explicit-any public get allPropertyConfig(): ColumnConfig[] { return [ dateColumnConfig, numberColumnConfig, progressColumnConfig, selectColumnConfig, multiSelectColumnConfig, richTextColumnConfig, linkColumnConfig, checkboxColumnConfig, ]; } public override get detailSlots(): DetailSlots { return { ...super.detailSlots, header: createUniComponentFromWebComponent(BlockRenderer), }; } public rowMove(rowId: string, position: InsertToPosition): void { const model = this.page.getBlockById(rowId); if (model) { const index = insertPositionToIndex(position, this._model.children); const target = this._model.children[index]; this.page.moveBlocks([model], this._model, target); } } } export const hiddenColumn = [titleColumnConfig, imageColumnConfig];