From 77e4b9aa8e318289db88afc2155328a874e6f928 Mon Sep 17 00:00:00 2001 From: zzj3720 <17165520+zzj3720@users.noreply.github.com> Date: Tue, 11 Mar 2025 02:12:40 +0000 Subject: [PATCH] refactor(editor): add schema for value of database block properties (#10749) --- .../block-data-view/src/block-meta/todo.ts | 2 +- .../block-data-view/src/columns/index.ts | 4 +- .../affine/blocks/block-database/src/index.ts | 1 - .../src/properties/link/define.ts | 5 +- .../src/properties/rich-text/define.ts | 104 ++++++++------- .../src/properties/title/define.ts | 12 +- .../block-database/src/properties/utils.ts | 9 -- .../data-view/src/core/data-source/base.ts | 2 +- .../data-view/src/core/property/types.ts | 2 + .../src/property-presets/checkbox/define.ts | 32 ++--- .../src/property-presets/date/define.ts | 8 +- .../src/property-presets/image/define.ts | 6 +- .../property-presets/multi-select/define.ts | 125 +++++++++--------- .../src/property-presets/number/define.ts | 6 +- .../src/property-presets/progress/define.ts | 41 +++--- .../src/property-presets/select/define.ts | 5 +- .../src/property-presets/text/define.ts | 6 +- 17 files changed, 198 insertions(+), 172 deletions(-) delete mode 100644 blocksuite/affine/blocks/block-database/src/properties/utils.ts 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 db3e96ee2..78f80ce18 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 @@ -36,7 +36,7 @@ todoMeta.addProperty({ metaConfig: propertyPresets.checkboxPropertyConfig, get: block => block.checked, set: (block, value) => { - block.checked = value; + block.checked = value ?? false; }, updated: (block, callback) => { return block.propsUpdated.on(({ key }) => { 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 dd545658d..dfae61b7f 100644 --- a/blocksuite/affine/blocks/block-data-view/src/columns/index.ts +++ b/blocksuite/affine/blocks/block-data-view/src/columns/index.ts @@ -10,7 +10,9 @@ export const queryBlockColumns = [ propertyPresets.multiSelectPropertyConfig, propertyPresets.checkboxPropertyConfig, ]; -export const queryBlockHiddenColumns = [richTextColumnConfig]; +export const queryBlockHiddenColumns: PropertyMetaConfig[] = [ + 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-database/src/index.ts b/blocksuite/affine/blocks/block-database/src/index.ts index 1e58ca1c8..29054837f 100644 --- a/blocksuite/affine/blocks/block-database/src/index.ts +++ b/blocksuite/affine/blocks/block-database/src/index.ts @@ -8,6 +8,5 @@ export * from './detail-panel/block-renderer'; export * from './detail-panel/note-renderer'; export * from './properties'; export * from './properties/rich-text/cell-renderer'; -export * from './properties/utils'; export * from './selection.js'; export * from './utils/block-utils'; 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 13da897ef..12203ddd2 100644 --- a/blocksuite/affine/blocks/block-database/src/properties/link/define.ts +++ b/blocksuite/affine/blocks/block-database/src/properties/link/define.ts @@ -1,8 +1,9 @@ import { propertyType, t } from '@blocksuite/data-view'; - +import zod from 'zod'; export const linkColumnType = propertyType('link'); -export const linkPropertyModelConfig = linkColumnType.modelConfig({ +export const linkPropertyModelConfig = linkColumnType.modelConfig({ name: 'Link', + valueSchema: zod.string().optional(), type: () => t.string.instance(), defaultData: () => ({}), cellToString: ({ value }) => value?.toString() ?? '', 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 6373f4e41..1bb0b90ab 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 @@ -2,56 +2,68 @@ import type { AffineTextAttributes } from '@blocksuite/affine-shared/types'; import { propertyType, t } from '@blocksuite/data-view'; import type { DeltaInsert } from '@blocksuite/inline'; import { Text } from '@blocksuite/store'; +import * as Y from 'yjs'; +import zod from 'zod'; import { HostContextKey } from '../../context/host-context.js'; import { isLinkedDoc } from '../../utils/title-doc.js'; -import { type RichTextCellType, toYText } from '../utils.js'; export const richTextColumnType = propertyType('rich-text'); +export type RichTextCellType = Text | Text['yText']; +export const toYText = (text?: RichTextCellType): undefined | Text['yText'] => { + if (text instanceof Text) { + return text.yText; + } + return text; +}; -export const richTextPropertyModelConfig = - richTextColumnType.modelConfig({ - name: 'Text', - type: () => t.richText.instance(), - defaultData: () => ({}), - cellToString: ({ value }) => value?.toString() ?? '', - cellFromString: ({ value }) => { - return { - value: new Text(value), - }; - }, - cellToJson: ({ 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; - }, - cellFromJson: ({ value }) => - typeof value !== 'string' ? undefined : new Text(value), - onUpdate: ({ value, callback }) => { +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), + }; + }, + cellToJson: ({ value, dataSource }) => { + if (!value) return null; + const host = dataSource.contextGet(HostContextKey); + if (host) { + const collection = host.std.workspace; 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()] : []), - }); + 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; + }, + 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 9631a1d4e..a1552bd4a 100644 --- a/blocksuite/affine/blocks/block-database/src/properties/title/define.ts +++ b/blocksuite/affine/blocks/block-database/src/properties/title/define.ts @@ -1,13 +1,15 @@ import { propertyType, t } from '@blocksuite/data-view'; import { Text } from '@blocksuite/store'; +import zod from 'zod'; import { HostContextKey } from '../../context/host-context.js'; import { isLinkedDoc } from '../../utils/title-doc.js'; export const titleColumnType = propertyType('title'); -export const titlePropertyModelConfig = titleColumnType.modelConfig({ +export const titlePropertyModelConfig = titleColumnType.modelConfig({ name: 'Title', + valueSchema: zod.custom(data => data instanceof Text).optional(), fixed: { defaultData: {}, defaultShow: true, @@ -42,22 +44,22 @@ export const titlePropertyModelConfig = titleColumnType.modelConfig({ cellFromJson: ({ value }) => typeof value !== 'string' ? undefined : new Text(value), onUpdate: ({ value, callback }) => { - value.yText.observe(callback); + value?.yText.observe(callback); callback(); return { dispose: () => { - value.yText.unobserve(callback); + value?.yText.unobserve(callback); }, }; }, valueUpdate: ({ value, newValue }) => { const v = newValue as unknown; if (typeof v === 'string') { - value.replace(0, value.length, v); + value?.replace(0, value.length, v); return value; } if (v == null) { - value.replace(0, value.length, ''); + value?.replace(0, value.length, ''); return value; } return newValue; diff --git a/blocksuite/affine/blocks/block-database/src/properties/utils.ts b/blocksuite/affine/blocks/block-database/src/properties/utils.ts deleted file mode 100644 index 851c5eb61..000000000 --- a/blocksuite/affine/blocks/block-database/src/properties/utils.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Text } from '@blocksuite/store'; - -export type RichTextCellType = Text | Text['yText']; -export const toYText = (text: RichTextCellType): Text['yText'] => { - if (text instanceof Text) { - return text.yText; - } - return text; -}; 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 3f68fb15f..ca8149c39 100644 --- a/blocksuite/affine/data-view/src/core/data-source/base.ts +++ b/blocksuite/affine/data-view/src/core/data-source/base.ts @@ -197,7 +197,7 @@ export abstract class DataSourceBase implements DataSource { return computed(() => this.propertyReadonlyGet(propertyId)); } - abstract propertyTypeGet(propertyId: string): string; + abstract propertyTypeGet(propertyId: string): string | undefined; propertyTypeGet$(propertyId: string): ReadonlySignal { return computed(() => this.propertyTypeGet(propertyId)); diff --git a/blocksuite/affine/data-view/src/core/property/types.ts b/blocksuite/affine/data-view/src/core/property/types.ts index 8dce4a46e..567f5ed42 100644 --- a/blocksuite/affine/data-view/src/core/property/types.ts +++ b/blocksuite/affine/data-view/src/core/property/types.ts @@ -1,4 +1,5 @@ import type { Disposable } from '@blocksuite/global/slot'; +import type { ZodType } from 'zod'; import type { DataSource } from '../data-source/base.js'; import type { TypeInstance } from '../logical/type.js'; @@ -15,6 +16,7 @@ export type PropertyConfig< Value = unknown, > = { name: string; + valueSchema: ZodType; hide?: boolean; fixed?: { defaultData: Data; 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 ff7ebb09a..24669bce9 100644 --- a/blocksuite/affine/data-view/src/property-presets/checkbox/define.ts +++ b/blocksuite/affine/data-view/src/property-presets/checkbox/define.ts @@ -1,6 +1,7 @@ +import zod from 'zod'; + import { t } from '../../core/logical/type-presets.js'; import { propertyType } from '../../core/property/property-config.js'; - export const checkboxPropertyType = propertyType('checkbox'); const FALSE_VALUES = new Set([ @@ -18,18 +19,17 @@ const FALSE_VALUES = new Set([ '关闭', ]); -export const checkboxPropertyModelConfig = - checkboxPropertyType.modelConfig({ - name: 'Checkbox', - 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, - minWidth: 34, - }); +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, + minWidth: 34, +}); 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 770951755..84caccdf1 100644 --- a/blocksuite/affine/data-view/src/property-presets/date/define.ts +++ b/blocksuite/affine/data-view/src/property-presets/date/define.ts @@ -1,15 +1,17 @@ import { format } from 'date-fns/format'; import { parse } from 'date-fns/parse'; +import zod from 'zod'; import { t } from '../../core/logical/type-presets.js'; import { propertyType } from '../../core/property/property-config.js'; - export const datePropertyType = propertyType('date'); -export const datePropertyModelConfig = datePropertyType.modelConfig({ +export const datePropertyModelConfig = datePropertyType.modelConfig({ name: 'Date', type: () => t.date.instance(), + valueSchema: zod.number().optional(), defaultData: () => ({}), - cellToString: ({ value }) => format(value, 'yyyy-MM-dd'), + cellToString: ({ value }) => + value != null ? format(value, 'yyyy-MM-dd') : '', cellFromString: ({ value }) => { const date = parse(value, 'yyyy-MM-dd', new Date()); 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 722038d8a..1cca5f953 100644 --- a/blocksuite/affine/data-view/src/property-presets/image/define.ts +++ b/blocksuite/affine/data-view/src/property-presets/image/define.ts @@ -1,10 +1,12 @@ +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({ +export const imagePropertyModelConfig = imagePropertyType.modelConfig({ name: 'image', + valueSchema: zod.string().optional(), hide: true, type: () => t.image.instance(), defaultData: () => ({}), 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 1a9182f5a..0053fbb56 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 @@ -1,69 +1,76 @@ 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 { propertyType } from '../../core/property/property-config.js'; import type { SelectPropertyData } from '../select/define.js'; - export const multiSelectPropertyType = propertyType('multi-select'); export const multiSelectPropertyModelConfig = - multiSelectPropertyType.modelConfig({ - name: 'Multi-select', - type: ({ data }) => t.array.instance(t.tag.instance(data.options)), - defaultData: () => ({ - 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 }) => { - const optionMap = Object.fromEntries(data.options.map(v => [v.value, v])); - const optionNames = oldValue - .split(',') - .map(v => v.trim()) - .filter(v => v); - - const value: string[] = []; - optionNames.forEach(name => { - if (!optionMap[name]) { - const newOption: SelectTag = { - id: nanoid(), - value: name, - color: getTagColor(), - }; - data.options.push(newOption); - value.push(newOption.id); - } else { - value.push(optionMap[name].id); + multiSelectPropertyType.modelConfig( + { + name: 'Multi-select', + valueSchema: zod.array(zod.string()).optional(), + type: ({ data }) => t.array.instance(t.tag.instance(data.options)), + defaultData: () => ({ + 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 }) => { + const optionMap = Object.fromEntries( + data.options.map(v => [v.value, v]) + ); + const optionNames = oldValue + .split(',') + .map(v => v.trim()) + .filter(v => v); - return { - value, - data: data, - }; - }, - cellToJson: ({ value }) => value ?? null, - cellFromJson: ({ value }) => - Array.isArray(value) && value.every(v => typeof v === 'string') - ? value - : undefined, - isEmpty: ({ value }) => value == null || value.length === 0, - }); + const value: string[] = []; + optionNames.forEach(name => { + if (!optionMap[name]) { + const newOption: SelectTag = { + id: nanoid(), + value: name, + color: getTagColor(), + }; + data.options.push(newOption); + value.push(newOption.id); + } else { + value.push(optionMap[name].id); + } + }); + + return { + value, + data: data, + }; + }, + cellToJson: ({ value }) => value ?? null, + cellFromJson: ({ value }) => + Array.isArray(value) && value.every(v => typeof v === 'string') + ? value + : undefined, + isEmpty: ({ value }) => value == null || value.length === 0, + } + ); 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 b39932b59..c88099e2f 100644 --- a/blocksuite/affine/data-view/src/property-presets/number/define.ts +++ b/blocksuite/affine/data-view/src/property-presets/number/define.ts @@ -1,14 +1,16 @@ +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'; - export const numberPropertyType = propertyType('number'); export const numberPropertyModelConfig = numberPropertyType.modelConfig< - number, + number | undefined, NumberPropertyDataType >({ name: 'Number', + valueSchema: zod.number().optional(), type: () => t.number.instance(), defaultData: () => ({ decimal: 0, format: 'number' }), cellToString: ({ value }) => value?.toString() ?? '', 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 4fbc660cf..e841ce396 100644 --- a/blocksuite/affine/data-view/src/property-presets/progress/define.ts +++ b/blocksuite/affine/data-view/src/property-presets/progress/define.ts @@ -1,24 +1,25 @@ +import zod from 'zod'; + import { t } from '../../core/logical/type-presets.js'; import { propertyType } from '../../core/property/property-config.js'; - export const progressPropertyType = propertyType('progress'); -export const progressPropertyModelConfig = - progressPropertyType.modelConfig({ - name: 'Progress', - type: () => t.number.instance(), - defaultData: () => ({}), - cellToString: ({ value }) => value?.toString() ?? '', - cellFromString: ({ value }) => { - const num = value ? Number(value) : NaN; - return { - value: isNaN(num) ? null : num, - }; - }, - cellToJson: ({ value }) => value ?? null, - cellFromJson: ({ value }) => { - if (typeof value !== 'number') return undefined; - return value; - }, - isEmpty: () => false, - }); +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, + }; + }, + cellToJson: ({ value }) => value ?? null, + cellFromJson: ({ value }) => { + if (typeof value !== 'number') return undefined; + return value; + }, + isEmpty: () => false, +}); 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 ed611152c..e1fa345d8 100644 --- a/blocksuite/affine/data-view/src/property-presets/select/define.ts +++ b/blocksuite/affine/data-view/src/property-presets/select/define.ts @@ -1,19 +1,20 @@ 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 { propertyType } from '../../core/property/property-config.js'; - export const selectPropertyType = propertyType('select'); export type SelectPropertyData = { options: SelectTag[]; }; export const selectPropertyModelConfig = selectPropertyType.modelConfig< - string, + string | undefined, SelectPropertyData >({ name: 'Select', + valueSchema: zod.string().optional(), type: ({ data }) => t.tag.instance(data.options), defaultData: () => ({ options: [], 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 8df3b2a31..64f8576ef 100644 --- a/blocksuite/affine/data-view/src/property-presets/text/define.ts +++ b/blocksuite/affine/data-view/src/property-presets/text/define.ts @@ -1,10 +1,12 @@ +import zod from 'zod'; + import { t } from '../../core/index.js'; import { propertyType } from '../../core/property/property-config.js'; - export const textPropertyType = propertyType('text'); -export const textPropertyModelConfig = textPropertyType.modelConfig({ +export const textPropertyModelConfig = textPropertyType.modelConfig({ name: 'Plain-Text', + valueSchema: zod.string().optional(), type: () => t.string.instance(), defaultData: () => ({}), cellToString: ({ value }) => value ?? '',