import { DatabaseBlockDataSource, databasePropertyConverts, } from '@blocksuite/affine-block-database'; import type { ColumnDataType } from '@blocksuite/affine-model'; import { insertPositionToIndex, type InsertToPosition, } from '@blocksuite/affine-shared/utils'; import type { EditorHost } from '@blocksuite/block-std'; import { DataSourceBase, type PropertyMetaConfig } from '@blocksuite/data-view'; import { propertyPresets } from '@blocksuite/data-view/property-presets'; import { BlockSuiteError } from '@blocksuite/global/exceptions'; import type { Block, Store } from '@blocksuite/store'; import { Subject } from 'rxjs'; import type { BlockMeta } from './block-meta/base.js'; import { blockMetaMap } from './block-meta/index.js'; import { queryBlockAllColumnMap, queryBlockColumns } from './columns/index.js'; import type { DataViewBlockModel } from './data-view-model.js'; export type BlockQueryDataSourceConfig = { type: keyof typeof blockMetaMap; }; // @ts-expect-error FIXME: ts error export class BlockQueryDataSource extends DataSourceBase { private readonly columnMetaMap = new Map< string, PropertyMetaConfig >(); private readonly meta: BlockMeta; blockMap = new Map(); docDisposeMap = new Map void>(); slots = { update: new Subject(), }; private get blocks() { return [...this.blockMap.values()]; } get properties(): string[] { return [ ...this.meta.properties.map(v => v.key), ...this.block.props.columns.map(v => v.id), ]; } get propertyMetas(): PropertyMetaConfig[] { return queryBlockColumns as PropertyMetaConfig[]; } get rows(): string[] { return this.blocks.map(v => v.id); } get workspace() { return this.host.doc.workspace; } constructor( private readonly host: EditorHost, private readonly block: DataViewBlockModel, config: BlockQueryDataSourceConfig ) { super(); this.meta = blockMetaMap[config.type]; for (const property of this.meta.properties) { this.columnMetaMap.set(property.metaConfig.type, property.metaConfig); } for (const collection of this.workspace.docs.values()) { for (const block of Object.values(collection.getStore().blocks.peek())) { if (this.meta.selector(block)) { this.blockMap.set(block.id, block); } } } this.workspace.docs.forEach(doc => { this.listenToDoc(doc.getStore()); }); this.workspace.slots.docCreated.subscribe(id => { const doc = this.workspace.getDoc(id); if (doc) { this.listenToDoc(doc); } }); this.workspace.slots.docRemoved.subscribe(id => { this.docDisposeMap.get(id)?.(); }); } private getProperty(propertyId: string) { const property = this.meta.properties.find(v => v.key === propertyId); if (!property) { throw new BlockSuiteError( BlockSuiteError.ErrorCode.ValueNotExists, `property ${propertyId} not found` ); } return property; } private newColumnName() { let i = 1; while ( this.block.props.columns.some(column => column.name === `Column ${i}`) ) { i++; } return `Column ${i}`; } cellValueChange(rowId: string, propertyId: string, value: unknown): void { const viewColumn = this.getViewColumn(propertyId); if (viewColumn) { this.block.props.cells[rowId] = { ...this.block.props.cells[rowId], [propertyId]: value, }; return; } const block = this.blockMap.get(rowId); if (block) { this.meta.properties .find(v => v.key === propertyId) ?.set?.(block.model, value); } } cellValueGet(rowId: string, propertyId: string): unknown { const viewColumn = this.getViewColumn(propertyId); if (viewColumn) { return this.block.props.cells[rowId]?.[propertyId]; } const block = this.blockMap.get(rowId); if (block) { return this.getProperty(propertyId)?.get(block.model); } return; } getViewColumn(id: string) { return this.block.props.columns.find(v => v.id === id); } listenToDoc(doc: Store) { this.docDisposeMap.set( doc.id, doc.slots.blockUpdated.subscribe(v => { if (v.type === 'add') { const blockById = doc.getBlock(v.id); if (blockById && this.meta.selector(blockById)) { this.blockMap.set(v.id, blockById); } } else if (v.type === 'delete') { this.blockMap.delete(v.id); } this.slots.update.next(undefined); }).unsubscribe ); } propertyAdd( insertToPosition: InsertToPosition, type: string | undefined ): string { const doc = this.block.doc; doc.captureSync(); const column = DatabaseBlockDataSource.propertiesMap.value[ type ?? propertyPresets.multiSelectPropertyConfig.type ].create(this.newColumnName()); const id = doc.workspace.idGenerator(); if (this.block.props.columns.some(v => v.id === id)) { return id; } doc.transact(() => { const col: ColumnDataType = { ...column, id, }; this.block.props.columns.splice( insertPositionToIndex(insertToPosition, this.block.props.columns), 0, col ); }); return id; } propertyDataGet(propertyId: string): Record { const viewColumn = this.getViewColumn(propertyId); if (viewColumn) { return viewColumn.data; } const property = this.getProperty(propertyId); return ( property.getColumnData?.(this.blocks[0].model) ?? property.metaConfig.config.propertyData.default() ); } propertyDataSet(propertyId: string, data: Record): void { const viewColumn = this.getViewColumn(propertyId); if (viewColumn) { viewColumn.data = data; } } propertyDelete(_id: string): void { const index = this.block.props.columns.findIndex(v => v.id === _id); if (index >= 0) { this.block.props.columns.splice(index, 1); } } propertyDuplicate(_columnId: string): string | undefined { throw new Error('Method not implemented.'); } propertyMetaGet(type: string): PropertyMetaConfig { const meta = this.columnMetaMap.get(type); if (meta) { return meta; } return queryBlockAllColumnMap[type]; } propertyNameGet(propertyId: string): string { const viewColumn = this.getViewColumn(propertyId); if (viewColumn) { return viewColumn.name; } if (propertyId === 'type') { return 'Block Type'; } return this.getProperty(propertyId)?.name ?? ''; } propertyNameSet(propertyId: string, name: string): void { const viewColumn = this.getViewColumn(propertyId); if (viewColumn) { viewColumn.name = name; } } override propertyReadonlyGet(propertyId: string): boolean { const viewColumn = this.getViewColumn(propertyId); if (viewColumn) { return false; } if (propertyId === 'type') return true; return this.getProperty(propertyId)?.set == null; } propertyTypeGet(propertyId: string): string { const viewColumn = this.getViewColumn(propertyId); if (viewColumn) { return viewColumn.type; } if (propertyId === 'type') { return 'image'; } return this.getProperty(propertyId).metaConfig.type; } propertyTypeSet(propertyId: string, toType: string): void { const viewColumn = this.getViewColumn(propertyId); if (viewColumn) { const currentType = viewColumn.type; const currentData = viewColumn.data; const rows = this.rows$.value; const currentCells = rows.map(rowId => this.cellValueGet(rowId, propertyId) ); const convertFunction = databasePropertyConverts.find( v => v.from === currentType && v.to === toType )?.convert; const result = convertFunction?.( currentData as any, currentCells as any ) ?? { property: DatabaseBlockDataSource.propertiesMap.value[ toType ].config.propertyData.default(), cells: currentCells.map(() => undefined), }; this.block.doc.captureSync(); viewColumn.type = toType; viewColumn.data = result.property; currentCells.forEach((value, i) => { if (value != null || result.cells[i] != null) { this.block.props.cells[rows[i]] = { ...this.block.props.cells[rows[i]], [propertyId]: result.cells[i], }; } }); } } rowAdd(_insertPosition: InsertToPosition | number): string { throw new Error('Method not implemented.'); } rowDelete(_ids: string[]): void { throw new Error('Method not implemented.'); } rowMove(_rowId: string, _position: InsertToPosition): void {} }