refactor(editor): add runtime type checks to database cell values (#10770)

This commit is contained in:
zzj3720
2025-03-12 09:22:41 +00:00
parent fd3ce431fe
commit 01151ec18f
54 changed files with 775 additions and 629 deletions

View File

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

View File

@@ -107,7 +107,7 @@ export class DatabaseBlockDataSource extends DataSourceBase {
return this._model.doc;
}
allPropertyMetas$ = computed<PropertyMetaConfig<any, any, any>[]>(() => {
allPropertyMetas$ = computed<PropertyMetaConfig<any, any, any, any>[]>(() => {
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();

View File

@@ -29,7 +29,7 @@ import {
} from './cell-renderer.css.js';
import { linkPropertyModelConfig } from './define.js';
export class LinkCell extends BaseCellRenderer<string> {
export class LinkCell extends BaseCellRenderer<string, string> {
protected override firstUpdated(_changedProperties: PropertyValues) {
super.firstUpdated(_changedProperties);
this.classList.add(linkCellStyle);

View File

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

View File

@@ -81,7 +81,7 @@ function toggleStyle(
inlineEditor.syncInlineRange();
}
export class RichTextCell extends BaseCellRenderer<Text> {
export class RichTextCell extends BaseCellRenderer<Text, string> {
inlineEditor$ = computed(() => {
return this.richText$.value?.inlineEditor;
});

View File

@@ -19,51 +19,59 @@ export const toYText = (text?: RichTextCellType): undefined | Text['yText'] => {
export const richTextPropertyModelConfig = richTextColumnType.modelConfig({
name: 'Text',
valueSchema: zod
.custom<RichTextCellType>(
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<RichTextCellType>(
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<AffineTextAttributes>) => {
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<AffineTextAttributes>) => {
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()] : []),
});

View File

@@ -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<Text>(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<Text>(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()] : []),
});

View File

@@ -29,7 +29,7 @@ import {
titleRichTextStyle,
} from './cell-renderer.css.js';
export class HeaderAreaTextCell extends BaseCellRenderer<Text> {
export class HeaderAreaTextCell extends BaseCellRenderer<Text, string> {
activity = true;
docId$ = signal<string>();