refactor(editor): add schema for value of database block properties (#10749)

This commit is contained in:
zzj3720
2025-03-11 02:12:40 +00:00
parent db707dff7f
commit 77e4b9aa8e
17 changed files with 198 additions and 172 deletions

View File

@@ -36,7 +36,7 @@ todoMeta.addProperty({
metaConfig: propertyPresets.checkboxPropertyConfig, metaConfig: propertyPresets.checkboxPropertyConfig,
get: block => block.checked, get: block => block.checked,
set: (block, value) => { set: (block, value) => {
block.checked = value; block.checked = value ?? false;
}, },
updated: (block, callback) => { updated: (block, callback) => {
return block.propsUpdated.on(({ key }) => { return block.propsUpdated.on(({ key }) => {

View File

@@ -10,7 +10,9 @@ export const queryBlockColumns = [
propertyPresets.multiSelectPropertyConfig, propertyPresets.multiSelectPropertyConfig,
propertyPresets.checkboxPropertyConfig, propertyPresets.checkboxPropertyConfig,
]; ];
export const queryBlockHiddenColumns = [richTextColumnConfig]; export const queryBlockHiddenColumns: PropertyMetaConfig<string, any, any>[] = [
richTextColumnConfig,
];
const queryBlockAllColumns = [...queryBlockColumns, ...queryBlockHiddenColumns]; const queryBlockAllColumns = [...queryBlockColumns, ...queryBlockHiddenColumns];
export const queryBlockAllColumnMap = Object.fromEntries( export const queryBlockAllColumnMap = Object.fromEntries(
queryBlockAllColumns.map(v => [v.type, v as PropertyMetaConfig]) queryBlockAllColumns.map(v => [v.type, v as PropertyMetaConfig])

View File

@@ -8,6 +8,5 @@ export * from './detail-panel/block-renderer';
export * from './detail-panel/note-renderer'; export * from './detail-panel/note-renderer';
export * from './properties'; export * from './properties';
export * from './properties/rich-text/cell-renderer'; export * from './properties/rich-text/cell-renderer';
export * from './properties/utils';
export * from './selection.js'; export * from './selection.js';
export * from './utils/block-utils'; export * from './utils/block-utils';

View File

@@ -1,8 +1,9 @@
import { propertyType, t } from '@blocksuite/data-view'; import { propertyType, t } from '@blocksuite/data-view';
import zod from 'zod';
export const linkColumnType = propertyType('link'); export const linkColumnType = propertyType('link');
export const linkPropertyModelConfig = linkColumnType.modelConfig<string>({ export const linkPropertyModelConfig = linkColumnType.modelConfig({
name: 'Link', name: 'Link',
valueSchema: zod.string().optional(),
type: () => t.string.instance(), type: () => t.string.instance(),
defaultData: () => ({}), defaultData: () => ({}),
cellToString: ({ value }) => value?.toString() ?? '', cellToString: ({ value }) => value?.toString() ?? '',

View File

@@ -2,56 +2,68 @@ import type { AffineTextAttributes } from '@blocksuite/affine-shared/types';
import { propertyType, t } from '@blocksuite/data-view'; import { propertyType, t } from '@blocksuite/data-view';
import type { DeltaInsert } from '@blocksuite/inline'; import type { DeltaInsert } from '@blocksuite/inline';
import { Text } from '@blocksuite/store'; import { Text } from '@blocksuite/store';
import * as Y from 'yjs';
import zod from 'zod';
import { HostContextKey } from '../../context/host-context.js'; import { HostContextKey } from '../../context/host-context.js';
import { isLinkedDoc } from '../../utils/title-doc.js'; import { isLinkedDoc } from '../../utils/title-doc.js';
import { type RichTextCellType, toYText } from '../utils.js';
export const richTextColumnType = propertyType('rich-text'); 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 = export const richTextPropertyModelConfig = richTextColumnType.modelConfig({
richTextColumnType.modelConfig<RichTextCellType>({ name: 'Text',
name: 'Text', valueSchema: zod
type: () => t.richText.instance(), .custom<RichTextCellType>(
defaultData: () => ({}), data => data instanceof Text || data instanceof Y.Text
cellToString: ({ value }) => value?.toString() ?? '', )
cellFromString: ({ value }) => { .optional(),
return { type: () => t.richText.instance(),
value: new Text(value), defaultData: () => ({}),
}; cellToString: ({ value }) => value?.toString() ?? '',
}, cellFromString: ({ value }) => {
cellToJson: ({ value, dataSource }) => { return {
if (!value) return null; value: new Text(value),
const host = dataSource.contextGet(HostContextKey); };
if (host) { },
const collection = host.std.workspace; cellToJson: ({ value, dataSource }) => {
const yText = toYText(value); if (!value) return null;
const deltas = yText.toDelta(); const host = dataSource.contextGet(HostContextKey);
const text = deltas if (host) {
.map((delta: DeltaInsert<AffineTextAttributes>) => { const collection = host.std.workspace;
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); const yText = toYText(value);
yText.observe(callback); const deltas = yText?.toDelta();
callback(); const text = deltas
return { .map((delta: DeltaInsert<AffineTextAttributes>) => {
dispose: () => { if (isLinkedDoc(delta)) {
yText.unobserve(callback); const linkedDocId = delta.attributes?.reference?.pageId as string;
}, return collection.getDoc(linkedDocId)?.meta?.title;
}; }
}, return delta.insert;
isEmpty: ({ value }) => value == null || value.length === 0, })
values: ({ value }) => (value?.toString() ? [value.toString()] : []), .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()] : []),
});

View File

@@ -1,13 +1,15 @@
import { propertyType, t } from '@blocksuite/data-view'; import { propertyType, t } from '@blocksuite/data-view';
import { Text } from '@blocksuite/store'; import { Text } from '@blocksuite/store';
import zod from 'zod';
import { HostContextKey } from '../../context/host-context.js'; import { HostContextKey } from '../../context/host-context.js';
import { isLinkedDoc } from '../../utils/title-doc.js'; import { isLinkedDoc } from '../../utils/title-doc.js';
export const titleColumnType = propertyType('title'); export const titleColumnType = propertyType('title');
export const titlePropertyModelConfig = titleColumnType.modelConfig<Text>({ export const titlePropertyModelConfig = titleColumnType.modelConfig({
name: 'Title', name: 'Title',
valueSchema: zod.custom<Text>(data => data instanceof Text).optional(),
fixed: { fixed: {
defaultData: {}, defaultData: {},
defaultShow: true, defaultShow: true,
@@ -42,22 +44,22 @@ export const titlePropertyModelConfig = titleColumnType.modelConfig<Text>({
cellFromJson: ({ value }) => cellFromJson: ({ value }) =>
typeof value !== 'string' ? undefined : new Text(value), typeof value !== 'string' ? undefined : new Text(value),
onUpdate: ({ value, callback }) => { onUpdate: ({ value, callback }) => {
value.yText.observe(callback); value?.yText.observe(callback);
callback(); callback();
return { return {
dispose: () => { dispose: () => {
value.yText.unobserve(callback); value?.yText.unobserve(callback);
}, },
}; };
}, },
valueUpdate: ({ value, newValue }) => { valueUpdate: ({ value, newValue }) => {
const v = newValue as unknown; const v = newValue as unknown;
if (typeof v === 'string') { if (typeof v === 'string') {
value.replace(0, value.length, v); value?.replace(0, value.length, v);
return value; return value;
} }
if (v == null) { if (v == null) {
value.replace(0, value.length, ''); value?.replace(0, value.length, '');
return value; return value;
} }
return newValue; return newValue;

View File

@@ -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;
};

View File

@@ -197,7 +197,7 @@ export abstract class DataSourceBase implements DataSource {
return computed(() => this.propertyReadonlyGet(propertyId)); return computed(() => this.propertyReadonlyGet(propertyId));
} }
abstract propertyTypeGet(propertyId: string): string; abstract propertyTypeGet(propertyId: string): string | undefined;
propertyTypeGet$(propertyId: string): ReadonlySignal<string | undefined> { propertyTypeGet$(propertyId: string): ReadonlySignal<string | undefined> {
return computed(() => this.propertyTypeGet(propertyId)); return computed(() => this.propertyTypeGet(propertyId));

View File

@@ -1,4 +1,5 @@
import type { Disposable } from '@blocksuite/global/slot'; import type { Disposable } from '@blocksuite/global/slot';
import type { ZodType } from 'zod';
import type { DataSource } from '../data-source/base.js'; import type { DataSource } from '../data-source/base.js';
import type { TypeInstance } from '../logical/type.js'; import type { TypeInstance } from '../logical/type.js';
@@ -15,6 +16,7 @@ export type PropertyConfig<
Value = unknown, Value = unknown,
> = { > = {
name: string; name: string;
valueSchema: ZodType<Value>;
hide?: boolean; hide?: boolean;
fixed?: { fixed?: {
defaultData: Data; defaultData: Data;

View File

@@ -1,6 +1,7 @@
import zod from 'zod';
import { t } from '../../core/logical/type-presets.js'; import { t } from '../../core/logical/type-presets.js';
import { propertyType } from '../../core/property/property-config.js'; import { propertyType } from '../../core/property/property-config.js';
export const checkboxPropertyType = propertyType('checkbox'); export const checkboxPropertyType = propertyType('checkbox');
const FALSE_VALUES = new Set([ const FALSE_VALUES = new Set([
@@ -18,18 +19,17 @@ const FALSE_VALUES = new Set([
'关闭', '关闭',
]); ]);
export const checkboxPropertyModelConfig = export const checkboxPropertyModelConfig = checkboxPropertyType.modelConfig({
checkboxPropertyType.modelConfig<boolean>({ name: 'Checkbox',
name: 'Checkbox', valueSchema: zod.boolean().optional(),
type: () => t.boolean.instance(), type: () => t.boolean.instance(),
defaultData: () => ({}), defaultData: () => ({}),
cellToString: ({ value }) => (value ? 'True' : 'False'), cellToString: ({ value }) => (value ? 'True' : 'False'),
cellFromString: ({ value }) => ({ cellFromString: ({ value }) => ({
value: !FALSE_VALUES.has((value?.trim() ?? '').toLowerCase()), value: !FALSE_VALUES.has((value?.trim() ?? '').toLowerCase()),
}), }),
cellToJson: ({ value }) => value ?? null, cellToJson: ({ value }) => value ?? null,
cellFromJson: ({ value }) => cellFromJson: ({ value }) => (typeof value !== 'boolean' ? undefined : value),
typeof value !== 'boolean' ? undefined : value, isEmpty: () => false,
isEmpty: () => false, minWidth: 34,
minWidth: 34, });
});

View File

@@ -1,15 +1,17 @@
import { format } from 'date-fns/format'; import { format } from 'date-fns/format';
import { parse } from 'date-fns/parse'; import { parse } from 'date-fns/parse';
import zod from 'zod';
import { t } from '../../core/logical/type-presets.js'; import { t } from '../../core/logical/type-presets.js';
import { propertyType } from '../../core/property/property-config.js'; import { propertyType } from '../../core/property/property-config.js';
export const datePropertyType = propertyType('date'); export const datePropertyType = propertyType('date');
export const datePropertyModelConfig = datePropertyType.modelConfig<number>({ export const datePropertyModelConfig = datePropertyType.modelConfig({
name: 'Date', name: 'Date',
type: () => t.date.instance(), type: () => t.date.instance(),
valueSchema: zod.number().optional(),
defaultData: () => ({}), defaultData: () => ({}),
cellToString: ({ value }) => format(value, 'yyyy-MM-dd'), cellToString: ({ value }) =>
value != null ? format(value, 'yyyy-MM-dd') : '',
cellFromString: ({ value }) => { cellFromString: ({ value }) => {
const date = parse(value, 'yyyy-MM-dd', new Date()); const date = parse(value, 'yyyy-MM-dd', new Date());

View File

@@ -1,10 +1,12 @@
import zod from 'zod';
import { t } from '../../core/logical/type-presets.js'; import { t } from '../../core/logical/type-presets.js';
import { propertyType } from '../../core/property/property-config.js'; import { propertyType } from '../../core/property/property-config.js';
export const imagePropertyType = propertyType('image'); export const imagePropertyType = propertyType('image');
export const imagePropertyModelConfig = imagePropertyType.modelConfig<string>({ export const imagePropertyModelConfig = imagePropertyType.modelConfig({
name: 'image', name: 'image',
valueSchema: zod.string().optional(),
hide: true, hide: true,
type: () => t.image.instance(), type: () => t.image.instance(),
defaultData: () => ({}), defaultData: () => ({}),

View File

@@ -1,69 +1,76 @@
import { nanoid } from '@blocksuite/store'; import { nanoid } from '@blocksuite/store';
import zod from 'zod';
import { getTagColor } from '../../core/component/tags/colors.js'; import { getTagColor } from '../../core/component/tags/colors.js';
import { type SelectTag, t } from '../../core/index.js'; import { type SelectTag, t } from '../../core/index.js';
import { propertyType } from '../../core/property/property-config.js'; import { propertyType } from '../../core/property/property-config.js';
import type { SelectPropertyData } from '../select/define.js'; import type { SelectPropertyData } from '../select/define.js';
export const multiSelectPropertyType = propertyType('multi-select'); export const multiSelectPropertyType = propertyType('multi-select');
export const multiSelectPropertyModelConfig = export const multiSelectPropertyModelConfig =
multiSelectPropertyType.modelConfig<string[], SelectPropertyData>({ multiSelectPropertyType.modelConfig<string[] | undefined, SelectPropertyData>(
name: 'Multi-select', {
type: ({ data }) => t.array.instance(t.tag.instance(data.options)), name: 'Multi-select',
defaultData: () => ({ valueSchema: zod.array(zod.string()).optional(),
options: [], type: ({ data }) => t.array.instance(t.tag.instance(data.options)),
}), defaultData: () => ({
addGroup: ({ text, oldData }) => { options: [],
return { }),
options: [ addGroup: ({ text, oldData }) => {
...(oldData.options ?? []), return {
{ options: [
id: nanoid(), ...(oldData.options ?? []),
value: text, {
color: getTagColor(), id: nanoid(),
}, value: text,
], color: getTagColor(),
}; },
}, ],
formatValue: ({ value }) => { };
if (Array.isArray(value)) { },
return value.filter(v => v != null); formatValue: ({ value }) => {
} if (Array.isArray(value)) {
return []; return value.filter(v => v != null);
},
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);
} }
}); 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 { const value: string[] = [];
value, optionNames.forEach(name => {
data: data, if (!optionMap[name]) {
}; const newOption: SelectTag = {
}, id: nanoid(),
cellToJson: ({ value }) => value ?? null, value: name,
cellFromJson: ({ value }) => color: getTagColor(),
Array.isArray(value) && value.every(v => typeof v === 'string') };
? value data.options.push(newOption);
: undefined, value.push(newOption.id);
isEmpty: ({ value }) => value == null || value.length === 0, } 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,
}
);

View File

@@ -1,14 +1,16 @@
import zod from 'zod';
import { t } from '../../core/logical/type-presets.js'; import { t } from '../../core/logical/type-presets.js';
import { propertyType } from '../../core/property/property-config.js'; import { propertyType } from '../../core/property/property-config.js';
import type { NumberPropertyDataType } from './types.js'; import type { NumberPropertyDataType } from './types.js';
export const numberPropertyType = propertyType('number'); export const numberPropertyType = propertyType('number');
export const numberPropertyModelConfig = numberPropertyType.modelConfig< export const numberPropertyModelConfig = numberPropertyType.modelConfig<
number, number | undefined,
NumberPropertyDataType NumberPropertyDataType
>({ >({
name: 'Number', name: 'Number',
valueSchema: zod.number().optional(),
type: () => t.number.instance(), type: () => t.number.instance(),
defaultData: () => ({ decimal: 0, format: 'number' }), defaultData: () => ({ decimal: 0, format: 'number' }),
cellToString: ({ value }) => value?.toString() ?? '', cellToString: ({ value }) => value?.toString() ?? '',

View File

@@ -1,24 +1,25 @@
import zod from 'zod';
import { t } from '../../core/logical/type-presets.js'; import { t } from '../../core/logical/type-presets.js';
import { propertyType } from '../../core/property/property-config.js'; import { propertyType } from '../../core/property/property-config.js';
export const progressPropertyType = propertyType('progress'); export const progressPropertyType = propertyType('progress');
export const progressPropertyModelConfig = export const progressPropertyModelConfig = progressPropertyType.modelConfig({
progressPropertyType.modelConfig<number>({ name: 'Progress',
name: 'Progress', valueSchema: zod.number().optional(),
type: () => t.number.instance(), type: () => t.number.instance(),
defaultData: () => ({}), defaultData: () => ({}),
cellToString: ({ value }) => value?.toString() ?? '', cellToString: ({ value }) => value?.toString() ?? '',
cellFromString: ({ value }) => { cellFromString: ({ value }) => {
const num = value ? Number(value) : NaN; const num = value ? Number(value) : NaN;
return { return {
value: isNaN(num) ? null : num, value: isNaN(num) ? null : num,
}; };
}, },
cellToJson: ({ value }) => value ?? null, cellToJson: ({ value }) => value ?? null,
cellFromJson: ({ value }) => { cellFromJson: ({ value }) => {
if (typeof value !== 'number') return undefined; if (typeof value !== 'number') return undefined;
return value; return value;
}, },
isEmpty: () => false, isEmpty: () => false,
}); });

View File

@@ -1,19 +1,20 @@
import { nanoid } from '@blocksuite/store'; import { nanoid } from '@blocksuite/store';
import zod from 'zod';
import { getTagColor } from '../../core/component/tags/colors.js'; import { getTagColor } from '../../core/component/tags/colors.js';
import { type SelectTag, t } from '../../core/index.js'; import { type SelectTag, t } from '../../core/index.js';
import { propertyType } from '../../core/property/property-config.js'; import { propertyType } from '../../core/property/property-config.js';
export const selectPropertyType = propertyType('select'); export const selectPropertyType = propertyType('select');
export type SelectPropertyData = { export type SelectPropertyData = {
options: SelectTag[]; options: SelectTag[];
}; };
export const selectPropertyModelConfig = selectPropertyType.modelConfig< export const selectPropertyModelConfig = selectPropertyType.modelConfig<
string, string | undefined,
SelectPropertyData SelectPropertyData
>({ >({
name: 'Select', name: 'Select',
valueSchema: zod.string().optional(),
type: ({ data }) => t.tag.instance(data.options), type: ({ data }) => t.tag.instance(data.options),
defaultData: () => ({ defaultData: () => ({
options: [], options: [],

View File

@@ -1,10 +1,12 @@
import zod from 'zod';
import { t } from '../../core/index.js'; import { t } from '../../core/index.js';
import { propertyType } from '../../core/property/property-config.js'; import { propertyType } from '../../core/property/property-config.js';
export const textPropertyType = propertyType('text'); export const textPropertyType = propertyType('text');
export const textPropertyModelConfig = textPropertyType.modelConfig<string>({ export const textPropertyModelConfig = textPropertyType.modelConfig({
name: 'Plain-Text', name: 'Plain-Text',
valueSchema: zod.string().optional(),
type: () => t.string.instance(), type: () => t.string.instance(),
defaultData: () => ({}), defaultData: () => ({}),
cellToString: ({ value }) => value ?? '', cellToString: ({ value }) => value ?? '',