From 01151ec18f5d06467cfd4cf17f768bbcd9eb091d Mon Sep 17 00:00:00 2001 From: zzj3720 <17165520+zzj3720@users.noreply.github.com> Date: Wed, 12 Mar 2025 09:22:41 +0000 Subject: [PATCH] refactor(editor): add runtime type checks to database cell values (#10770) --- .../block-data-view/src/block-meta/base.ts | 9 +- .../block-data-view/src/block-meta/todo.ts | 47 ------- .../block-data-view/src/columns/index.ts | 9 +- .../blocks/block-data-view/src/data-source.ts | 5 +- .../block-database/src/adapters/utils.ts | 2 +- .../blocks/block-database/src/data-source.ts | 85 ++++++++---- .../src/properties/link/cell-renderer.ts | 2 +- .../src/properties/link/define.ts | 30 ++-- .../src/properties/rich-text/cell-renderer.ts | 2 +- .../src/properties/rich-text/define.ts | 96 +++++++------ .../src/properties/title/define.ts | 120 +++++++++------- .../src/properties/title/text.ts | 2 +- .../data-view/src/core/data-source/base.ts | 11 +- .../data-view/src/core/group-by/default.ts | 2 +- .../data-view/src/core/group-by/trait.ts | 7 +- .../data-view/src/core/group-by/types.ts | 23 ++-- .../src/core/logical/type-presets.ts | 1 - .../data-view/src/core/property/base-cell.ts | 11 +- .../data-view/src/core/property/convert.ts | 10 +- .../data-view/src/core/property/manager.ts | 18 ++- .../src/core/property/property-config.ts | 35 +++-- .../data-view/src/core/property/renderer.ts | 10 +- .../data-view/src/core/property/types.ts | 130 +++++++++--------- .../data-view/src/core/property/utils.ts | 30 ++++ .../data-view/src/core/statistics/any.ts | 34 +++-- .../data-view/src/core/view-manager/cell.ts | 43 +++--- .../src/core/view-manager/property.ts | 24 ++-- .../src/core/view-manager/single-view.ts | 105 ++++++++------ blocksuite/affine/data-view/src/effects.ts | 4 +- .../src/property-presets/checkbox/define.ts | 29 ++-- .../property-presets/date/cell-renderer.ts | 2 +- .../src/property-presets/date/define.ts | 33 +++-- .../property-presets/image/cell-renderer.ts | 4 +- .../src/property-presets/image/define.ts | 34 +++-- .../multi-select/cell-renderer.ts | 1 + .../property-presets/multi-select/define.ts | 70 +++++----- .../property-presets/number/cell-renderer.ts | 1 + .../src/property-presets/number/define.ts | 38 ++--- .../src/property-presets/number/types.ts | 13 +- .../number/utils/formatter.ts | 23 ++-- .../progress/cell-renderer.ts | 2 +- .../src/property-presets/progress/define.ts | 32 +++-- .../property-presets/select/cell-renderer.ts | 6 +- .../src/property-presets/select/define.ts | 108 ++++++++------- .../property-presets/text/cell-renderer.ts | 2 +- .../src/property-presets/text/define.ts | 30 ++-- .../kanban/kanban-view-manager.ts | 16 ++- .../table/mobile/column-header.ts | 8 +- .../table/pc/header/database-header-column.ts | 8 +- .../table/stats/column-stats-column.ts | 2 +- .../view-presets/table/table-view-manager.ts | 5 +- .../playground/apps/starter/data/database.ts | 24 ++-- .../e2e/database/database.spec.ts | 3 + .../tests-legacy/e2e/utils/actions/misc.ts | 3 + 54 files changed, 775 insertions(+), 629 deletions(-) create mode 100644 blocksuite/affine/data-view/src/core/property/utils.ts diff --git a/blocksuite/affine/blocks/block-data-view/src/block-meta/base.ts b/blocksuite/affine/blocks/block-data-view/src/block-meta/base.ts index 8dc164909..f4fdeb6bf 100644 --- a/blocksuite/affine/blocks/block-data-view/src/block-meta/base.ts +++ b/blocksuite/affine/blocks/block-data-view/src/block-meta/base.ts @@ -4,16 +4,17 @@ import type { Block, BlockModel } from '@blocksuite/store'; type PropertyMeta< T extends BlockModel = BlockModel, - Value = unknown, + RawValue = unknown, + JsonValue = unknown, ColumnData extends NonNullable = NonNullable, > = { name: string; key: string; - metaConfig: PropertyMetaConfig; + metaConfig: PropertyMetaConfig; getColumnData?: (block: T) => ColumnData; setColumnData?: (block: T, data: ColumnData) => void; - get: (block: T) => Value; - set?: (block: T, value: Value) => void; + get: (block: T) => RawValue; + set?: (block: T, value: RawValue) => void; updated: (block: T, callback: () => void) => DisposableMember; }; export type BlockMeta = { diff --git a/blocksuite/affine/blocks/block-data-view/src/block-meta/todo.ts b/blocksuite/affine/blocks/block-data-view/src/block-meta/todo.ts index d106a7da3..334dbbecb 100644 --- a/blocksuite/affine/blocks/block-data-view/src/block-meta/todo.ts +++ b/blocksuite/affine/blocks/block-data-view/src/block-meta/todo.ts @@ -1,6 +1,4 @@ -import { richTextColumnConfig } from '@blocksuite/affine-block-database'; import { type ListBlockModel, ListBlockSchema } from '@blocksuite/affine-model'; -import { propertyPresets } from '@blocksuite/data-view/property-presets'; import { createBlockMeta } from './base.js'; @@ -13,48 +11,3 @@ export const todoMeta = createBlockMeta({ return (block.model as ListBlockModel).type === 'todo'; }, }); -todoMeta.addProperty({ - name: 'Content', - key: 'todo-title', - metaConfig: richTextColumnConfig, - get: block => block.text.yText, - set: (_block, _value) => { - // - }, - updated: (block, callback) => { - block.text?.yText.observe(callback); - return { - dispose: () => { - block.text?.yText.unobserve(callback); - }, - }; - }, -}); -todoMeta.addProperty({ - name: 'Checked', - key: 'todo-checked', - metaConfig: propertyPresets.checkboxPropertyConfig, - get: block => block.checked, - set: (block, value) => { - block.checked = value ?? false; - }, - updated: (block, callback) => { - return block.propsUpdated.subscribe(({ key }) => { - if (key === 'checked') { - callback(); - } - }); - }, -}); - -todoMeta.addProperty({ - name: 'Source', - key: 'todo-source', - metaConfig: propertyPresets.textPropertyConfig, - get: block => block.doc.meta?.title ?? '', - updated: (block, callback) => { - return block.doc.workspace.slots.docListUpdated.subscribe(() => { - callback(); - }); - }, -}); diff --git a/blocksuite/affine/blocks/block-data-view/src/columns/index.ts b/blocksuite/affine/blocks/block-data-view/src/columns/index.ts index dfae61b7f..bc8e2ee84 100644 --- a/blocksuite/affine/blocks/block-data-view/src/columns/index.ts +++ b/blocksuite/affine/blocks/block-data-view/src/columns/index.ts @@ -10,9 +10,12 @@ export const queryBlockColumns = [ propertyPresets.multiSelectPropertyConfig, propertyPresets.checkboxPropertyConfig, ]; -export const queryBlockHiddenColumns: PropertyMetaConfig[] = [ - richTextColumnConfig, -]; +export const queryBlockHiddenColumns: PropertyMetaConfig< + string, + any, + any, + any +>[] = [richTextColumnConfig]; const queryBlockAllColumns = [...queryBlockColumns, ...queryBlockHiddenColumns]; export const queryBlockAllColumnMap = Object.fromEntries( queryBlockAllColumns.map(v => [v.type, v as PropertyMetaConfig]) diff --git a/blocksuite/affine/blocks/block-data-view/src/data-source.ts b/blocksuite/affine/blocks/block-data-view/src/data-source.ts index b813b0d3d..6680c30f4 100644 --- a/blocksuite/affine/blocks/block-data-view/src/data-source.ts +++ b/blocksuite/affine/blocks/block-data-view/src/data-source.ts @@ -199,7 +199,7 @@ export class BlockQueryDataSource extends DataSourceBase { const property = this.getProperty(propertyId); return ( property.getColumnData?.(this.blocks[0].model) ?? - property.metaConfig.config.defaultData() + property.metaConfig.config.propertyData.default() ); } @@ -284,7 +284,8 @@ export class BlockQueryDataSource extends DataSourceBase { currentCells as any ) ?? { - property: databaseBlockAllPropertyMap[toType].config.defaultData(), + property: + databaseBlockAllPropertyMap[toType].config.propertyData.default(), cells: currentCells.map(() => undefined), }; this.block.doc.captureSync(); diff --git a/blocksuite/affine/blocks/block-database/src/adapters/utils.ts b/blocksuite/affine/blocks/block-database/src/adapters/utils.ts index 9b605d50f..c1299d768 100644 --- a/blocksuite/affine/blocks/block-database/src/adapters/utils.ts +++ b/blocksuite/affine/blocks/block-database/src/adapters/utils.ts @@ -94,7 +94,7 @@ export const processTable = ( if (isDelta(cell.value)) { value = cell.value; } else { - value = property.config.cellToString({ + value = property.config.rawValue.toString({ value: cell.value, data: col.data, }); diff --git a/blocksuite/affine/blocks/block-database/src/data-source.ts b/blocksuite/affine/blocks/block-database/src/data-source.ts index a6b90d954..2c8cc0c2a 100644 --- a/blocksuite/affine/blocks/block-database/src/data-source.ts +++ b/blocksuite/affine/blocks/block-database/src/data-source.ts @@ -107,7 +107,7 @@ export class DatabaseBlockDataSource extends DataSourceBase { return this._model.doc; } - allPropertyMetas$ = computed[]>(() => { + allPropertyMetas$ = computed[]>(() => { return databaseBlockPropertyList; }); @@ -151,23 +151,27 @@ export class DatabaseBlockDataSource extends DataSourceBase { this._runCapture(); const type = this.propertyTypeGet(propertyId); - const update = this.propertyMetaGet(type).config.valueUpdate; - let newValue = value; - if (update) { - const old = this.cellValueGet(rowId, propertyId); - newValue = update({ - value: old, - data: this.propertyDataGet(propertyId), - dataSource: this, - newValue: value, + const update = this.propertyMetaGet(type)?.config.rawValue.setValue; + const old = this.cellValueGet(rowId, propertyId); + const updateFn = + update ?? + (({ setValue, newValue }) => { + setValue(newValue); }); - } - if (this._model.columns$.value.some(v => v.id === propertyId)) { - updateCell(this._model, rowId, { - columnId: propertyId, - value: newValue, - }); - } + updateFn({ + value: old, + data: this.propertyDataGet(propertyId), + dataSource: this, + newValue: value, + setValue: newValue => { + if (this._model.columns$.value.some(v => v.id === propertyId)) { + updateCell(this._model, rowId, { + columnId: propertyId, + value: newValue, + }); + } + }, + }); } cellValueGet(rowId: string, propertyId: string): unknown { @@ -183,14 +187,32 @@ export class DatabaseBlockDataSource extends DataSourceBase { const model = this.getModelById(rowId); return model?.text; } - return getCell(this._model, rowId, propertyId)?.value; + const meta = this.propertyMetaGet(type); + if (!meta) { + return; + } + const rawValue = + getCell(this._model, rowId, propertyId)?.value ?? + meta.config.rawValue.default(); + const schema = meta.config.rawValue.schema; + const result = schema.safeParse(rawValue); + if (result.success) { + return result.data; + } + return; } - propertyAdd(insertToPosition: InsertToPosition, type?: string): string { + propertyAdd( + insertToPosition: InsertToPosition, + type?: string + ): string | undefined { this.doc.captureSync(); const property = this.propertyMetaGet( type ?? propertyPresets.multiSelectPropertyConfig.type ); + if (!property) { + return; + } const result = addProperty( this._model, insertToPosition, @@ -233,6 +255,9 @@ export class DatabaseBlockDataSource extends DataSourceBase { } if (this.isFixedProperty(propertyId)) { const meta = this.propertyMetaGet(propertyId); + if (!meta) { + return; + } const defaultData = meta.config.fixed?.defaultData ?? {}; return { column: { @@ -288,7 +313,10 @@ export class DatabaseBlockDataSource extends DataSourceBase { } const { column } = result; const meta = this.propertyMetaGet(column.type); - return meta.config.type({ + if (!meta) { + return; + } + return meta.config?.jsonValue.type({ data: column.data, dataSource: this, }); @@ -335,15 +363,8 @@ export class DatabaseBlockDataSource extends DataSourceBase { return id; } - propertyMetaGet(type: string): PropertyMetaConfig { - const property = databaseBlockAllPropertyMap[type]; - if (!property) { - throw new BlockSuiteError( - ErrorCode.DatabaseBlockError, - `Unknown property type: ${type}` - ); - } - return property; + propertyMetaGet(type: string): PropertyMetaConfig | undefined { + return databaseBlockAllPropertyMap[type]; } propertyNameGet(propertyId: string): string { @@ -382,6 +403,10 @@ export class DatabaseBlockDataSource extends DataSourceBase { if (this.isFixedProperty(propertyId)) { return; } + const meta = this.propertyMetaGet(toType); + if (!meta) { + return; + } const currentType = this.propertyTypeGet(propertyId); const currentData = this.propertyDataGet(propertyId); const rows = this.rows$.value; @@ -396,7 +421,7 @@ export class DatabaseBlockDataSource extends DataSourceBase { currentCells as any ) ?? { - property: this.propertyMetaGet(toType).config.defaultData(), + property: meta.config.propertyData.default(), cells: currentCells.map(() => undefined), }; this.doc.captureSync(); diff --git a/blocksuite/affine/blocks/block-database/src/properties/link/cell-renderer.ts b/blocksuite/affine/blocks/block-database/src/properties/link/cell-renderer.ts index de1bbde27..bbba75918 100644 --- a/blocksuite/affine/blocks/block-database/src/properties/link/cell-renderer.ts +++ b/blocksuite/affine/blocks/block-database/src/properties/link/cell-renderer.ts @@ -29,7 +29,7 @@ import { } from './cell-renderer.css.js'; import { linkPropertyModelConfig } from './define.js'; -export class LinkCell extends BaseCellRenderer { +export class LinkCell extends BaseCellRenderer { protected override firstUpdated(_changedProperties: PropertyValues) { super.firstUpdated(_changedProperties); this.classList.add(linkCellStyle); diff --git a/blocksuite/affine/blocks/block-database/src/properties/link/define.ts b/blocksuite/affine/blocks/block-database/src/properties/link/define.ts index 12203ddd2..6556cd9c6 100644 --- a/blocksuite/affine/blocks/block-database/src/properties/link/define.ts +++ b/blocksuite/affine/blocks/block-database/src/properties/link/define.ts @@ -3,17 +3,23 @@ import zod from 'zod'; export const linkColumnType = propertyType('link'); export const linkPropertyModelConfig = linkColumnType.modelConfig({ name: 'Link', - valueSchema: zod.string().optional(), - type: () => t.string.instance(), - defaultData: () => ({}), - cellToString: ({ value }) => value?.toString() ?? '', - cellFromString: ({ value }) => { - return { - value: value, - }; + propertyData: { + schema: zod.object({}), + default: () => ({}), + }, + jsonValue: { + schema: zod.string(), + type: () => t.string.instance(), + isEmpty: ({ value }) => !value, + }, + rawValue: { + schema: zod.string(), + default: () => '', + toString: ({ value }) => value, + fromString: ({ value }) => { + return { value: value }; + }, + toJson: ({ value }) => value, + fromJson: ({ value }) => value, }, - cellToJson: ({ value }) => value ?? null, - cellFromJson: ({ value }) => (typeof value !== 'string' ? undefined : value), - - isEmpty: ({ value }) => value == null || value.length == 0, }); diff --git a/blocksuite/affine/blocks/block-database/src/properties/rich-text/cell-renderer.ts b/blocksuite/affine/blocks/block-database/src/properties/rich-text/cell-renderer.ts index aa2ac45ab..94a39e0d9 100644 --- a/blocksuite/affine/blocks/block-database/src/properties/rich-text/cell-renderer.ts +++ b/blocksuite/affine/blocks/block-database/src/properties/rich-text/cell-renderer.ts @@ -81,7 +81,7 @@ function toggleStyle( inlineEditor.syncInlineRange(); } -export class RichTextCell extends BaseCellRenderer { +export class RichTextCell extends BaseCellRenderer { inlineEditor$ = computed(() => { return this.richText$.value?.inlineEditor; }); diff --git a/blocksuite/affine/blocks/block-database/src/properties/rich-text/define.ts b/blocksuite/affine/blocks/block-database/src/properties/rich-text/define.ts index 1bb0b90ab..b861ba2d9 100644 --- a/blocksuite/affine/blocks/block-database/src/properties/rich-text/define.ts +++ b/blocksuite/affine/blocks/block-database/src/properties/rich-text/define.ts @@ -19,51 +19,59 @@ export const toYText = (text?: RichTextCellType): undefined | Text['yText'] => { export const richTextPropertyModelConfig = richTextColumnType.modelConfig({ name: 'Text', - valueSchema: zod - .custom( - data => data instanceof Text || data instanceof Y.Text - ) - .optional(), - type: () => t.richText.instance(), - defaultData: () => ({}), - cellToString: ({ value }) => value?.toString() ?? '', - cellFromString: ({ value }) => { - return { - value: new Text(value), - }; + propertyData: { + schema: zod.object({}), + default: () => ({}), }, - cellToJson: ({ value, dataSource }) => { - if (!value) return null; - const host = dataSource.contextGet(HostContextKey); - if (host) { - const collection = host.std.workspace; + jsonValue: { + schema: zod.string(), + type: () => t.richText.instance(), + isEmpty: ({ value }) => !value, + }, + rawValue: { + schema: zod + .custom( + data => data instanceof Text || data instanceof Y.Text + ) + .optional(), + default: () => undefined, + toString: ({ value }) => value?.toString() ?? '', + fromString: ({ value }) => { + return { + value: new Text(value), + }; + }, + toJson: ({ value, dataSource }) => { + if (!value) return null; + const host = dataSource.contextGet(HostContextKey); + if (host) { + const collection = host.std.workspace; + const yText = toYText(value); + const deltas = yText?.toDelta(); + const text = deltas + .map((delta: DeltaInsert) => { + if (isLinkedDoc(delta)) { + const linkedDocId = delta.attributes?.reference?.pageId as string; + return collection.getDoc(linkedDocId)?.meta?.title; + } + return delta.insert; + }) + .join(''); + return text; + } + return value?.toString() ?? null; + }, + fromJson: ({ value }) => + typeof value !== 'string' ? undefined : new Text(value), + onUpdate: ({ value, callback }) => { const yText = toYText(value); - const deltas = yText?.toDelta(); - const text = deltas - .map((delta: DeltaInsert) => { - if (isLinkedDoc(delta)) { - const linkedDocId = delta.attributes?.reference?.pageId as string; - return collection.getDoc(linkedDocId)?.meta?.title; - } - return delta.insert; - }) - .join(''); - return text; - } - return value?.toString() ?? null; + yText?.observe(callback); + callback(); + return { + dispose: () => { + yText?.unobserve(callback); + }, + }; + }, }, - cellFromJson: ({ value }) => - typeof value !== 'string' ? undefined : new Text(value), - onUpdate: ({ value, callback }) => { - const yText = toYText(value); - yText?.observe(callback); - callback(); - return { - dispose: () => { - yText?.unobserve(callback); - }, - }; - }, - isEmpty: ({ value }) => value == null || value.length === 0, - values: ({ value }) => (value?.toString() ? [value.toString()] : []), }); diff --git a/blocksuite/affine/blocks/block-database/src/properties/title/define.ts b/blocksuite/affine/blocks/block-database/src/properties/title/define.ts index a1552bd4a..6450fae4f 100644 --- a/blocksuite/affine/blocks/block-database/src/properties/title/define.ts +++ b/blocksuite/affine/blocks/block-database/src/properties/title/define.ts @@ -1,5 +1,6 @@ import { propertyType, t } from '@blocksuite/data-view'; import { Text } from '@blocksuite/store'; +import { Doc } from 'yjs'; import zod from 'zod'; import { HostContextKey } from '../../context/host-context.js'; @@ -9,61 +10,74 @@ export const titleColumnType = propertyType('title'); export const titlePropertyModelConfig = titleColumnType.modelConfig({ name: 'Title', - valueSchema: zod.custom(data => data instanceof Text).optional(), + propertyData: { + schema: zod.object({}), + default: () => ({}), + }, + jsonValue: { + schema: zod.string(), + type: () => t.richText.instance(), + isEmpty: ({ value }) => !value, + }, + rawValue: { + schema: zod.custom(data => data instanceof Text).optional(), + default: () => undefined, + toString: ({ value }) => value?.toString() ?? '', + fromString: ({ value }) => { + return { value: new Text(value) }; + }, + toJson: ({ value, dataSource }) => { + if (!value) return ''; + const host = dataSource.contextGet(HostContextKey); + if (host) { + const collection = host.std.workspace; + const deltas = value.deltas$.value; + const text = deltas + .map(delta => { + if (isLinkedDoc(delta)) { + const linkedDocId = delta.attributes?.reference?.pageId as string; + return collection.getDoc(linkedDocId)?.meta?.title; + } + return delta.insert; + }) + .join(''); + return text; + } + return value?.toString() ?? ''; + }, + fromJson: ({ value }) => new Text(value), + onUpdate: ({ value, callback }) => { + value?.yText.observe(callback); + callback(); + return { + dispose: () => { + value?.yText.unobserve(callback); + }, + }; + }, + setValue: ({ value, newValue }) => { + if (value == null) { + return; + } + const v = newValue as unknown; + if (v == null) { + value.replace(0, value.length, ''); + return; + } + if (typeof v === 'string') { + value.replace(0, value.length, v); + return; + } + if (newValue instanceof Text) { + new Doc().getMap('root').set('text', newValue.yText); + value.clear(); + value.applyDelta(newValue.toDelta()); + return; + } + }, + }, fixed: { defaultData: {}, defaultShow: true, }, - type: () => t.richText.instance(), - defaultData: () => ({}), - cellToString: ({ value }) => value?.toString() ?? '', - cellFromString: ({ value }) => { - return { - value: value, - }; - }, - cellToJson: ({ value, dataSource }) => { - if (!value) return null; - const host = dataSource.contextGet(HostContextKey); - if (host) { - const collection = host.std.workspace; - const deltas = value.deltas$.value; - const text = deltas - .map(delta => { - if (isLinkedDoc(delta)) { - const linkedDocId = delta.attributes?.reference?.pageId as string; - return collection.getDoc(linkedDocId)?.meta?.title; - } - return delta.insert; - }) - .join(''); - return text; - } - return value?.toString() ?? null; - }, - cellFromJson: ({ value }) => - typeof value !== 'string' ? undefined : new Text(value), - onUpdate: ({ value, callback }) => { - value?.yText.observe(callback); - callback(); - return { - dispose: () => { - value?.yText.unobserve(callback); - }, - }; - }, - valueUpdate: ({ value, newValue }) => { - const v = newValue as unknown; - if (typeof v === 'string') { - value?.replace(0, value.length, v); - return value; - } - if (v == null) { - value?.replace(0, value.length, ''); - return value; - } - return newValue; - }, - isEmpty: ({ value }) => value == null || value.length === 0, - values: ({ value }) => (value?.toString() ? [value.toString()] : []), }); diff --git a/blocksuite/affine/blocks/block-database/src/properties/title/text.ts b/blocksuite/affine/blocks/block-database/src/properties/title/text.ts index 1151cb008..6da55b2f8 100644 --- a/blocksuite/affine/blocks/block-database/src/properties/title/text.ts +++ b/blocksuite/affine/blocks/block-database/src/properties/title/text.ts @@ -29,7 +29,7 @@ import { titleRichTextStyle, } from './cell-renderer.css.js'; -export class HeaderAreaTextCell extends BaseCellRenderer { +export class HeaderAreaTextCell extends BaseCellRenderer { activity = true; docId$ = signal(); diff --git a/blocksuite/affine/data-view/src/core/data-source/base.ts b/blocksuite/affine/data-view/src/core/data-source/base.ts index ca8149c39..5de1190fb 100644 --- a/blocksuite/affine/data-view/src/core/data-source/base.ts +++ b/blocksuite/affine/data-view/src/core/data-source/base.ts @@ -53,8 +53,11 @@ export interface DataSource { propertyReadonlyGet(propertyId: string): boolean; propertyReadonlyGet$(propertyId: string): ReadonlySignal; - propertyMetaGet(type: string): PropertyMetaConfig; - propertyAdd(insertToPosition: InsertToPosition, type?: string): string; + propertyMetaGet(type: string): PropertyMetaConfig | undefined; + propertyAdd( + insertToPosition: InsertToPosition, + type?: string + ): string | undefined; propertyDuplicate(propertyId: string): string | undefined; propertyCanDuplicate(propertyId: string): boolean; @@ -152,7 +155,7 @@ export abstract class DataSourceBase implements DataSource { abstract propertyAdd( insertToPosition: InsertToPosition, type?: string - ): string; + ): string | undefined; abstract propertyDataGet(propertyId: string): Record; @@ -179,7 +182,7 @@ export abstract class DataSourceBase implements DataSource { abstract propertyDuplicate(propertyId: string): string | undefined; - abstract propertyMetaGet(type: string): PropertyMetaConfig; + abstract propertyMetaGet(type: string): PropertyMetaConfig | undefined; abstract propertyNameGet(propertyId: string): string; diff --git a/blocksuite/affine/data-view/src/core/group-by/default.ts b/blocksuite/affine/data-view/src/core/group-by/default.ts index c1f62474f..97a74f7e0 100644 --- a/blocksuite/affine/data-view/src/core/group-by/default.ts +++ b/blocksuite/affine/data-view/src/core/group-by/default.ts @@ -10,7 +10,7 @@ export const defaultGroupBy = ( data: NonNullable ): GroupBy | undefined => { const name = groupByMatcher.match( - propertyMeta.config.type({ data, dataSource }) + propertyMeta.config.jsonValue.type({ data, dataSource }) )?.name; return name != null ? { diff --git a/blocksuite/affine/data-view/src/core/group-by/trait.ts b/blocksuite/affine/data-view/src/core/group-by/trait.ts index f758d9dc2..1fa35ecd6 100644 --- a/blocksuite/affine/data-view/src/core/group-by/trait.ts +++ b/blocksuite/affine/data-view/src/core/group-by/trait.ts @@ -6,7 +6,6 @@ import { computed, type ReadonlySignal } from '@preact/signals-core'; import type { GroupBy, GroupProperty } from '../common/types.js'; import type { TypeInstance } from '../logical/type.js'; -import type { DVJSON } from '../property/types.js'; import { createTraitKey } from '../traits/key.js'; import type { Property } from '../view-manager/property.js'; import type { SingleView } from '../view-manager/single-view.js'; @@ -19,7 +18,7 @@ export type GroupData = { key: string; name: string; type: TypeInstance; - value: DVJSON; + value: unknown; rows: string[]; }; @@ -235,7 +234,7 @@ export class GroupTrait { } const remove = this.config$.value?.removeFromGroup ?? (() => null); const group = fromGroupKey != null ? groupMap[fromGroupKey] : undefined; - let newValue: DVJSON = null; + let newValue: unknown = null; if (group) { newValue = remove( group.value, @@ -284,7 +283,7 @@ export class GroupTrait { this.view.cellValueSet(rowId, propertyId, newValue); } - updateValue(rows: string[], value: DVJSON) { + updateValue(rows: string[], value: unknown) { const propertyId = this.propertyId; if (!propertyId) { return; diff --git a/blocksuite/affine/data-view/src/core/group-by/types.ts b/blocksuite/affine/data-view/src/core/group-by/types.ts index 78f6302dc..3af11396f 100644 --- a/blocksuite/affine/data-view/src/core/group-by/types.ts +++ b/blocksuite/affine/data-view/src/core/group-by/types.ts @@ -1,34 +1,35 @@ import type { UniComponent } from '@blocksuite/affine-shared/types'; import type { TypeInstance } from '../logical/type.js'; -import type { DVJSON } from '../property/types.js'; - export interface GroupRenderProps< Data extends NonNullable = NonNullable, - Value = DVJSON, + JsonValue = unknown, > { data: Data; updateData?: (data: Data) => void; - value: Value; - updateValue?: (value: Value) => void; + value: JsonValue; + updateValue?: (value: JsonValue) => void; readonly: boolean; } -export type GroupByConfig = { +export type GroupByConfig< + JsonValue = unknown, + Data extends NonNullable = NonNullable, +> = { name: string; groupName: (type: TypeInstance, value: unknown) => string; defaultKeys: (type: TypeInstance) => { key: string; - value: DVJSON; + value: JsonValue; }[]; valuesGroup: ( value: unknown, type: TypeInstance ) => { key: string; - value: DVJSON; + value: JsonValue; }[]; - addToGroup?: (value: DVJSON, oldValue: DVJSON) => DVJSON; - removeFromGroup?: (value: DVJSON, oldValue: DVJSON) => DVJSON; - view: UniComponent; + addToGroup?: (value: JsonValue, oldValue: JsonValue) => JsonValue; + removeFromGroup?: (value: JsonValue, oldValue: JsonValue) => JsonValue; + view: UniComponent>; }; diff --git a/blocksuite/affine/data-view/src/core/logical/type-presets.ts b/blocksuite/affine/data-view/src/core/logical/type-presets.ts index 5a18d90a7..3e3188f21 100644 --- a/blocksuite/affine/data-view/src/core/logical/type-presets.ts +++ b/blocksuite/affine/data-view/src/core/logical/type-presets.ts @@ -11,7 +11,6 @@ export const SelectTagSchema = Zod.object({ id: Zod.string(), color: Zod.string(), value: Zod.string(), - parentId: Zod.string().optional(), }); export const unknown = defineDataType('Unknown', zod.never(), zod.unknown()); export const dt = { diff --git a/blocksuite/affine/data-view/src/core/property/base-cell.ts b/blocksuite/affine/data-view/src/core/property/base-cell.ts index 18a88a6a1..66c8c002f 100644 --- a/blocksuite/affine/data-view/src/core/property/base-cell.ts +++ b/blocksuite/affine/data-view/src/core/property/base-cell.ts @@ -8,18 +8,19 @@ import type { Cell } from '../view-manager/cell.js'; import type { CellRenderProps, DataViewCellLifeCycle } from './manager.js'; export abstract class BaseCellRenderer< - Value, + RawValue = unknown, + JsonValue = unknown, Data extends Record = Record, > extends SignalWatcher(WithDisposable(ShadowlessElement)) - implements DataViewCellLifeCycle, CellRenderProps + implements DataViewCellLifeCycle, CellRenderProps { get expose() { return this; } @property({ attribute: false }) - accessor cell!: Cell; + accessor cell!: Cell; readonly$ = computed(() => { return this.cell.property.readonly$.value; @@ -101,11 +102,11 @@ export abstract class BaseCellRenderer< this.requestUpdate(); } - valueSetImmediate(value: Value | undefined): void { + valueSetImmediate(value: RawValue | undefined): void { this.cell.valueSet(value); } - valueSetNextTick(value: Value | undefined) { + valueSetNextTick(value: RawValue | undefined) { requestAnimationFrame(() => { this.cell.valueSet(value); }); diff --git a/blocksuite/affine/data-view/src/core/property/convert.ts b/blocksuite/affine/data-view/src/core/property/convert.ts index cc3945cd6..9c24d6ce2 100644 --- a/blocksuite/affine/data-view/src/core/property/convert.ts +++ b/blocksuite/affine/data-view/src/core/property/convert.ts @@ -1,7 +1,7 @@ import type { PropertyModel } from './property-config.js'; import type { - GetCellDataFromConfig, GetPropertyDataFromConfig, + GetRawValueFromConfig, } from './types.js'; export type ConvertFunction< @@ -9,14 +9,14 @@ export type ConvertFunction< To extends PropertyModel = PropertyModel, > = ( property: GetPropertyDataFromConfig, - cells: (GetCellDataFromConfig | undefined)[] + cells: (GetRawValueFromConfig | undefined)[] ) => { property: GetPropertyDataFromConfig; - cells: (GetCellDataFromConfig | undefined)[]; + cells: (GetRawValueFromConfig | undefined)[]; }; export const createPropertyConvert = < - From extends PropertyModel, - To extends PropertyModel, + From extends PropertyModel, + To extends PropertyModel, >( from: From, to: To, diff --git a/blocksuite/affine/data-view/src/core/property/manager.ts b/blocksuite/affine/data-view/src/core/property/manager.ts index 657d002b1..e8518fd8a 100644 --- a/blocksuite/affine/data-view/src/core/property/manager.ts +++ b/blocksuite/affine/data-view/src/core/property/manager.ts @@ -5,9 +5,10 @@ import type { Cell } from '../view-manager/cell.js'; export interface CellRenderProps< Data extends NonNullable = NonNullable, - Value = unknown, + RawValue = unknown, + JsonValue = unknown, > { - cell: Cell; + cell: Cell; isEditing$: ReadonlySignal; selectCurrentCell: (editing: boolean) => void; } @@ -27,12 +28,17 @@ export interface DataViewCellLifeCycle { export type DataViewCellComponent< Data extends NonNullable = NonNullable, - Value = unknown, -> = UniComponent, DataViewCellLifeCycle>; + RawValue = unknown, + JsonValue = unknown, +> = UniComponent< + CellRenderProps, + DataViewCellLifeCycle +>; export type CellRenderer< Data extends NonNullable = NonNullable, - Value = unknown, + RawValue = unknown, + JsonValue = unknown, > = { - view: DataViewCellComponent; + view: DataViewCellComponent; }; diff --git a/blocksuite/affine/data-view/src/core/property/property-config.ts b/blocksuite/affine/data-view/src/core/property/property-config.ts index b6545a501..47477997b 100644 --- a/blocksuite/affine/data-view/src/core/property/property-config.ts +++ b/blocksuite/affine/data-view/src/core/property/property-config.ts @@ -4,20 +4,22 @@ import type { PropertyConfig } from './types.js'; export type PropertyMetaConfig< Type extends string = string, PropertyData extends NonNullable = NonNullable, - CellData = unknown, + RawValue = unknown, + JsonValue = unknown, > = { type: Type; - config: PropertyConfig; + config: PropertyConfig; create: Create; - renderer: Renderer; + renderer: Renderer; }; type CreatePropertyMeta< Type extends string = string, PropertyData extends Record = Record, - CellData = unknown, + RawValue = unknown, + JsonValue = unknown, > = ( - renderer: Omit, 'type'> -) => PropertyMetaConfig; + renderer: Omit, 'type'> +) => PropertyMetaConfig; type Create< PropertyData extends Record = Record, > = ( @@ -32,26 +34,33 @@ type Create< export type PropertyModel< Type extends string = string, PropertyData extends Record = Record, - CellData = unknown, + RawValue = unknown, + JsonValue = unknown, > = { type: Type; - config: PropertyConfig; + config: PropertyConfig; create: Create; - createPropertyMeta: CreatePropertyMeta; + createPropertyMeta: CreatePropertyMeta< + Type, + PropertyData, + RawValue, + JsonValue + >; }; export const propertyType = (type: Type) => ({ type: type, modelConfig: < - CellData, PropertyData extends Record = Record, + RawValue = unknown, + JsonValue = unknown, >( - ops: PropertyConfig - ): PropertyModel => { + ops: PropertyConfig + ): PropertyModel => { const create: Create = (name, data) => { return { type, name, - data: data ?? ops.defaultData(), + data: data ?? ops.propertyData.default(), }; }; return { diff --git a/blocksuite/affine/data-view/src/core/property/renderer.ts b/blocksuite/affine/data-view/src/core/property/renderer.ts index 951f453c4..2a2a9078c 100644 --- a/blocksuite/affine/data-view/src/core/property/renderer.ts +++ b/blocksuite/affine/data-view/src/core/property/renderer.ts @@ -6,18 +6,20 @@ import type { CellRenderer, DataViewCellComponent } from './manager.js'; export interface Renderer< Data extends NonNullable = NonNullable, - Value = unknown, + RawValue = unknown, + JsonValue = unknown, > { type: string; icon?: UniComponent; - cellRenderer: CellRenderer; + cellRenderer: CellRenderer; } export const createFromBaseCellRenderer = < - Value, + RawValue = unknown, + JsonValue = unknown, Data extends Record = Record, >( - renderer: new () => BaseCellRenderer + renderer: new () => BaseCellRenderer ): DataViewCellComponent => { return createUniComponentFromWebComponent(renderer as never) as never; }; diff --git a/blocksuite/affine/data-view/src/core/property/types.ts b/blocksuite/affine/data-view/src/core/property/types.ts index 8fc6080af..351704d4a 100644 --- a/blocksuite/affine/data-view/src/core/property/types.ts +++ b/blocksuite/affine/data-view/src/core/property/types.ts @@ -8,86 +8,84 @@ export type WithCommonPropertyConfig = T & { dataSource: DataSource; }; export type GetPropertyDataFromConfig = - T extends PropertyConfig ? R : never; -export type GetCellDataFromConfig = - T extends PropertyConfig ? R : never; -export type PropertyConfig< - Data extends NonNullable = NonNullable, - Value = unknown, -> = { + T extends PropertyConfig ? R : never; +export type GetRawValueFromConfig = + T extends PropertyConfig ? R : never; +export type GetJsonValueFromConfig = + T extends PropertyConfig ? R : never; +export type PropertyConfig = { name: string; - valueSchema: ZodType; hide?: boolean; + propertyData: { + schema: ZodType; + default: () => Data; + }; + rawValue: { + schema: ZodType; + default: () => RawValue; + toString: (config: { value: RawValue; data: Data }) => string; + fromString: ( + config: WithCommonPropertyConfig<{ + value: string; + data: Data; + }> + ) => { + value: unknown; + data?: Record; + }; + toJson: ( + config: WithCommonPropertyConfig<{ + value: RawValue; + data: Data; + }> + ) => JsonValue; + fromJson: ( + config: WithCommonPropertyConfig<{ + value: JsonValue; + data: Data; + }> + ) => RawValue | undefined; + setValue?: ( + config: WithCommonPropertyConfig<{ + data: Data; + value: RawValue; + newValue: RawValue; + setValue: (value: RawValue) => void; + }> + ) => void; + onUpdate?: ( + config: WithCommonPropertyConfig<{ + value: RawValue; + data: Data; + callback: () => void; + }> + ) => Disposable; + }; + jsonValue: { + schema: ZodType; + type: ( + config: WithCommonPropertyConfig<{ + data: Data; + }> + ) => TypeInstance; + isEmpty: ( + config: WithCommonPropertyConfig<{ + value: JsonValue; + }> + ) => boolean; + }; fixed?: { defaultData: Data; defaultOrder?: string; defaultShow?: boolean; }; - defaultData: () => Data; - type: ( - config: WithCommonPropertyConfig<{ - data: Data; - }> - ) => TypeInstance; - formatValue?: ( - config: WithCommonPropertyConfig<{ - value: Value; - data: Data; - }> - ) => Value; - isEmpty: ( - config: WithCommonPropertyConfig<{ - value?: Value; - }> - ) => boolean; minWidth?: number; - values?: ( - config: WithCommonPropertyConfig<{ - value?: Value; - }> - ) => unknown[]; - cellToString: (config: { value: Value; data: Data }) => string; - cellFromString: ( - config: WithCommonPropertyConfig<{ - value: string; - data: Data; - }> - ) => { - value: unknown; - data?: Record; - }; - cellToJson: ( - config: WithCommonPropertyConfig<{ - value?: Value; - data: Data; - }> - ) => DVJSON; - cellFromJson: ( - config: WithCommonPropertyConfig<{ - value: DVJSON; - data: Data; - }> - ) => Value | undefined; addGroup?: ( config: WithCommonPropertyConfig<{ text: string; oldData: Data; }> ) => Data; - onUpdate?: ( - config: WithCommonPropertyConfig<{ - value: Value; - data: Data; - callback: () => void; - }> - ) => Disposable; - valueUpdate?: ( - config: WithCommonPropertyConfig<{ - value: Value; - data: Data; - newValue: Value; - }> - ) => Value; }; export type DVJSON = diff --git a/blocksuite/affine/data-view/src/core/property/utils.ts b/blocksuite/affine/data-view/src/core/property/utils.ts new file mode 100644 index 000000000..6544936ed --- /dev/null +++ b/blocksuite/affine/data-view/src/core/property/utils.ts @@ -0,0 +1,30 @@ +import type { DataSource } from '../data-source/base'; +import type { PropertyConfig } from './types'; + +export const fromJson = ( + config: PropertyConfig, + { + value, + data, + dataSource, + }: { + value: unknown; + data: Data; + dataSource: DataSource; + } +): RawValue | undefined => { + const fromJson = config.rawValue.fromJson; + const jsonSchema = config.jsonValue.schema; + if (!fromJson || !jsonSchema) { + return; + } + const jsonResult = jsonSchema.safeParse(value); + if (!jsonResult.success) { + return; + } + return fromJson({ + value: jsonResult.data, + data, + dataSource, + }); +}; diff --git a/blocksuite/affine/data-view/src/core/statistics/any.ts b/blocksuite/affine/data-view/src/core/statistics/any.ts index 3f946e95a..831571caa 100644 --- a/blocksuite/affine/data-view/src/core/statistics/any.ts +++ b/blocksuite/affine/data-view/src/core/statistics/any.ts @@ -19,16 +19,14 @@ export const anyTypeStatsFunctions: StatisticsConfig[] = [ displayName: 'Values', type: 'count-values', dataType: t.unknown.instance(), - impl: (data, { meta, dataSource }) => { - const values = data - .flatMap(v => { - if (meta.config.values) { - return meta.config.values({ value: v, dataSource }); - } - return v; - }) - .filter(v => v != null); - return values.length.toString(); + impl: data => { + const values = data.reduce((acc: number, v) => { + if (Array.isArray(v)) { + return acc + v.length; + } + return acc + (v == null ? 0 : 1); + }, 0); + return values.toString(); }, }), createStatisticConfig({ @@ -37,13 +35,13 @@ export const anyTypeStatsFunctions: StatisticsConfig[] = [ displayName: 'Unique Values', type: 'count-unique-values', dataType: t.unknown.instance(), - impl: (data, { meta, dataSource }) => { + impl: data => { const values = data .flatMap(v => { - if (meta.config.values) { - return meta.config.values({ value: v, dataSource }); + if (Array.isArray(v)) { + return v; } - return v; + return [v]; }) .filter(v => v != null); return new Set(values).size.toString(); @@ -57,7 +55,7 @@ export const anyTypeStatsFunctions: StatisticsConfig[] = [ dataType: t.unknown.instance(), impl: (data, { meta, dataSource }) => { const emptyList = data.filter(value => - meta.config.isEmpty({ value, dataSource }) + meta.config.jsonValue.isEmpty({ value, dataSource }) ); return emptyList.length.toString(); }, @@ -70,7 +68,7 @@ export const anyTypeStatsFunctions: StatisticsConfig[] = [ dataType: t.unknown.instance(), impl: (data, { meta, dataSource }) => { const notEmptyList = data.filter( - value => !meta.config.isEmpty({ value, dataSource }) + value => !meta.config.jsonValue.isEmpty({ value, dataSource }) ); return notEmptyList.length.toString(); }, @@ -84,7 +82,7 @@ export const anyTypeStatsFunctions: StatisticsConfig[] = [ impl: (data, { meta, dataSource }) => { if (data.length === 0) return ''; const emptyList = data.filter(value => - meta.config.isEmpty({ value, dataSource }) + meta.config.jsonValue.isEmpty({ value, dataSource }) ); return ((emptyList.length / data.length) * 100).toFixed(2) + '%'; }, @@ -98,7 +96,7 @@ export const anyTypeStatsFunctions: StatisticsConfig[] = [ impl: (data, { meta, dataSource }) => { if (data.length === 0) return ''; const notEmptyList = data.filter( - value => !meta.config.isEmpty({ value, dataSource }) + value => !meta.config.jsonValue.isEmpty({ value, dataSource }) ); return ((notEmptyList.length / data.length) * 100).toFixed(2) + '%'; }, diff --git a/blocksuite/affine/data-view/src/core/view-manager/cell.ts b/blocksuite/affine/data-view/src/core/view-manager/cell.ts index 81400c492..beb24bcb3 100644 --- a/blocksuite/affine/data-view/src/core/view-manager/cell.ts +++ b/blocksuite/affine/data-view/src/core/view-manager/cell.ts @@ -5,26 +5,29 @@ import type { Row } from './row.js'; import type { SingleView } from './single-view.js'; export interface Cell< - Value = unknown, + RawValue = unknown, + JsonValue = unknown, Data extends Record = Record, > { readonly rowId: string; readonly view: SingleView; readonly row: Row; readonly propertyId: string; - readonly property: Property; + readonly property: Property; readonly isEmpty$: ReadonlySignal; readonly stringValue$: ReadonlySignal; - readonly jsonValue$: ReadonlySignal; + readonly jsonValue$: ReadonlySignal; - readonly value$: ReadonlySignal; - valueSet(value: Value | undefined): void; + readonly value$: ReadonlySignal; + + valueSet(value: RawValue | undefined): void; } export class CellBase< - Value = unknown, + RawValue = unknown, + JsonValue = unknown, Data extends Record = Record, -> implements Cell +> implements Cell { meta$ = computed(() => { return this.view.manager.dataSource.propertyMetaGet( @@ -36,29 +39,35 @@ export class CellBase< return this.view.manager.dataSource.cellValueGet( this.rowId, this.propertyId - ) as Value; + ) as RawValue; }); isEmpty$: ReadonlySignal = computed(() => { - return this.meta$.value.config.isEmpty({ - value: this.value$.value, - dataSource: this.view.manager.dataSource, - }); + return ( + this.meta$.value?.config.jsonValue.isEmpty({ + value: this.jsonValue$.value, + dataSource: this.view.manager.dataSource, + }) ?? true + ); }); - jsonValue$: ReadonlySignal = computed(() => { - return this.view.cellJsonValueGet(this.rowId, this.propertyId); + jsonValue$: ReadonlySignal = computed(() => { + return this.view.cellJsonValueGet(this.rowId, this.propertyId) as JsonValue; }); property$ = computed(() => { - return this.view.propertyGet(this.propertyId) as Property; + return this.view.propertyGet(this.propertyId) as Property< + RawValue, + JsonValue, + Data + >; }); stringValue$: ReadonlySignal = computed(() => { return this.view.cellStringValueGet(this.rowId, this.propertyId)!; }); - get property(): Property { + get property(): Property { return this.property$.value; } @@ -72,7 +81,7 @@ export class CellBase< public rowId: string ) {} - valueSet(value: unknown | undefined): void { + valueSet(value: RawValue | undefined): void { this.view.manager.dataSource.cellValueChange( this.rowId, this.propertyId, diff --git a/blocksuite/affine/data-view/src/core/view-manager/property.ts b/blocksuite/affine/data-view/src/core/view-manager/property.ts index 50418d33d..2523ba79d 100644 --- a/blocksuite/affine/data-view/src/core/view-manager/property.ts +++ b/blocksuite/affine/data-view/src/core/view-manager/property.ts @@ -8,7 +8,8 @@ import type { Cell } from './cell.js'; import type { SingleView } from './single-view.js'; export interface Property< - Value = unknown, + RawValue = unknown, + JsonValue = unknown, Data extends Record = Record, > { readonly id: string; @@ -28,7 +29,7 @@ export interface Property< readonly duplicate?: () => void; get canDuplicate(): boolean; - cellGet(rowId: string): Cell; + cellGet(rowId: string): Cell; readonly data$: ReadonlySignal; dataUpdate(updater: PropertyDataUpdater): void; @@ -44,17 +45,18 @@ export interface Property< hideSet(hide: boolean): void; get hideCanSet(): boolean; - valueGet(rowId: string): Value | undefined; - valueSet(rowId: string, value: Value | undefined): void; + valueGet(rowId: string): RawValue | undefined; + valueSet(rowId: string, value: RawValue | undefined): void; stringValueGet(rowId: string): string; valueSetFromString(rowId: string, value: string): void; } export abstract class PropertyBase< - Value = unknown, + RawValue = unknown, + JsonValue = unknown, Data extends Record = Record, -> implements Property +> implements Property { cells$ = computed(() => { return this.view.rows$.value.map(id => this.cellGet(id)); @@ -141,8 +143,8 @@ export abstract class PropertyBase< return this.view.propertyCanHide(this.id); } - cellGet(rowId: string): Cell { - return this.view.cellGet(rowId, this.id) as Cell; + cellGet(rowId: string): Cell { + return this.view.cellGet(rowId, this.id) as Cell; } dataUpdate(updater: PropertyDataUpdater): void { @@ -165,11 +167,11 @@ export abstract class PropertyBase< return this.cellGet(rowId).stringValue$.value; } - valueGet(rowId: string): Value | undefined { + valueGet(rowId: string): RawValue | undefined { return this.cellGet(rowId).value$.value; } - valueSet(rowId: string, value: Value | undefined): void { + valueSet(rowId: string, value: RawValue | undefined): void { return this.cellGet(rowId).valueSet(value); } @@ -181,6 +183,6 @@ export abstract class PropertyBase< if (data.data) { this.dataUpdate(() => data.data as Data); } - this.valueSet(rowId, data.value as Value); + this.valueSet(rowId, data.value as RawValue); } } diff --git a/blocksuite/affine/data-view/src/core/view-manager/single-view.ts b/blocksuite/affine/data-view/src/core/view-manager/single-view.ts index 6569dcba5..5efc10a1a 100644 --- a/blocksuite/affine/data-view/src/core/view-manager/single-view.ts +++ b/blocksuite/affine/data-view/src/core/view-manager/single-view.ts @@ -4,9 +4,9 @@ import { computed, type ReadonlySignal, signal } from '@preact/signals-core'; import type { DataViewContextKey } from '../data-source/context.js'; import type { Variable } from '../expression/types.js'; -import type { DVJSON } from '../index.js'; import type { TypeInstance } from '../logical/type.js'; import type { PropertyMetaConfig } from '../property/property-config.js'; +import { fromJson } from '../property/utils'; import type { TraitKey } from '../traits/key.js'; import type { DatabaseFlags } from '../types.js'; import type { DataViewDataType, ViewMeta } from '../view/data-view.js'; @@ -50,9 +50,9 @@ export interface SingleView { cellValueSet(rowId: string, propertyId: string, value: unknown): void; - cellJsonValueGet(rowId: string, propertyId: string): DVJSON; + cellJsonValueGet(rowId: string, propertyId: string): unknown | null; - cellJsonValueSet(rowId: string, propertyId: string, value: DVJSON): void; + cellJsonValueSet(rowId: string, propertyId: string, value: unknown): void; cellStringValueGet(rowId: string, propertyId: string): string | undefined; @@ -82,12 +82,17 @@ export interface SingleView { readonly propertyMetas$: ReadonlySignal; - propertyAdd(toAfterOfProperty: InsertToPosition, type?: string): string; + propertyAdd( + toAfterOfProperty: InsertToPosition, + type?: string + ): string | undefined; propertyDelete(propertyId: string): void; + propertyCanDelete(propertyId: string): boolean; propertyDuplicate(propertyId: string): void; + propertyCanDuplicate(propertyId: string): boolean; propertyGet(propertyId: string): Property; @@ -105,11 +110,13 @@ export interface SingleView { propertyTypeGet(propertyId: string): string | undefined; propertyTypeSet(propertyId: string, type: string): void; + propertyTypeCanSet(propertyId: string): boolean; propertyHideGet(propertyId: string): boolean; propertyHideSet(propertyId: string, hide: boolean): void; + propertyCanHide(propertyId: string): boolean; propertyDataGet(propertyId: string): Record; @@ -187,13 +194,16 @@ export abstract class SingleViewBase< }); vars$ = computed(() => { - return this.propertiesWithoutFilter$.value.map(id => { + return this.propertiesWithoutFilter$.value.flatMap(id => { const v = this.propertyGet(id); const propertyMeta = this.dataSource.propertyMetaGet(v.type$.value); + if (!propertyMeta) { + return []; + } return { id: v.id, name: v.name$.value, - type: propertyMeta.config.type({ + type: propertyMeta.config.jsonValue.type({ data: v.data$.value, dataSource: this.dataSource, }), @@ -229,15 +239,19 @@ export abstract class SingleViewBase< public manager: ViewManager, public id: string ) {} + propertyCanDelete(propertyId: string): boolean { return this.dataSource.propertyCanDelete(propertyId); } + propertyCanDuplicate(propertyId: string): boolean { return this.dataSource.propertyCanDuplicate(propertyId); } + propertyTypeCanSet(propertyId: string): boolean { return this.dataSource.propertyTypeCanSet(propertyId); } + propertyCanHide(propertyId: string): boolean { return this.propertyTypeGet(propertyId) !== 'title'; } @@ -264,33 +278,35 @@ export abstract class SingleViewBase< return new CellBase(this, propertyId, rowId); } - cellJsonValueGet(rowId: string, propertyId: string): DVJSON { + cellJsonValueGet(rowId: string, propertyId: string): unknown | null { const type = this.propertyTypeGet(propertyId); if (!type) { return null; } - return this.dataSource.propertyMetaGet(type).config.cellToJson({ - value: this.dataSource.cellValueGet(rowId, propertyId), - data: this.propertyDataGet(propertyId), - dataSource: this.dataSource, - }); + return ( + this.dataSource.propertyMetaGet(type)?.config.rawValue.toJson({ + value: this.dataSource.cellValueGet(rowId, propertyId), + data: this.propertyDataGet(propertyId), + dataSource: this.dataSource, + }) ?? null + ); } - cellJsonValueSet(rowId: string, propertyId: string, value: DVJSON): void { + cellJsonValueSet(rowId: string, propertyId: string, value: unknown): void { const type = this.propertyTypeGet(propertyId); if (!type) { return; } - const fromJson = this.dataSource.propertyMetaGet(type).config.cellFromJson; - this.dataSource.cellValueChange( - rowId, - propertyId, - fromJson({ - value, - data: this.propertyDataGet(propertyId), - dataSource: this.dataSource, - }) - ); + const config = this.dataSource.propertyMetaGet(type)?.config; + if (!config) { + return; + } + const rawValue = fromJson(config, { + value: value, + data: this.propertyDataGet(propertyId), + dataSource: this.dataSource, + }); + this.dataSource.cellValueChange(rowId, propertyId, rawValue); } cellStringValueGet(rowId: string, propertyId: string): string | undefined { @@ -299,7 +315,7 @@ export abstract class SingleViewBase< return; } return ( - this.dataSource.propertyMetaGet(type).config.cellToString({ + this.dataSource.propertyMetaGet(type)?.config.rawValue.toString({ value: this.dataSource.cellValueGet(rowId, propertyId), data: this.propertyDataGet(propertyId), }) ?? '' @@ -311,14 +327,7 @@ export abstract class SingleViewBase< if (!type) { return; } - const cellValue = this.dataSource.cellValueGet(rowId, propertyId); - return ( - this.dataSource.propertyMetaGet(type).config.formatValue?.({ - value: cellValue, - data: this.propertyDataGet(propertyId), - dataSource: this.dataSource, - }) ?? cellValue - ); + return this.dataSource.cellValueGet(rowId, propertyId); } cellValueSet(rowId: string, propertyId: string, value: unknown): void { @@ -355,8 +364,11 @@ export abstract class SingleViewBase< }); } - propertyAdd(position: InsertToPosition, type?: string): string { + propertyAdd(position: InsertToPosition, type?: string): string | undefined { const id = this.dataSource.propertyAdd(position, type); + if (!id) { + return; + } this.propertyMove(id, position); return id; } @@ -374,7 +386,11 @@ export abstract class SingleViewBase< if (!type) { return; } - return this.dataSource.propertyMetaGet(type).config.type({ + const meta = this.dataSource.propertyMetaGet(type); + if (!meta) { + return; + } + return meta.config.jsonValue.type({ data: this.propertyDataGet(propertyId), dataSource: this.dataSource, }); @@ -402,7 +418,7 @@ export abstract class SingleViewBase< abstract propertyHideSet(propertyId: string, hide: boolean): void; propertyIconGet(type: string): UniComponent | undefined { - return this.dataSource.propertyMetaGet(type).renderer.icon; + return this.dataSource.propertyMetaGet(type)?.renderer.icon; } propertyIdGetByIndex(index: number): string | undefined { @@ -413,7 +429,7 @@ export abstract class SingleViewBase< return this.propertyIds$.value.indexOf(propertyId); } - propertyMetaGet(type: string): PropertyMetaConfig { + propertyMetaGet(type: string): PropertyMetaConfig | undefined { return this.dataSource.propertyMetaGet(type); } @@ -439,13 +455,16 @@ export abstract class SingleViewBase< if (!type) { return; } - return ( - this.dataSource.propertyMetaGet(type).config.cellFromString({ - value: cellData, - data: this.propertyDataGet(propertyId), - dataSource: this.dataSource, - }) ?? '' - ); + const fromString = + this.dataSource.propertyMetaGet(type)?.config.rawValue.fromString; + if (!fromString) { + return; + } + return fromString({ + value: cellData, + data: this.propertyDataGet(propertyId), + dataSource: this.dataSource, + }); } propertyPreGet(propertyId: string): Property | undefined { diff --git a/blocksuite/affine/data-view/src/effects.ts b/blocksuite/affine/data-view/src/effects.ts index 3c8b527cd..9cdefcad6 100644 --- a/blocksuite/affine/data-view/src/effects.ts +++ b/blocksuite/affine/data-view/src/effects.ts @@ -15,7 +15,7 @@ import { AffineLitIcon, UniAnyRender, UniLit } from './core/index.js'; import { AnyRender } from './core/utils/uni-component/render-template.js'; import { CheckboxCell } from './property-presets/checkbox/cell-renderer.js'; import { DateCell } from './property-presets/date/cell-renderer.js'; -import { TextCell as ImageTextCell } from './property-presets/image/cell-renderer.js'; +import { ImageCell } from './property-presets/image/cell-renderer.js'; import { MultiSelectCell } from './property-presets/multi-select/cell-renderer.js'; import { NumberCell } from './property-presets/number/cell-renderer.js'; import { ProgressCell } from './property-presets/progress/cell-renderer.js'; @@ -74,7 +74,7 @@ export function effects() { customElements.define('mobile-table-cell', MobileTableCell); customElements.define('affine-data-view-renderer', DataViewRenderer); customElements.define('any-render', AnyRender); - customElements.define('affine-database-image-cell', ImageTextCell); + customElements.define('affine-database-image-cell', ImageCell); customElements.define('affine-database-date-cell', DateCell); customElements.define( 'data-view-properties-setting', diff --git a/blocksuite/affine/data-view/src/property-presets/checkbox/define.ts b/blocksuite/affine/data-view/src/property-presets/checkbox/define.ts index 24669bce9..86bc6044b 100644 --- a/blocksuite/affine/data-view/src/property-presets/checkbox/define.ts +++ b/blocksuite/affine/data-view/src/property-presets/checkbox/define.ts @@ -21,15 +21,24 @@ const FALSE_VALUES = new Set([ export const checkboxPropertyModelConfig = checkboxPropertyType.modelConfig({ name: 'Checkbox', - valueSchema: zod.boolean().optional(), - type: () => t.boolean.instance(), - defaultData: () => ({}), - cellToString: ({ value }) => (value ? 'True' : 'False'), - cellFromString: ({ value }) => ({ - value: !FALSE_VALUES.has((value?.trim() ?? '').toLowerCase()), - }), - cellToJson: ({ value }) => value ?? null, - cellFromJson: ({ value }) => (typeof value !== 'boolean' ? undefined : value), - isEmpty: () => false, + propertyData: { + schema: zod.object({}), + default: () => ({}), + }, + jsonValue: { + schema: zod.boolean(), + isEmpty: () => false, + type: () => t.boolean.instance(), + }, + rawValue: { + schema: zod.boolean(), + default: () => false, + fromString: ({ value }) => ({ + value: !FALSE_VALUES.has((value?.trim() ?? '').toLowerCase()), + }), + toString: ({ value }) => (value ? 'True' : 'False'), + toJson: ({ value }) => value, + fromJson: ({ value }) => value, + }, minWidth: 34, }); diff --git a/blocksuite/affine/data-view/src/property-presets/date/cell-renderer.ts b/blocksuite/affine/data-view/src/property-presets/date/cell-renderer.ts index 800bece17..53ddb668c 100644 --- a/blocksuite/affine/data-view/src/property-presets/date/cell-renderer.ts +++ b/blocksuite/affine/data-view/src/property-presets/date/cell-renderer.ts @@ -20,7 +20,7 @@ import { } from './cell-renderer.css.js'; import { datePropertyModelConfig } from './define.js'; -export class DateCell extends BaseCellRenderer { +export class DateCell extends BaseCellRenderer { private _prevPortalAbortController: AbortController | null = null; private readonly openDatePicker = () => { diff --git a/blocksuite/affine/data-view/src/property-presets/date/define.ts b/blocksuite/affine/data-view/src/property-presets/date/define.ts index 84caccdf1..1b8e4f06f 100644 --- a/blocksuite/affine/data-view/src/property-presets/date/define.ts +++ b/blocksuite/affine/data-view/src/property-presets/date/define.ts @@ -7,19 +7,24 @@ import { propertyType } from '../../core/property/property-config.js'; export const datePropertyType = propertyType('date'); export const datePropertyModelConfig = datePropertyType.modelConfig({ name: 'Date', - type: () => t.date.instance(), - valueSchema: zod.number().optional(), - defaultData: () => ({}), - cellToString: ({ value }) => - value != null ? format(value, 'yyyy-MM-dd') : '', - cellFromString: ({ value }) => { - const date = parse(value, 'yyyy-MM-dd', new Date()); - - return { - value: +date, - }; + propertyData: { + schema: zod.object({}), + default: () => ({}), + }, + jsonValue: { + schema: zod.number().nullable(), + isEmpty: () => false, + type: () => t.date.instance(), + }, + rawValue: { + schema: zod.number().nullable(), + default: () => null, + toString: ({ value }) => (value != null ? format(value, 'yyyy-MM-dd') : ''), + fromString: ({ value }) => { + const date = parse(value, 'yyyy-MM-dd', new Date()); + return { value: +date }; + }, + toJson: ({ value }) => value, + fromJson: ({ value }) => value, }, - cellToJson: ({ value }) => value ?? null, - cellFromJson: ({ value }) => (typeof value !== 'number' ? undefined : value), - isEmpty: ({ value }) => value == null, }); diff --git a/blocksuite/affine/data-view/src/property-presets/image/cell-renderer.ts b/blocksuite/affine/data-view/src/property-presets/image/cell-renderer.ts index 81f458914..6d9311f47 100644 --- a/blocksuite/affine/data-view/src/property-presets/image/cell-renderer.ts +++ b/blocksuite/affine/data-view/src/property-presets/image/cell-renderer.ts @@ -5,7 +5,7 @@ import { createFromBaseCellRenderer } from '../../core/property/renderer.js'; import { createIcon } from '../../core/utils/uni-icon.js'; import { imagePropertyModelConfig } from './define.js'; -export class TextCell extends BaseCellRenderer { +export class ImageCell extends BaseCellRenderer { static override styles = css` affine-database-image-cell { width: 100%; @@ -27,6 +27,6 @@ export class TextCell extends BaseCellRenderer { export const imagePropertyConfig = imagePropertyModelConfig.createPropertyMeta({ icon: createIcon('ImageIcon'), cellRenderer: { - view: createFromBaseCellRenderer(TextCell), + view: createFromBaseCellRenderer(ImageCell), }, }); diff --git a/blocksuite/affine/data-view/src/property-presets/image/define.ts b/blocksuite/affine/data-view/src/property-presets/image/define.ts index 1cca5f953..533a8ae48 100644 --- a/blocksuite/affine/data-view/src/property-presets/image/define.ts +++ b/blocksuite/affine/data-view/src/property-presets/image/define.ts @@ -2,21 +2,31 @@ import zod from 'zod'; import { t } from '../../core/logical/type-presets.js'; import { propertyType } from '../../core/property/property-config.js'; + export const imagePropertyType = propertyType('image'); export const imagePropertyModelConfig = imagePropertyType.modelConfig({ name: 'image', - valueSchema: zod.string().optional(), - hide: true, - type: () => t.image.instance(), - defaultData: () => ({}), - cellToString: ({ value }) => value ?? '', - cellFromString: ({ value }) => { - return { - value: value, - }; + propertyData: { + schema: zod.object({}), + default: () => ({}), }, - cellToJson: ({ value }) => value ?? null, - cellFromJson: ({ value }) => (typeof value !== 'string' ? undefined : value), - isEmpty: ({ value }) => value == null, + jsonValue: { + schema: zod.string().nullable(), + isEmpty: ({ value }) => value == null, + type: () => t.image.instance(), + }, + rawValue: { + schema: zod.string().nullable(), + default: () => null, + toString: ({ value }) => value ?? '', + fromString: ({ value }) => { + return { + value: value, + }; + }, + toJson: ({ value }) => value, + fromJson: ({ value }) => value, + }, + hide: true, }); diff --git a/blocksuite/affine/data-view/src/property-presets/multi-select/cell-renderer.ts b/blocksuite/affine/data-view/src/property-presets/multi-select/cell-renderer.ts index f110a3e1f..2b930eeec 100644 --- a/blocksuite/affine/data-view/src/property-presets/multi-select/cell-renderer.ts +++ b/blocksuite/affine/data-view/src/property-presets/multi-select/cell-renderer.ts @@ -13,6 +13,7 @@ import { multiSelectStyle } from './cell-renderer.css.js'; import { multiSelectPropertyModelConfig } from './define.js'; export class MultiSelectCell extends BaseCellRenderer< + string[], string[], SelectPropertyData > { diff --git a/blocksuite/affine/data-view/src/property-presets/multi-select/define.ts b/blocksuite/affine/data-view/src/property-presets/multi-select/define.ts index 0053fbb56..ba6788df7 100644 --- a/blocksuite/affine/data-view/src/property-presets/multi-select/define.ts +++ b/blocksuite/affine/data-view/src/property-presets/multi-select/define.ts @@ -4,40 +4,29 @@ import zod from 'zod'; import { getTagColor } from '../../core/component/tags/colors.js'; import { type SelectTag, t } from '../../core/index.js'; import { propertyType } from '../../core/property/property-config.js'; -import type { SelectPropertyData } from '../select/define.js'; +import { SelectPropertySchema } from '../select/define.js'; export const multiSelectPropertyType = propertyType('multi-select'); + export const multiSelectPropertyModelConfig = - multiSelectPropertyType.modelConfig( - { - name: 'Multi-select', - valueSchema: zod.array(zod.string()).optional(), - type: ({ data }) => t.array.instance(t.tag.instance(data.options)), - defaultData: () => ({ + multiSelectPropertyType.modelConfig({ + name: 'Multi-select', + propertyData: { + schema: SelectPropertySchema, + default: () => ({ options: [], }), - addGroup: ({ text, oldData }) => { - return { - options: [ - ...(oldData.options ?? []), - { - id: nanoid(), - value: text, - color: getTagColor(), - }, - ], - }; - }, - formatValue: ({ value }) => { - if (Array.isArray(value)) { - return value.filter(v => v != null); - } - return []; - }, - cellToString: ({ value, data }) => - value - ?.map(id => data.options.find(v => v.id === id)?.value) - .join(',') ?? '', - cellFromString: ({ value: oldValue, data }) => { + }, + jsonValue: { + schema: zod.array(zod.string()), + isEmpty: ({ value }) => value.length === 0, + type: ({ data }) => t.array.instance(t.tag.instance(data.options)), + }, + rawValue: { + schema: zod.array(zod.string()), + default: () => [], + toString: ({ value, data }) => + value.map(id => data.options.find(v => v.id === id)?.value).join(','), + fromString: ({ value: oldValue, data }) => { const optionMap = Object.fromEntries( data.options.map(v => [v.value, v]) ); @@ -66,11 +55,22 @@ export const multiSelectPropertyModelConfig = data: data, }; }, - cellToJson: ({ value }) => value ?? null, - cellFromJson: ({ value }) => + toJson: ({ value }) => value ?? null, + fromJson: ({ value }) => Array.isArray(value) && value.every(v => typeof v === 'string') ? value : undefined, - isEmpty: ({ value }) => value == null || value.length === 0, - } - ); + }, + addGroup: ({ text, oldData }) => { + return { + options: [ + ...(oldData.options ?? []), + { + id: nanoid(), + value: text, + color: getTagColor(), + }, + ], + }; + }, + }); diff --git a/blocksuite/affine/data-view/src/property-presets/number/cell-renderer.ts b/blocksuite/affine/data-view/src/property-presets/number/cell-renderer.ts index f0163fd81..4c844313c 100644 --- a/blocksuite/affine/data-view/src/property-presets/number/cell-renderer.ts +++ b/blocksuite/affine/data-view/src/property-presets/number/cell-renderer.ts @@ -16,6 +16,7 @@ import { } from './utils/formatter.js'; export class NumberCell extends BaseCellRenderer< + number, number, NumberPropertyDataType > { diff --git a/blocksuite/affine/data-view/src/property-presets/number/define.ts b/blocksuite/affine/data-view/src/property-presets/number/define.ts index c88099e2f..b1b1de3bf 100644 --- a/blocksuite/affine/data-view/src/property-presets/number/define.ts +++ b/blocksuite/affine/data-view/src/property-presets/number/define.ts @@ -2,25 +2,29 @@ import zod from 'zod'; import { t } from '../../core/logical/type-presets.js'; import { propertyType } from '../../core/property/property-config.js'; -import type { NumberPropertyDataType } from './types.js'; +import { NumberPropertySchema } from './types.js'; export const numberPropertyType = propertyType('number'); -export const numberPropertyModelConfig = numberPropertyType.modelConfig< - number | undefined, - NumberPropertyDataType ->({ +export const numberPropertyModelConfig = numberPropertyType.modelConfig({ name: 'Number', - valueSchema: zod.number().optional(), - type: () => t.number.instance(), - defaultData: () => ({ decimal: 0, format: 'number' }), - cellToString: ({ value }) => value?.toString() ?? '', - cellFromString: ({ value }) => { - const num = value ? Number(value) : NaN; - return { - value: isNaN(num) ? null : num, - }; + propertyData: { + schema: NumberPropertySchema, + default: () => ({ decimal: 0, format: 'number' }) as const, + }, + jsonValue: { + schema: zod.number().nullable(), + isEmpty: ({ value }) => value == null, + type: () => t.number.instance(), + }, + rawValue: { + schema: zod.number().nullable(), + default: () => null, + toString: ({ value }) => value?.toString() ?? '', + fromString: ({ value }) => { + const num = value ? Number(value) : NaN; + return { value: isNaN(num) ? null : num }; + }, + toJson: ({ value }) => value ?? null, + fromJson: ({ value }) => (typeof value !== 'number' ? null : value), }, - cellToJson: ({ value }) => value ?? null, - cellFromJson: ({ value }) => (typeof value !== 'number' ? undefined : value), - isEmpty: ({ value }) => value == null, }); diff --git a/blocksuite/affine/data-view/src/property-presets/number/types.ts b/blocksuite/affine/data-view/src/property-presets/number/types.ts index f0f0f790a..7bdc62d5a 100644 --- a/blocksuite/affine/data-view/src/property-presets/number/types.ts +++ b/blocksuite/affine/data-view/src/property-presets/number/types.ts @@ -1,6 +1,9 @@ -import type { NumberFormat } from './utils/formatter.js'; +import zod from 'zod'; -export type NumberPropertyDataType = { - decimal?: number; - format?: NumberFormat; -}; +import { NumberFormatSchema } from './utils/formatter.js'; + +export const NumberPropertySchema = zod.object({ + decimal: zod.number().optional(), + format: NumberFormatSchema, +}); +export type NumberPropertyDataType = zod.infer; diff --git a/blocksuite/affine/data-view/src/property-presets/number/utils/formatter.ts b/blocksuite/affine/data-view/src/property-presets/number/utils/formatter.ts index 4b418d5ee..de4b83490 100644 --- a/blocksuite/affine/data-view/src/property-presets/number/utils/formatter.ts +++ b/blocksuite/affine/data-view/src/property-presets/number/utils/formatter.ts @@ -1,13 +1,16 @@ -export type NumberFormat = - | 'number' - | 'numberWithCommas' - | 'percent' - | 'currencyYen' - | 'currencyINR' - | 'currencyCNY' - | 'currencyUSD' - | 'currencyEUR' - | 'currencyGBP'; +import zod from 'zod'; +export const NumberFormatSchema = zod.enum([ + 'number', + 'numberWithCommas', + 'percent', + 'currencyYen', + 'currencyINR', + 'currencyCNY', + 'currencyUSD', + 'currencyEUR', + 'currencyGBP', +]); +export type NumberFormat = zod.infer; const currency = (currency: string): Intl.NumberFormatOptions => ({ style: 'currency', diff --git a/blocksuite/affine/data-view/src/property-presets/progress/cell-renderer.ts b/blocksuite/affine/data-view/src/property-presets/progress/cell-renderer.ts index 2987ed797..c514c42d6 100644 --- a/blocksuite/affine/data-view/src/property-presets/progress/cell-renderer.ts +++ b/blocksuite/affine/data-view/src/property-presets/progress/cell-renderer.ts @@ -23,7 +23,7 @@ const progressColors = { success: 'var(--affine-success-color)', }; -export class ProgressCell extends BaseCellRenderer { +export class ProgressCell extends BaseCellRenderer { startDrag = (event: MouseEvent) => { if (!this.isEditing$.value) return; diff --git a/blocksuite/affine/data-view/src/property-presets/progress/define.ts b/blocksuite/affine/data-view/src/property-presets/progress/define.ts index e841ce396..ce50f052a 100644 --- a/blocksuite/affine/data-view/src/property-presets/progress/define.ts +++ b/blocksuite/affine/data-view/src/property-presets/progress/define.ts @@ -6,20 +6,24 @@ export const progressPropertyType = propertyType('progress'); export const progressPropertyModelConfig = progressPropertyType.modelConfig({ name: 'Progress', - valueSchema: zod.number().optional(), - type: () => t.number.instance(), - defaultData: () => ({}), - cellToString: ({ value }) => value?.toString() ?? '', - cellFromString: ({ value }) => { - const num = value ? Number(value) : NaN; - return { - value: isNaN(num) ? null : num, - }; + propertyData: { + schema: zod.object({}), + default: () => ({}), }, - cellToJson: ({ value }) => value ?? null, - cellFromJson: ({ value }) => { - if (typeof value !== 'number') return undefined; - return value; + jsonValue: { + schema: zod.number(), + isEmpty: () => false, + type: () => t.number.instance(), + }, + rawValue: { + schema: zod.number(), + default: () => 0, + toString: ({ value }) => value.toString(), + fromString: ({ value }) => { + const num = value ? Number(value) : NaN; + return { value: isNaN(num) ? 0 : num }; + }, + toJson: ({ value }) => value, + fromJson: ({ value }) => value, }, - isEmpty: () => false, }); diff --git a/blocksuite/affine/data-view/src/property-presets/select/cell-renderer.ts b/blocksuite/affine/data-view/src/property-presets/select/cell-renderer.ts index 39e4d5c37..4ce1923d7 100644 --- a/blocksuite/affine/data-view/src/property-presets/select/cell-renderer.ts +++ b/blocksuite/affine/data-view/src/property-presets/select/cell-renderer.ts @@ -13,7 +13,11 @@ import { selectPropertyModelConfig, } from './define.js'; -export class SelectCell extends BaseCellRenderer { +export class SelectCell extends BaseCellRenderer< + string, + string, + SelectPropertyData +> { closePopup?: () => void; private readonly popTagSelect = () => { this.closePopup = popTagSelect(popupTargetFromElement(this), { diff --git a/blocksuite/affine/data-view/src/property-presets/select/define.ts b/blocksuite/affine/data-view/src/property-presets/select/define.ts index e1fa345d8..d4036b0a1 100644 --- a/blocksuite/affine/data-view/src/property-presets/select/define.ts +++ b/blocksuite/affine/data-view/src/property-presets/select/define.ts @@ -2,23 +2,66 @@ import { nanoid } from '@blocksuite/store'; import zod from 'zod'; import { getTagColor } from '../../core/component/tags/colors.js'; -import { type SelectTag, t } from '../../core/index.js'; +import { type SelectTag, SelectTagSchema, t } from '../../core/index.js'; import { propertyType } from '../../core/property/property-config.js'; export const selectPropertyType = propertyType('select'); - -export type SelectPropertyData = { - options: SelectTag[]; -}; -export const selectPropertyModelConfig = selectPropertyType.modelConfig< - string | undefined, - SelectPropertyData ->({ +export const SelectPropertySchema = zod.object({ + options: zod.array(SelectTagSchema), +}); +export type SelectPropertyData = zod.infer; +export const selectPropertyModelConfig = selectPropertyType.modelConfig({ name: 'Select', - valueSchema: zod.string().optional(), - type: ({ data }) => t.tag.instance(data.options), - defaultData: () => ({ - options: [], - }), + propertyData: { + schema: SelectPropertySchema, + default: () => ({ + options: [], + }), + }, + jsonValue: { + schema: zod.string().nullable(), + isEmpty: ({ value }) => value == null, + type: ({ data }) => t.tag.instance(data.options), + }, + rawValue: { + schema: zod.string().nullable(), + default: () => null, + toString: ({ value, data }) => + data.options.find(v => v.id === value)?.value ?? '', + fromString: ({ value: oldValue, data }) => { + if (!oldValue) { + return { value: null, data: data }; + } + const optionMap = Object.fromEntries(data.options.map(v => [v.value, v])); + const name = oldValue + .split(',') + .map(v => v.trim()) + .filter(v => v)[0]; + if (!name) { + return { value: null, data: data }; + } + + let value: string | undefined; + const option = optionMap[name]; + if (!option) { + const newOption: SelectTag = { + id: nanoid(), + value: name, + color: getTagColor(), + }; + data.options.push(newOption); + value = newOption.id; + } else { + value = option.id; + } + + return { + value, + data: data, + }; + }, + toJson: ({ value }) => value, + fromJson: ({ value }) => value, + }, addGroup: ({ text, oldData }) => { return { options: [ @@ -27,41 +70,4 @@ export const selectPropertyModelConfig = selectPropertyType.modelConfig< ], }; }, - cellToString: ({ value, data }) => - data.options.find(v => v.id === value)?.value ?? '', - cellFromString: ({ value: oldValue, data }) => { - if (!oldValue) { - return { value: null, data: data }; - } - const optionMap = Object.fromEntries(data.options.map(v => [v.value, v])); - const name = oldValue - .split(',') - .map(v => v.trim()) - .filter(v => v)[0]; - if (!name) { - return { value: null, data: data }; - } - - let value: string | undefined; - const option = optionMap[name]; - if (!option) { - const newOption: SelectTag = { - id: nanoid(), - value: name, - color: getTagColor(), - }; - data.options.push(newOption); - value = newOption.id; - } else { - value = option.id; - } - - return { - value, - data: data, - }; - }, - cellToJson: ({ value }) => value ?? null, - cellFromJson: ({ value }) => (typeof value !== 'string' ? undefined : value), - isEmpty: ({ value }) => value == null, }); diff --git a/blocksuite/affine/data-view/src/property-presets/text/cell-renderer.ts b/blocksuite/affine/data-view/src/property-presets/text/cell-renderer.ts index d7753a504..77a77f5ea 100644 --- a/blocksuite/affine/data-view/src/property-presets/text/cell-renderer.ts +++ b/blocksuite/affine/data-view/src/property-presets/text/cell-renderer.ts @@ -7,7 +7,7 @@ import { createIcon } from '../../core/utils/uni-icon.js'; import { textInputStyle, textStyle } from './cell-renderer.css.js'; import { textPropertyModelConfig } from './define.js'; -export class TextCell extends BaseCellRenderer { +export class TextCell extends BaseCellRenderer { @query('input') private accessor _inputEle!: HTMLInputElement; diff --git a/blocksuite/affine/data-view/src/property-presets/text/define.ts b/blocksuite/affine/data-view/src/property-presets/text/define.ts index 64f8576ef..a7d6f62bc 100644 --- a/blocksuite/affine/data-view/src/property-presets/text/define.ts +++ b/blocksuite/affine/data-view/src/property-presets/text/define.ts @@ -6,16 +6,24 @@ export const textPropertyType = propertyType('text'); export const textPropertyModelConfig = textPropertyType.modelConfig({ name: 'Plain-Text', - valueSchema: zod.string().optional(), - type: () => t.string.instance(), - defaultData: () => ({}), - cellToString: ({ value }) => value ?? '', - cellFromString: ({ value }) => { - return { - value: value, - }; + propertyData: { + schema: zod.object({}), + default: () => ({}), }, - cellToJson: ({ value }) => value ?? null, - cellFromJson: ({ value }) => (typeof value !== 'string' ? undefined : value), - isEmpty: ({ value }) => value == null || value.length === 0, + jsonValue: { + schema: zod.string(), + type: () => t.string.instance(), + isEmpty: ({ value }) => !value, + }, + rawValue: { + schema: zod.string(), + default: () => '', + toString: ({ value }) => value, + fromString: ({ value }) => { + return { value: value }; + }, + toJson: ({ value }) => value, + fromJson: ({ value }) => value, + }, + hide: true, }); diff --git a/blocksuite/affine/data-view/src/view-presets/kanban/kanban-view-manager.ts b/blocksuite/affine/data-view/src/view-presets/kanban/kanban-view-manager.ts index c9b6b774e..f7aed80bc 100644 --- a/blocksuite/affine/data-view/src/view-presets/kanban/kanban-view-manager.ts +++ b/blocksuite/affine/data-view/src/view-presets/kanban/kanban-view-manager.ts @@ -14,6 +14,7 @@ import { groupTraitKey, sortByManually, } from '../../core/group-by/trait.js'; +import { fromJson } from '../../core/property/utils'; import { PropertyBase } from '../../core/view-manager/property.js'; import { SingleViewBase } from '../../core/view-manager/single-view.js'; import type { KanbanViewData } from './define.js'; @@ -183,14 +184,15 @@ export class KanbanSingleView extends SingleViewBase { Object.entries(defaultValues).forEach(([propertyId, jsonValue]) => { const property = this.propertyGet(propertyId); const propertyMeta = this.propertyMetaGet(property.type$.value); - if (propertyMeta?.config.cellFromJson) { - const value = propertyMeta.config.cellFromJson({ - value: jsonValue, - data: property.data$.value, - dataSource: this.dataSource, - }); - this.cellValueSet(id, propertyId, value); + if (!propertyMeta) { + return; } + const value = fromJson(propertyMeta.config, { + value: jsonValue, + data: property.data$.value, + dataSource: this.dataSource, + }); + this.cellValueSet(id, propertyId, value); }); } diff --git a/blocksuite/affine/data-view/src/view-presets/table/mobile/column-header.ts b/blocksuite/affine/data-view/src/view-presets/table/mobile/column-header.ts index 03ffaac34..ae5e272e2 100644 --- a/blocksuite/affine/data-view/src/view-presets/table/mobile/column-header.ts +++ b/blocksuite/affine/data-view/src/view-presets/table/mobile/column-header.ts @@ -23,7 +23,6 @@ import { html } from 'lit/static-html.js'; import { inputConfig, typeConfig } from '../../../core/common/property-menu.js'; import type { Property } from '../../../core/view-manager/property.js'; -import type { NumberPropertyDataType } from '../../../property-presets/index.js'; import { numberFormats } from '../../../property-presets/number/utils/formats.js'; import { DEFAULT_COLUMN_TITLE_HEIGHT } from '../consts.js'; import type { TableColumn, TableSingleView } from '../table-view-manager.js'; @@ -91,12 +90,7 @@ export class MobileTableColumnHeader extends SignalWatcher( items: [ numberFormatConfig(this.column), ...numberFormats.map(format => { - const data = ( - this.column as Property< - number, - NumberPropertyDataType - > - ).data$.value; + const data = this.column.data$.value; return menu.action({ isSelected: data.format === format.type, prefix: html` { - const data = ( - this.column as Property< - number, - NumberPropertyDataType - > - ).data$.value; + const data = this.column.data$.value; return menu.action({ isSelected: data.format === format.type, prefix: html` cell.value$.value); + return this.column.cells$.value.map(cell => cell.jsonValue$.value); }); groups$ = computed(() => { diff --git a/blocksuite/affine/data-view/src/view-presets/table/table-view-manager.ts b/blocksuite/affine/data-view/src/view-presets/table/table-view-manager.ts index ea549e71b..63a823db6 100644 --- a/blocksuite/affine/data-view/src/view-presets/table/table-view-manager.ts +++ b/blocksuite/affine/data-view/src/view-presets/table/table-view-manager.ts @@ -14,6 +14,7 @@ import { groupTraitKey, sortByManually, } from '../../core/group-by/trait.js'; +import { fromJson } from '../../core/property/utils'; import { SortManager, sortTraitKey } from '../../core/sort/manager.js'; import { PropertyBase } from '../../core/view-manager/property.js'; import { @@ -327,8 +328,8 @@ export class TableSingleView extends SingleViewBase { Object.entries(defaultValues).forEach(([propertyId, jsonValue]) => { const property = this.propertyGet(propertyId); const propertyMeta = this.propertyMetaGet(property.type$.value); - if (propertyMeta?.config.cellFromJson) { - const value = propertyMeta.config.cellFromJson({ + if (propertyMeta) { + const value = fromJson(propertyMeta.config, { value: jsonValue, data: property.data$.value, dataSource: this.dataSource, diff --git a/blocksuite/playground/apps/starter/data/database.ts b/blocksuite/playground/apps/starter/data/database.ts index a8edb4221..445131281 100644 --- a/blocksuite/playground/apps/starter/data/database.ts +++ b/blocksuite/playground/apps/starter/data/database.ts @@ -79,11 +79,13 @@ export const database: InitFn = (collection: Workspace, id: string) => { { type: type, text: new Text(`Paragraph type ${type}`) }, databaseId ); - datasource.cellValueChange( - id, - richTextId, - new Text(`Paragraph type ${type}`) - ); + if (richTextId) { + datasource.cellValueChange( + id, + richTextId, + new Text(`Paragraph type ${type}`) + ); + } }); const listTypes: ListType[] = ['numbered', 'bulleted', 'todo', 'toggle']; @@ -93,11 +95,13 @@ export const database: InitFn = (collection: Workspace, id: string) => { { type: type, text: new Text(`List type ${type}`) }, databaseId ); - datasource.cellValueChange( - id, - richTextId, - new Text(`List type ${type}`) - ); + if (richTextId) { + datasource.cellValueChange( + id, + richTextId, + new Text(`List type ${type}`) + ); + } }); // Add a paragraph after database doc.addBlock('affine:paragraph', {}, noteId); diff --git a/blocksuite/tests-legacy/e2e/database/database.spec.ts b/blocksuite/tests-legacy/e2e/database/database.spec.ts index e132fa1c0..128fd0018 100644 --- a/blocksuite/tests-legacy/e2e/database/database.spec.ts +++ b/blocksuite/tests-legacy/e2e/database/database.spec.ts @@ -531,8 +531,11 @@ test.describe('readonly mode', () => { }); await cell.click(); await pressEnter(page); + await waitNextFrame(page, 100); await type(page, '123'); + await waitNextFrame(page, 100); await pressEnter(page); + await waitNextFrame(page, 100); await assertDatabaseCellRichTexts(page, { text: '123' }); await switchReadonly(page); diff --git a/blocksuite/tests-legacy/e2e/utils/actions/misc.ts b/blocksuite/tests-legacy/e2e/utils/actions/misc.ts index ab926d31b..e9127333a 100644 --- a/blocksuite/tests-legacy/e2e/utils/actions/misc.ts +++ b/blocksuite/tests-legacy/e2e/utils/actions/misc.ts @@ -386,6 +386,9 @@ export async function initKanbanViewState( }); config.columns.forEach(column => { const columnId = datasource.propertyAdd('end', column.type); + if (!columnId) { + return; + } datasource.propertyNameSet(columnId, column.type); rowIds.forEach((rowId, index) => { const value = column.value?.[index];