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
@@ -4,16 +4,17 @@ import type { Block, BlockModel } from '@blocksuite/store';
type PropertyMeta< type PropertyMeta<
T extends BlockModel = BlockModel, T extends BlockModel = BlockModel,
Value = unknown, RawValue = unknown,
JsonValue = unknown,
ColumnData extends NonNullable<unknown> = NonNullable<unknown>, ColumnData extends NonNullable<unknown> = NonNullable<unknown>,
> = { > = {
name: string; name: string;
key: string; key: string;
metaConfig: PropertyMetaConfig<string, ColumnData, Value>; metaConfig: PropertyMetaConfig<string, ColumnData, RawValue, JsonValue>;
getColumnData?: (block: T) => ColumnData; getColumnData?: (block: T) => ColumnData;
setColumnData?: (block: T, data: ColumnData) => void; setColumnData?: (block: T, data: ColumnData) => void;
get: (block: T) => Value; get: (block: T) => RawValue;
set?: (block: T, value: Value) => void; set?: (block: T, value: RawValue) => void;
updated: (block: T, callback: () => void) => DisposableMember; updated: (block: T, callback: () => void) => DisposableMember;
}; };
export type BlockMeta<T extends BlockModel = BlockModel> = { export type BlockMeta<T extends BlockModel = BlockModel> = {
@@ -1,6 +1,4 @@
import { richTextColumnConfig } from '@blocksuite/affine-block-database';
import { type ListBlockModel, ListBlockSchema } from '@blocksuite/affine-model'; import { type ListBlockModel, ListBlockSchema } from '@blocksuite/affine-model';
import { propertyPresets } from '@blocksuite/data-view/property-presets';
import { createBlockMeta } from './base.js'; import { createBlockMeta } from './base.js';
@@ -13,48 +11,3 @@ export const todoMeta = createBlockMeta<ListBlockModel>({
return (block.model as ListBlockModel).type === 'todo'; 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();
});
},
});
@@ -10,9 +10,12 @@ export const queryBlockColumns = [
propertyPresets.multiSelectPropertyConfig, propertyPresets.multiSelectPropertyConfig,
propertyPresets.checkboxPropertyConfig, propertyPresets.checkboxPropertyConfig,
]; ];
export const queryBlockHiddenColumns: PropertyMetaConfig<string, any, any>[] = [ export const queryBlockHiddenColumns: PropertyMetaConfig<
richTextColumnConfig, string,
]; any,
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])
@@ -199,7 +199,7 @@ export class BlockQueryDataSource extends DataSourceBase {
const property = this.getProperty(propertyId); const property = this.getProperty(propertyId);
return ( return (
property.getColumnData?.(this.blocks[0].model) ?? 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 currentCells as any
) ?? { ) ?? {
property: databaseBlockAllPropertyMap[toType].config.defaultData(), property:
databaseBlockAllPropertyMap[toType].config.propertyData.default(),
cells: currentCells.map(() => undefined), cells: currentCells.map(() => undefined),
}; };
this.block.doc.captureSync(); this.block.doc.captureSync();
@@ -94,7 +94,7 @@ export const processTable = (
if (isDelta(cell.value)) { if (isDelta(cell.value)) {
value = cell.value; value = cell.value;
} else { } else {
value = property.config.cellToString({ value = property.config.rawValue.toString({
value: cell.value, value: cell.value,
data: col.data, data: col.data,
}); });
@@ -107,7 +107,7 @@ export class DatabaseBlockDataSource extends DataSourceBase {
return this._model.doc; return this._model.doc;
} }
allPropertyMetas$ = computed<PropertyMetaConfig<any, any, any>[]>(() => { allPropertyMetas$ = computed<PropertyMetaConfig<any, any, any, any>[]>(() => {
return databaseBlockPropertyList; return databaseBlockPropertyList;
}); });
@@ -151,23 +151,27 @@ export class DatabaseBlockDataSource extends DataSourceBase {
this._runCapture(); this._runCapture();
const type = this.propertyTypeGet(propertyId); const type = this.propertyTypeGet(propertyId);
const update = this.propertyMetaGet(type).config.valueUpdate; const update = this.propertyMetaGet(type)?.config.rawValue.setValue;
let newValue = value; const old = this.cellValueGet(rowId, propertyId);
if (update) { const updateFn =
const old = this.cellValueGet(rowId, propertyId); update ??
newValue = update({ (({ setValue, newValue }) => {
value: old, setValue(newValue);
data: this.propertyDataGet(propertyId),
dataSource: this,
newValue: value,
}); });
} updateFn({
if (this._model.columns$.value.some(v => v.id === propertyId)) { value: old,
updateCell(this._model, rowId, { data: this.propertyDataGet(propertyId),
columnId: propertyId, dataSource: this,
value: newValue, 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 { cellValueGet(rowId: string, propertyId: string): unknown {
@@ -183,14 +187,32 @@ export class DatabaseBlockDataSource extends DataSourceBase {
const model = this.getModelById(rowId); const model = this.getModelById(rowId);
return model?.text; 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(); this.doc.captureSync();
const property = this.propertyMetaGet( const property = this.propertyMetaGet(
type ?? propertyPresets.multiSelectPropertyConfig.type type ?? propertyPresets.multiSelectPropertyConfig.type
); );
if (!property) {
return;
}
const result = addProperty( const result = addProperty(
this._model, this._model,
insertToPosition, insertToPosition,
@@ -233,6 +255,9 @@ export class DatabaseBlockDataSource extends DataSourceBase {
} }
if (this.isFixedProperty(propertyId)) { if (this.isFixedProperty(propertyId)) {
const meta = this.propertyMetaGet(propertyId); const meta = this.propertyMetaGet(propertyId);
if (!meta) {
return;
}
const defaultData = meta.config.fixed?.defaultData ?? {}; const defaultData = meta.config.fixed?.defaultData ?? {};
return { return {
column: { column: {
@@ -288,7 +313,10 @@ export class DatabaseBlockDataSource extends DataSourceBase {
} }
const { column } = result; const { column } = result;
const meta = this.propertyMetaGet(column.type); const meta = this.propertyMetaGet(column.type);
return meta.config.type({ if (!meta) {
return;
}
return meta.config?.jsonValue.type({
data: column.data, data: column.data,
dataSource: this, dataSource: this,
}); });
@@ -335,15 +363,8 @@ export class DatabaseBlockDataSource extends DataSourceBase {
return id; return id;
} }
propertyMetaGet(type: string): PropertyMetaConfig { propertyMetaGet(type: string): PropertyMetaConfig | undefined {
const property = databaseBlockAllPropertyMap[type]; return databaseBlockAllPropertyMap[type];
if (!property) {
throw new BlockSuiteError(
ErrorCode.DatabaseBlockError,
`Unknown property type: ${type}`
);
}
return property;
} }
propertyNameGet(propertyId: string): string { propertyNameGet(propertyId: string): string {
@@ -382,6 +403,10 @@ export class DatabaseBlockDataSource extends DataSourceBase {
if (this.isFixedProperty(propertyId)) { if (this.isFixedProperty(propertyId)) {
return; return;
} }
const meta = this.propertyMetaGet(toType);
if (!meta) {
return;
}
const currentType = this.propertyTypeGet(propertyId); const currentType = this.propertyTypeGet(propertyId);
const currentData = this.propertyDataGet(propertyId); const currentData = this.propertyDataGet(propertyId);
const rows = this.rows$.value; const rows = this.rows$.value;
@@ -396,7 +421,7 @@ export class DatabaseBlockDataSource extends DataSourceBase {
currentCells as any currentCells as any
) ?? { ) ?? {
property: this.propertyMetaGet(toType).config.defaultData(), property: meta.config.propertyData.default(),
cells: currentCells.map(() => undefined), cells: currentCells.map(() => undefined),
}; };
this.doc.captureSync(); this.doc.captureSync();
@@ -29,7 +29,7 @@ import {
} from './cell-renderer.css.js'; } from './cell-renderer.css.js';
import { linkPropertyModelConfig } from './define.js'; import { linkPropertyModelConfig } from './define.js';
export class LinkCell extends BaseCellRenderer<string> { export class LinkCell extends BaseCellRenderer<string, string> {
protected override firstUpdated(_changedProperties: PropertyValues) { protected override firstUpdated(_changedProperties: PropertyValues) {
super.firstUpdated(_changedProperties); super.firstUpdated(_changedProperties);
this.classList.add(linkCellStyle); this.classList.add(linkCellStyle);
@@ -3,17 +3,23 @@ import zod from 'zod';
export const linkColumnType = propertyType('link'); export const linkColumnType = propertyType('link');
export const linkPropertyModelConfig = linkColumnType.modelConfig({ export const linkPropertyModelConfig = linkColumnType.modelConfig({
name: 'Link', name: 'Link',
valueSchema: zod.string().optional(), propertyData: {
type: () => t.string.instance(), schema: zod.object({}),
defaultData: () => ({}), default: () => ({}),
cellToString: ({ value }) => value?.toString() ?? '', },
cellFromString: ({ value }) => { jsonValue: {
return { schema: zod.string(),
value: value, 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,
}); });
@@ -81,7 +81,7 @@ function toggleStyle(
inlineEditor.syncInlineRange(); inlineEditor.syncInlineRange();
} }
export class RichTextCell extends BaseCellRenderer<Text> { export class RichTextCell extends BaseCellRenderer<Text, string> {
inlineEditor$ = computed(() => { inlineEditor$ = computed(() => {
return this.richText$.value?.inlineEditor; return this.richText$.value?.inlineEditor;
}); });
@@ -19,51 +19,59 @@ export const toYText = (text?: RichTextCellType): undefined | Text['yText'] => {
export const richTextPropertyModelConfig = richTextColumnType.modelConfig({ export const richTextPropertyModelConfig = richTextColumnType.modelConfig({
name: 'Text', name: 'Text',
valueSchema: zod propertyData: {
.custom<RichTextCellType>( schema: zod.object({}),
data => data instanceof Text || data instanceof Y.Text default: () => ({}),
)
.optional(),
type: () => t.richText.instance(),
defaultData: () => ({}),
cellToString: ({ value }) => value?.toString() ?? '',
cellFromString: ({ value }) => {
return {
value: new Text(value),
};
}, },
cellToJson: ({ value, dataSource }) => { jsonValue: {
if (!value) return null; schema: zod.string(),
const host = dataSource.contextGet(HostContextKey); type: () => t.richText.instance(),
if (host) { isEmpty: ({ value }) => !value,
const collection = host.std.workspace; },
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 yText = toYText(value);
const deltas = yText?.toDelta(); yText?.observe(callback);
const text = deltas callback();
.map((delta: DeltaInsert<AffineTextAttributes>) => { return {
if (isLinkedDoc(delta)) { dispose: () => {
const linkedDocId = delta.attributes?.reference?.pageId as string; yText?.unobserve(callback);
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()] : []),
}); });
@@ -1,5 +1,6 @@
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 { Doc } from 'yjs';
import zod from 'zod'; import zod from 'zod';
import { HostContextKey } from '../../context/host-context.js'; import { HostContextKey } from '../../context/host-context.js';
@@ -9,61 +10,74 @@ export const titleColumnType = propertyType('title');
export const titlePropertyModelConfig = titleColumnType.modelConfig({ export const titlePropertyModelConfig = titleColumnType.modelConfig({
name: 'Title', 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: { fixed: {
defaultData: {}, defaultData: {},
defaultShow: true, 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()] : []),
}); });
@@ -29,7 +29,7 @@ import {
titleRichTextStyle, titleRichTextStyle,
} from './cell-renderer.css.js'; } from './cell-renderer.css.js';
export class HeaderAreaTextCell extends BaseCellRenderer<Text> { export class HeaderAreaTextCell extends BaseCellRenderer<Text, string> {
activity = true; activity = true;
docId$ = signal<string>(); docId$ = signal<string>();
@@ -53,8 +53,11 @@ export interface DataSource {
propertyReadonlyGet(propertyId: string): boolean; propertyReadonlyGet(propertyId: string): boolean;
propertyReadonlyGet$(propertyId: string): ReadonlySignal<boolean>; propertyReadonlyGet$(propertyId: string): ReadonlySignal<boolean>;
propertyMetaGet(type: string): PropertyMetaConfig; propertyMetaGet(type: string): PropertyMetaConfig | undefined;
propertyAdd(insertToPosition: InsertToPosition, type?: string): string; propertyAdd(
insertToPosition: InsertToPosition,
type?: string
): string | undefined;
propertyDuplicate(propertyId: string): string | undefined; propertyDuplicate(propertyId: string): string | undefined;
propertyCanDuplicate(propertyId: string): boolean; propertyCanDuplicate(propertyId: string): boolean;
@@ -152,7 +155,7 @@ export abstract class DataSourceBase implements DataSource {
abstract propertyAdd( abstract propertyAdd(
insertToPosition: InsertToPosition, insertToPosition: InsertToPosition,
type?: string type?: string
): string; ): string | undefined;
abstract propertyDataGet(propertyId: string): Record<string, unknown>; abstract propertyDataGet(propertyId: string): Record<string, unknown>;
@@ -179,7 +182,7 @@ export abstract class DataSourceBase implements DataSource {
abstract propertyDuplicate(propertyId: string): string | undefined; abstract propertyDuplicate(propertyId: string): string | undefined;
abstract propertyMetaGet(type: string): PropertyMetaConfig; abstract propertyMetaGet(type: string): PropertyMetaConfig | undefined;
abstract propertyNameGet(propertyId: string): string; abstract propertyNameGet(propertyId: string): string;
@@ -10,7 +10,7 @@ export const defaultGroupBy = (
data: NonNullable<unknown> data: NonNullable<unknown>
): GroupBy | undefined => { ): GroupBy | undefined => {
const name = groupByMatcher.match( const name = groupByMatcher.match(
propertyMeta.config.type({ data, dataSource }) propertyMeta.config.jsonValue.type({ data, dataSource })
)?.name; )?.name;
return name != null return name != null
? { ? {
@@ -6,7 +6,6 @@ import { computed, type ReadonlySignal } from '@preact/signals-core';
import type { GroupBy, GroupProperty } from '../common/types.js'; import type { GroupBy, GroupProperty } from '../common/types.js';
import type { TypeInstance } from '../logical/type.js'; import type { TypeInstance } from '../logical/type.js';
import type { DVJSON } from '../property/types.js';
import { createTraitKey } from '../traits/key.js'; import { createTraitKey } from '../traits/key.js';
import type { Property } from '../view-manager/property.js'; import type { Property } from '../view-manager/property.js';
import type { SingleView } from '../view-manager/single-view.js'; import type { SingleView } from '../view-manager/single-view.js';
@@ -19,7 +18,7 @@ export type GroupData = {
key: string; key: string;
name: string; name: string;
type: TypeInstance; type: TypeInstance;
value: DVJSON; value: unknown;
rows: string[]; rows: string[];
}; };
@@ -235,7 +234,7 @@ export class GroupTrait {
} }
const remove = this.config$.value?.removeFromGroup ?? (() => null); const remove = this.config$.value?.removeFromGroup ?? (() => null);
const group = fromGroupKey != null ? groupMap[fromGroupKey] : undefined; const group = fromGroupKey != null ? groupMap[fromGroupKey] : undefined;
let newValue: DVJSON = null; let newValue: unknown = null;
if (group) { if (group) {
newValue = remove( newValue = remove(
group.value, group.value,
@@ -284,7 +283,7 @@ export class GroupTrait {
this.view.cellValueSet(rowId, propertyId, newValue); this.view.cellValueSet(rowId, propertyId, newValue);
} }
updateValue(rows: string[], value: DVJSON) { updateValue(rows: string[], value: unknown) {
const propertyId = this.propertyId; const propertyId = this.propertyId;
if (!propertyId) { if (!propertyId) {
return; return;
@@ -1,34 +1,35 @@
import type { UniComponent } from '@blocksuite/affine-shared/types'; import type { UniComponent } from '@blocksuite/affine-shared/types';
import type { TypeInstance } from '../logical/type.js'; import type { TypeInstance } from '../logical/type.js';
import type { DVJSON } from '../property/types.js';
export interface GroupRenderProps< export interface GroupRenderProps<
Data extends NonNullable<unknown> = NonNullable<unknown>, Data extends NonNullable<unknown> = NonNullable<unknown>,
Value = DVJSON, JsonValue = unknown,
> { > {
data: Data; data: Data;
updateData?: (data: Data) => void; updateData?: (data: Data) => void;
value: Value; value: JsonValue;
updateValue?: (value: Value) => void; updateValue?: (value: JsonValue) => void;
readonly: boolean; readonly: boolean;
} }
export type GroupByConfig = { export type GroupByConfig<
JsonValue = unknown,
Data extends NonNullable<unknown> = NonNullable<unknown>,
> = {
name: string; name: string;
groupName: (type: TypeInstance, value: unknown) => string; groupName: (type: TypeInstance, value: unknown) => string;
defaultKeys: (type: TypeInstance) => { defaultKeys: (type: TypeInstance) => {
key: string; key: string;
value: DVJSON; value: JsonValue;
}[]; }[];
valuesGroup: ( valuesGroup: (
value: unknown, value: unknown,
type: TypeInstance type: TypeInstance
) => { ) => {
key: string; key: string;
value: DVJSON; value: JsonValue;
}[]; }[];
addToGroup?: (value: DVJSON, oldValue: DVJSON) => DVJSON; addToGroup?: (value: JsonValue, oldValue: JsonValue) => JsonValue;
removeFromGroup?: (value: DVJSON, oldValue: DVJSON) => DVJSON; removeFromGroup?: (value: JsonValue, oldValue: JsonValue) => JsonValue;
view: UniComponent<GroupRenderProps>; view: UniComponent<GroupRenderProps<Data, JsonValue>>;
}; };
@@ -11,7 +11,6 @@ export const SelectTagSchema = Zod.object({
id: Zod.string(), id: Zod.string(),
color: Zod.string(), color: Zod.string(),
value: Zod.string(), value: Zod.string(),
parentId: Zod.string().optional(),
}); });
export const unknown = defineDataType('Unknown', zod.never(), zod.unknown()); export const unknown = defineDataType('Unknown', zod.never(), zod.unknown());
export const dt = { export const dt = {
@@ -8,18 +8,19 @@ import type { Cell } from '../view-manager/cell.js';
import type { CellRenderProps, DataViewCellLifeCycle } from './manager.js'; import type { CellRenderProps, DataViewCellLifeCycle } from './manager.js';
export abstract class BaseCellRenderer< export abstract class BaseCellRenderer<
Value, RawValue = unknown,
JsonValue = unknown,
Data extends Record<string, unknown> = Record<string, unknown>, Data extends Record<string, unknown> = Record<string, unknown>,
> >
extends SignalWatcher(WithDisposable(ShadowlessElement)) extends SignalWatcher(WithDisposable(ShadowlessElement))
implements DataViewCellLifeCycle, CellRenderProps<Data, Value> implements DataViewCellLifeCycle, CellRenderProps<Data, RawValue, JsonValue>
{ {
get expose() { get expose() {
return this; return this;
} }
@property({ attribute: false }) @property({ attribute: false })
accessor cell!: Cell<Value, Data>; accessor cell!: Cell<RawValue, JsonValue, Data>;
readonly$ = computed(() => { readonly$ = computed(() => {
return this.cell.property.readonly$.value; return this.cell.property.readonly$.value;
@@ -101,11 +102,11 @@ export abstract class BaseCellRenderer<
this.requestUpdate(); this.requestUpdate();
} }
valueSetImmediate(value: Value | undefined): void { valueSetImmediate(value: RawValue | undefined): void {
this.cell.valueSet(value); this.cell.valueSet(value);
} }
valueSetNextTick(value: Value | undefined) { valueSetNextTick(value: RawValue | undefined) {
requestAnimationFrame(() => { requestAnimationFrame(() => {
this.cell.valueSet(value); this.cell.valueSet(value);
}); });
@@ -1,7 +1,7 @@
import type { PropertyModel } from './property-config.js'; import type { PropertyModel } from './property-config.js';
import type { import type {
GetCellDataFromConfig,
GetPropertyDataFromConfig, GetPropertyDataFromConfig,
GetRawValueFromConfig,
} from './types.js'; } from './types.js';
export type ConvertFunction< export type ConvertFunction<
@@ -9,14 +9,14 @@ export type ConvertFunction<
To extends PropertyModel = PropertyModel, To extends PropertyModel = PropertyModel,
> = ( > = (
property: GetPropertyDataFromConfig<From['config']>, property: GetPropertyDataFromConfig<From['config']>,
cells: (GetCellDataFromConfig<From['config']> | undefined)[] cells: (GetRawValueFromConfig<From['config']> | undefined)[]
) => { ) => {
property: GetPropertyDataFromConfig<To['config']>; property: GetPropertyDataFromConfig<To['config']>;
cells: (GetCellDataFromConfig<To['config']> | undefined)[]; cells: (GetRawValueFromConfig<To['config']> | undefined)[];
}; };
export const createPropertyConvert = < export const createPropertyConvert = <
From extends PropertyModel<any, any, any>, From extends PropertyModel<any, any, any, any>,
To extends PropertyModel<any, any, any>, To extends PropertyModel<any, any, any, any>,
>( >(
from: From, from: From,
to: To, to: To,
@@ -5,9 +5,10 @@ import type { Cell } from '../view-manager/cell.js';
export interface CellRenderProps< export interface CellRenderProps<
Data extends NonNullable<unknown> = NonNullable<unknown>, Data extends NonNullable<unknown> = NonNullable<unknown>,
Value = unknown, RawValue = unknown,
JsonValue = unknown,
> { > {
cell: Cell<Value, Data>; cell: Cell<RawValue, JsonValue, Data>;
isEditing$: ReadonlySignal<boolean>; isEditing$: ReadonlySignal<boolean>;
selectCurrentCell: (editing: boolean) => void; selectCurrentCell: (editing: boolean) => void;
} }
@@ -27,12 +28,17 @@ export interface DataViewCellLifeCycle {
export type DataViewCellComponent< export type DataViewCellComponent<
Data extends NonNullable<unknown> = NonNullable<unknown>, Data extends NonNullable<unknown> = NonNullable<unknown>,
Value = unknown, RawValue = unknown,
> = UniComponent<CellRenderProps<Data, Value>, DataViewCellLifeCycle>; JsonValue = unknown,
> = UniComponent<
CellRenderProps<Data, RawValue, JsonValue>,
DataViewCellLifeCycle
>;
export type CellRenderer< export type CellRenderer<
Data extends NonNullable<unknown> = NonNullable<unknown>, Data extends NonNullable<unknown> = NonNullable<unknown>,
Value = unknown, RawValue = unknown,
JsonValue = unknown,
> = { > = {
view: DataViewCellComponent<Data, Value>; view: DataViewCellComponent<Data, RawValue, JsonValue>;
}; };
@@ -4,20 +4,22 @@ import type { PropertyConfig } from './types.js';
export type PropertyMetaConfig< export type PropertyMetaConfig<
Type extends string = string, Type extends string = string,
PropertyData extends NonNullable<unknown> = NonNullable<unknown>, PropertyData extends NonNullable<unknown> = NonNullable<unknown>,
CellData = unknown, RawValue = unknown,
JsonValue = unknown,
> = { > = {
type: Type; type: Type;
config: PropertyConfig<PropertyData, CellData>; config: PropertyConfig<PropertyData, RawValue, JsonValue>;
create: Create<PropertyData>; create: Create<PropertyData>;
renderer: Renderer<PropertyData, CellData>; renderer: Renderer<PropertyData, RawValue, JsonValue>;
}; };
type CreatePropertyMeta< type CreatePropertyMeta<
Type extends string = string, Type extends string = string,
PropertyData extends Record<string, unknown> = Record<string, never>, PropertyData extends Record<string, unknown> = Record<string, never>,
CellData = unknown, RawValue = unknown,
JsonValue = unknown,
> = ( > = (
renderer: Omit<Renderer<PropertyData, CellData>, 'type'> renderer: Omit<Renderer<PropertyData, RawValue, JsonValue>, 'type'>
) => PropertyMetaConfig<Type, PropertyData, CellData>; ) => PropertyMetaConfig<Type, PropertyData, RawValue, JsonValue>;
type Create< type Create<
PropertyData extends Record<string, unknown> = Record<string, never>, PropertyData extends Record<string, unknown> = Record<string, never>,
> = ( > = (
@@ -32,26 +34,33 @@ type Create<
export type PropertyModel< export type PropertyModel<
Type extends string = string, Type extends string = string,
PropertyData extends Record<string, unknown> = Record<string, unknown>, PropertyData extends Record<string, unknown> = Record<string, unknown>,
CellData = unknown, RawValue = unknown,
JsonValue = unknown,
> = { > = {
type: Type; type: Type;
config: PropertyConfig<PropertyData, CellData>; config: PropertyConfig<PropertyData, RawValue, JsonValue>;
create: Create<PropertyData>; create: Create<PropertyData>;
createPropertyMeta: CreatePropertyMeta<Type, PropertyData, CellData>; createPropertyMeta: CreatePropertyMeta<
Type,
PropertyData,
RawValue,
JsonValue
>;
}; };
export const propertyType = <Type extends string>(type: Type) => ({ export const propertyType = <Type extends string>(type: Type) => ({
type: type, type: type,
modelConfig: < modelConfig: <
CellData,
PropertyData extends Record<string, unknown> = Record<string, never>, PropertyData extends Record<string, unknown> = Record<string, never>,
RawValue = unknown,
JsonValue = unknown,
>( >(
ops: PropertyConfig<PropertyData, CellData> ops: PropertyConfig<PropertyData, RawValue, JsonValue>
): PropertyModel<Type, PropertyData, CellData> => { ): PropertyModel<Type, PropertyData, RawValue, JsonValue> => {
const create: Create<PropertyData> = (name, data) => { const create: Create<PropertyData> = (name, data) => {
return { return {
type, type,
name, name,
data: data ?? ops.defaultData(), data: data ?? ops.propertyData.default(),
}; };
}; };
return { return {
@@ -6,18 +6,20 @@ import type { CellRenderer, DataViewCellComponent } from './manager.js';
export interface Renderer< export interface Renderer<
Data extends NonNullable<unknown> = NonNullable<unknown>, Data extends NonNullable<unknown> = NonNullable<unknown>,
Value = unknown, RawValue = unknown,
JsonValue = unknown,
> { > {
type: string; type: string;
icon?: UniComponent; icon?: UniComponent;
cellRenderer: CellRenderer<Data, Value>; cellRenderer: CellRenderer<Data, RawValue, JsonValue>;
} }
export const createFromBaseCellRenderer = < export const createFromBaseCellRenderer = <
Value, RawValue = unknown,
JsonValue = unknown,
Data extends Record<string, unknown> = Record<string, unknown>, Data extends Record<string, unknown> = Record<string, unknown>,
>( >(
renderer: new () => BaseCellRenderer<Value, Data> renderer: new () => BaseCellRenderer<RawValue, JsonValue, Data>
): DataViewCellComponent => { ): DataViewCellComponent => {
return createUniComponentFromWebComponent(renderer as never) as never; return createUniComponentFromWebComponent(renderer as never) as never;
}; };
@@ -8,86 +8,84 @@ export type WithCommonPropertyConfig<T = {}> = T & {
dataSource: DataSource; dataSource: DataSource;
}; };
export type GetPropertyDataFromConfig<T> = export type GetPropertyDataFromConfig<T> =
T extends PropertyConfig<infer R, any> ? R : never; T extends PropertyConfig<infer R, any, any> ? R : never;
export type GetCellDataFromConfig<T> = export type GetRawValueFromConfig<T> =
T extends PropertyConfig<any, infer R> ? R : never; T extends PropertyConfig<any, infer R, any> ? R : never;
export type PropertyConfig< export type GetJsonValueFromConfig<T> =
Data extends NonNullable<unknown> = NonNullable<unknown>, T extends PropertyConfig<any, any, infer R> ? R : never;
Value = unknown, export type PropertyConfig<Data, RawValue = unknown, JsonValue = unknown> = {
> = {
name: string; name: string;
valueSchema: ZodType<Value>;
hide?: boolean; hide?: boolean;
propertyData: {
schema: ZodType<Data>;
default: () => Data;
};
rawValue: {
schema: ZodType<RawValue>;
default: () => RawValue;
toString: (config: { value: RawValue; data: Data }) => string;
fromString: (
config: WithCommonPropertyConfig<{
value: string;
data: Data;
}>
) => {
value: unknown;
data?: Record<string, unknown>;
};
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<JsonValue>;
type: (
config: WithCommonPropertyConfig<{
data: Data;
}>
) => TypeInstance;
isEmpty: (
config: WithCommonPropertyConfig<{
value: JsonValue;
}>
) => boolean;
};
fixed?: { fixed?: {
defaultData: Data; defaultData: Data;
defaultOrder?: string; defaultOrder?: string;
defaultShow?: boolean; 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; 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<string, unknown>;
};
cellToJson: (
config: WithCommonPropertyConfig<{
value?: Value;
data: Data;
}>
) => DVJSON;
cellFromJson: (
config: WithCommonPropertyConfig<{
value: DVJSON;
data: Data;
}>
) => Value | undefined;
addGroup?: ( addGroup?: (
config: WithCommonPropertyConfig<{ config: WithCommonPropertyConfig<{
text: string; text: string;
oldData: Data; oldData: Data;
}> }>
) => 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 = export type DVJSON =
@@ -0,0 +1,30 @@
import type { DataSource } from '../data-source/base';
import type { PropertyConfig } from './types';
export const fromJson = <Data, RawValue, JsonValue>(
config: PropertyConfig<Data, RawValue, JsonValue>,
{
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,
});
};
@@ -19,16 +19,14 @@ export const anyTypeStatsFunctions: StatisticsConfig[] = [
displayName: 'Values', displayName: 'Values',
type: 'count-values', type: 'count-values',
dataType: t.unknown.instance(), dataType: t.unknown.instance(),
impl: (data, { meta, dataSource }) => { impl: data => {
const values = data const values = data.reduce((acc: number, v) => {
.flatMap(v => { if (Array.isArray(v)) {
if (meta.config.values) { return acc + v.length;
return meta.config.values({ value: v, dataSource }); }
} return acc + (v == null ? 0 : 1);
return v; }, 0);
}) return values.toString();
.filter(v => v != null);
return values.length.toString();
}, },
}), }),
createStatisticConfig({ createStatisticConfig({
@@ -37,13 +35,13 @@ export const anyTypeStatsFunctions: StatisticsConfig[] = [
displayName: 'Unique Values', displayName: 'Unique Values',
type: 'count-unique-values', type: 'count-unique-values',
dataType: t.unknown.instance(), dataType: t.unknown.instance(),
impl: (data, { meta, dataSource }) => { impl: data => {
const values = data const values = data
.flatMap(v => { .flatMap(v => {
if (meta.config.values) { if (Array.isArray(v)) {
return meta.config.values({ value: v, dataSource }); return v;
} }
return v; return [v];
}) })
.filter(v => v != null); .filter(v => v != null);
return new Set(values).size.toString(); return new Set(values).size.toString();
@@ -57,7 +55,7 @@ export const anyTypeStatsFunctions: StatisticsConfig[] = [
dataType: t.unknown.instance(), dataType: t.unknown.instance(),
impl: (data, { meta, dataSource }) => { impl: (data, { meta, dataSource }) => {
const emptyList = data.filter(value => const emptyList = data.filter(value =>
meta.config.isEmpty({ value, dataSource }) meta.config.jsonValue.isEmpty({ value, dataSource })
); );
return emptyList.length.toString(); return emptyList.length.toString();
}, },
@@ -70,7 +68,7 @@ export const anyTypeStatsFunctions: StatisticsConfig[] = [
dataType: t.unknown.instance(), dataType: t.unknown.instance(),
impl: (data, { meta, dataSource }) => { impl: (data, { meta, dataSource }) => {
const notEmptyList = data.filter( const notEmptyList = data.filter(
value => !meta.config.isEmpty({ value, dataSource }) value => !meta.config.jsonValue.isEmpty({ value, dataSource })
); );
return notEmptyList.length.toString(); return notEmptyList.length.toString();
}, },
@@ -84,7 +82,7 @@ export const anyTypeStatsFunctions: StatisticsConfig[] = [
impl: (data, { meta, dataSource }) => { impl: (data, { meta, dataSource }) => {
if (data.length === 0) return ''; if (data.length === 0) return '';
const emptyList = data.filter(value => const emptyList = data.filter(value =>
meta.config.isEmpty({ value, dataSource }) meta.config.jsonValue.isEmpty({ value, dataSource })
); );
return ((emptyList.length / data.length) * 100).toFixed(2) + '%'; return ((emptyList.length / data.length) * 100).toFixed(2) + '%';
}, },
@@ -98,7 +96,7 @@ export const anyTypeStatsFunctions: StatisticsConfig[] = [
impl: (data, { meta, dataSource }) => { impl: (data, { meta, dataSource }) => {
if (data.length === 0) return ''; if (data.length === 0) return '';
const notEmptyList = data.filter( 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) + '%'; return ((notEmptyList.length / data.length) * 100).toFixed(2) + '%';
}, },
@@ -5,26 +5,29 @@ import type { Row } from './row.js';
import type { SingleView } from './single-view.js'; import type { SingleView } from './single-view.js';
export interface Cell< export interface Cell<
Value = unknown, RawValue = unknown,
JsonValue = unknown,
Data extends Record<string, unknown> = Record<string, unknown>, Data extends Record<string, unknown> = Record<string, unknown>,
> { > {
readonly rowId: string; readonly rowId: string;
readonly view: SingleView; readonly view: SingleView;
readonly row: Row; readonly row: Row;
readonly propertyId: string; readonly propertyId: string;
readonly property: Property<Value, Data>; readonly property: Property<RawValue, JsonValue, Data>;
readonly isEmpty$: ReadonlySignal<boolean>; readonly isEmpty$: ReadonlySignal<boolean>;
readonly stringValue$: ReadonlySignal<string>; readonly stringValue$: ReadonlySignal<string>;
readonly jsonValue$: ReadonlySignal<unknown>; readonly jsonValue$: ReadonlySignal<JsonValue>;
readonly value$: ReadonlySignal<Value | undefined>; readonly value$: ReadonlySignal<RawValue | undefined>;
valueSet(value: Value | undefined): void;
valueSet(value: RawValue | undefined): void;
} }
export class CellBase< export class CellBase<
Value = unknown, RawValue = unknown,
JsonValue = unknown,
Data extends Record<string, unknown> = Record<string, unknown>, Data extends Record<string, unknown> = Record<string, unknown>,
> implements Cell<Value, Data> > implements Cell<RawValue, JsonValue, Data>
{ {
meta$ = computed(() => { meta$ = computed(() => {
return this.view.manager.dataSource.propertyMetaGet( return this.view.manager.dataSource.propertyMetaGet(
@@ -36,29 +39,35 @@ export class CellBase<
return this.view.manager.dataSource.cellValueGet( return this.view.manager.dataSource.cellValueGet(
this.rowId, this.rowId,
this.propertyId this.propertyId
) as Value; ) as RawValue;
}); });
isEmpty$: ReadonlySignal<boolean> = computed(() => { isEmpty$: ReadonlySignal<boolean> = computed(() => {
return this.meta$.value.config.isEmpty({ return (
value: this.value$.value, this.meta$.value?.config.jsonValue.isEmpty({
dataSource: this.view.manager.dataSource, value: this.jsonValue$.value,
}); dataSource: this.view.manager.dataSource,
}) ?? true
);
}); });
jsonValue$: ReadonlySignal<unknown> = computed(() => { jsonValue$: ReadonlySignal<JsonValue> = computed(() => {
return this.view.cellJsonValueGet(this.rowId, this.propertyId); return this.view.cellJsonValueGet(this.rowId, this.propertyId) as JsonValue;
}); });
property$ = computed(() => { property$ = computed(() => {
return this.view.propertyGet(this.propertyId) as Property<Value, Data>; return this.view.propertyGet(this.propertyId) as Property<
RawValue,
JsonValue,
Data
>;
}); });
stringValue$: ReadonlySignal<string> = computed(() => { stringValue$: ReadonlySignal<string> = computed(() => {
return this.view.cellStringValueGet(this.rowId, this.propertyId)!; return this.view.cellStringValueGet(this.rowId, this.propertyId)!;
}); });
get property(): Property<Value, Data> { get property(): Property<RawValue, JsonValue, Data> {
return this.property$.value; return this.property$.value;
} }
@@ -72,7 +81,7 @@ export class CellBase<
public rowId: string public rowId: string
) {} ) {}
valueSet(value: unknown | undefined): void { valueSet(value: RawValue | undefined): void {
this.view.manager.dataSource.cellValueChange( this.view.manager.dataSource.cellValueChange(
this.rowId, this.rowId,
this.propertyId, this.propertyId,
@@ -8,7 +8,8 @@ import type { Cell } from './cell.js';
import type { SingleView } from './single-view.js'; import type { SingleView } from './single-view.js';
export interface Property< export interface Property<
Value = unknown, RawValue = unknown,
JsonValue = unknown,
Data extends Record<string, unknown> = Record<string, unknown>, Data extends Record<string, unknown> = Record<string, unknown>,
> { > {
readonly id: string; readonly id: string;
@@ -28,7 +29,7 @@ export interface Property<
readonly duplicate?: () => void; readonly duplicate?: () => void;
get canDuplicate(): boolean; get canDuplicate(): boolean;
cellGet(rowId: string): Cell<Value>; cellGet(rowId: string): Cell<RawValue, JsonValue, Data>;
readonly data$: ReadonlySignal<Data>; readonly data$: ReadonlySignal<Data>;
dataUpdate(updater: PropertyDataUpdater<Data>): void; dataUpdate(updater: PropertyDataUpdater<Data>): void;
@@ -44,17 +45,18 @@ export interface Property<
hideSet(hide: boolean): void; hideSet(hide: boolean): void;
get hideCanSet(): boolean; get hideCanSet(): boolean;
valueGet(rowId: string): Value | undefined; valueGet(rowId: string): RawValue | undefined;
valueSet(rowId: string, value: Value | undefined): void; valueSet(rowId: string, value: RawValue | undefined): void;
stringValueGet(rowId: string): string; stringValueGet(rowId: string): string;
valueSetFromString(rowId: string, value: string): void; valueSetFromString(rowId: string, value: string): void;
} }
export abstract class PropertyBase< export abstract class PropertyBase<
Value = unknown, RawValue = unknown,
JsonValue = unknown,
Data extends Record<string, unknown> = Record<string, unknown>, Data extends Record<string, unknown> = Record<string, unknown>,
> implements Property<Value, Data> > implements Property<RawValue, JsonValue, Data>
{ {
cells$ = computed(() => { cells$ = computed(() => {
return this.view.rows$.value.map(id => this.cellGet(id)); return this.view.rows$.value.map(id => this.cellGet(id));
@@ -141,8 +143,8 @@ export abstract class PropertyBase<
return this.view.propertyCanHide(this.id); return this.view.propertyCanHide(this.id);
} }
cellGet(rowId: string): Cell<Value> { cellGet(rowId: string): Cell<RawValue, JsonValue, Data> {
return this.view.cellGet(rowId, this.id) as Cell<Value>; return this.view.cellGet(rowId, this.id) as Cell<RawValue, JsonValue, Data>;
} }
dataUpdate(updater: PropertyDataUpdater<Data>): void { dataUpdate(updater: PropertyDataUpdater<Data>): void {
@@ -165,11 +167,11 @@ export abstract class PropertyBase<
return this.cellGet(rowId).stringValue$.value; return this.cellGet(rowId).stringValue$.value;
} }
valueGet(rowId: string): Value | undefined { valueGet(rowId: string): RawValue | undefined {
return this.cellGet(rowId).value$.value; 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); return this.cellGet(rowId).valueSet(value);
} }
@@ -181,6 +183,6 @@ export abstract class PropertyBase<
if (data.data) { if (data.data) {
this.dataUpdate(() => data.data as Data); this.dataUpdate(() => data.data as Data);
} }
this.valueSet(rowId, data.value as Value); this.valueSet(rowId, data.value as RawValue);
} }
} }
@@ -4,9 +4,9 @@ import { computed, type ReadonlySignal, signal } from '@preact/signals-core';
import type { DataViewContextKey } from '../data-source/context.js'; import type { DataViewContextKey } from '../data-source/context.js';
import type { Variable } from '../expression/types.js'; import type { Variable } from '../expression/types.js';
import type { DVJSON } from '../index.js';
import type { TypeInstance } from '../logical/type.js'; import type { TypeInstance } from '../logical/type.js';
import type { PropertyMetaConfig } from '../property/property-config.js'; import type { PropertyMetaConfig } from '../property/property-config.js';
import { fromJson } from '../property/utils';
import type { TraitKey } from '../traits/key.js'; import type { TraitKey } from '../traits/key.js';
import type { DatabaseFlags } from '../types.js'; import type { DatabaseFlags } from '../types.js';
import type { DataViewDataType, ViewMeta } from '../view/data-view.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; 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; cellStringValueGet(rowId: string, propertyId: string): string | undefined;
@@ -82,12 +82,17 @@ export interface SingleView {
readonly propertyMetas$: ReadonlySignal<PropertyMetaConfig[]>; readonly propertyMetas$: ReadonlySignal<PropertyMetaConfig[]>;
propertyAdd(toAfterOfProperty: InsertToPosition, type?: string): string; propertyAdd(
toAfterOfProperty: InsertToPosition,
type?: string
): string | undefined;
propertyDelete(propertyId: string): void; propertyDelete(propertyId: string): void;
propertyCanDelete(propertyId: string): boolean; propertyCanDelete(propertyId: string): boolean;
propertyDuplicate(propertyId: string): void; propertyDuplicate(propertyId: string): void;
propertyCanDuplicate(propertyId: string): boolean; propertyCanDuplicate(propertyId: string): boolean;
propertyGet(propertyId: string): Property; propertyGet(propertyId: string): Property;
@@ -105,11 +110,13 @@ export interface SingleView {
propertyTypeGet(propertyId: string): string | undefined; propertyTypeGet(propertyId: string): string | undefined;
propertyTypeSet(propertyId: string, type: string): void; propertyTypeSet(propertyId: string, type: string): void;
propertyTypeCanSet(propertyId: string): boolean; propertyTypeCanSet(propertyId: string): boolean;
propertyHideGet(propertyId: string): boolean; propertyHideGet(propertyId: string): boolean;
propertyHideSet(propertyId: string, hide: boolean): void; propertyHideSet(propertyId: string, hide: boolean): void;
propertyCanHide(propertyId: string): boolean; propertyCanHide(propertyId: string): boolean;
propertyDataGet(propertyId: string): Record<string, unknown>; propertyDataGet(propertyId: string): Record<string, unknown>;
@@ -187,13 +194,16 @@ export abstract class SingleViewBase<
}); });
vars$ = computed(() => { vars$ = computed(() => {
return this.propertiesWithoutFilter$.value.map(id => { return this.propertiesWithoutFilter$.value.flatMap(id => {
const v = this.propertyGet(id); const v = this.propertyGet(id);
const propertyMeta = this.dataSource.propertyMetaGet(v.type$.value); const propertyMeta = this.dataSource.propertyMetaGet(v.type$.value);
if (!propertyMeta) {
return [];
}
return { return {
id: v.id, id: v.id,
name: v.name$.value, name: v.name$.value,
type: propertyMeta.config.type({ type: propertyMeta.config.jsonValue.type({
data: v.data$.value, data: v.data$.value,
dataSource: this.dataSource, dataSource: this.dataSource,
}), }),
@@ -229,15 +239,19 @@ export abstract class SingleViewBase<
public manager: ViewManager, public manager: ViewManager,
public id: string public id: string
) {} ) {}
propertyCanDelete(propertyId: string): boolean { propertyCanDelete(propertyId: string): boolean {
return this.dataSource.propertyCanDelete(propertyId); return this.dataSource.propertyCanDelete(propertyId);
} }
propertyCanDuplicate(propertyId: string): boolean { propertyCanDuplicate(propertyId: string): boolean {
return this.dataSource.propertyCanDuplicate(propertyId); return this.dataSource.propertyCanDuplicate(propertyId);
} }
propertyTypeCanSet(propertyId: string): boolean { propertyTypeCanSet(propertyId: string): boolean {
return this.dataSource.propertyTypeCanSet(propertyId); return this.dataSource.propertyTypeCanSet(propertyId);
} }
propertyCanHide(propertyId: string): boolean { propertyCanHide(propertyId: string): boolean {
return this.propertyTypeGet(propertyId) !== 'title'; return this.propertyTypeGet(propertyId) !== 'title';
} }
@@ -264,33 +278,35 @@ export abstract class SingleViewBase<
return new CellBase(this, propertyId, rowId); return new CellBase(this, propertyId, rowId);
} }
cellJsonValueGet(rowId: string, propertyId: string): DVJSON { cellJsonValueGet(rowId: string, propertyId: string): unknown | null {
const type = this.propertyTypeGet(propertyId); const type = this.propertyTypeGet(propertyId);
if (!type) { if (!type) {
return null; return null;
} }
return this.dataSource.propertyMetaGet(type).config.cellToJson({ return (
value: this.dataSource.cellValueGet(rowId, propertyId), this.dataSource.propertyMetaGet(type)?.config.rawValue.toJson({
data: this.propertyDataGet(propertyId), value: this.dataSource.cellValueGet(rowId, propertyId),
dataSource: this.dataSource, 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); const type = this.propertyTypeGet(propertyId);
if (!type) { if (!type) {
return; return;
} }
const fromJson = this.dataSource.propertyMetaGet(type).config.cellFromJson; const config = this.dataSource.propertyMetaGet(type)?.config;
this.dataSource.cellValueChange( if (!config) {
rowId, return;
propertyId, }
fromJson({ const rawValue = fromJson(config, {
value, value: value,
data: this.propertyDataGet(propertyId), data: this.propertyDataGet(propertyId),
dataSource: this.dataSource, dataSource: this.dataSource,
}) });
); this.dataSource.cellValueChange(rowId, propertyId, rawValue);
} }
cellStringValueGet(rowId: string, propertyId: string): string | undefined { cellStringValueGet(rowId: string, propertyId: string): string | undefined {
@@ -299,7 +315,7 @@ export abstract class SingleViewBase<
return; return;
} }
return ( return (
this.dataSource.propertyMetaGet(type).config.cellToString({ this.dataSource.propertyMetaGet(type)?.config.rawValue.toString({
value: this.dataSource.cellValueGet(rowId, propertyId), value: this.dataSource.cellValueGet(rowId, propertyId),
data: this.propertyDataGet(propertyId), data: this.propertyDataGet(propertyId),
}) ?? '' }) ?? ''
@@ -311,14 +327,7 @@ export abstract class SingleViewBase<
if (!type) { if (!type) {
return; return;
} }
const cellValue = this.dataSource.cellValueGet(rowId, propertyId); return this.dataSource.cellValueGet(rowId, propertyId);
return (
this.dataSource.propertyMetaGet(type).config.formatValue?.({
value: cellValue,
data: this.propertyDataGet(propertyId),
dataSource: this.dataSource,
}) ?? cellValue
);
} }
cellValueSet(rowId: string, propertyId: string, value: unknown): void { 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); const id = this.dataSource.propertyAdd(position, type);
if (!id) {
return;
}
this.propertyMove(id, position); this.propertyMove(id, position);
return id; return id;
} }
@@ -374,7 +386,11 @@ export abstract class SingleViewBase<
if (!type) { if (!type) {
return; 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), data: this.propertyDataGet(propertyId),
dataSource: this.dataSource, dataSource: this.dataSource,
}); });
@@ -402,7 +418,7 @@ export abstract class SingleViewBase<
abstract propertyHideSet(propertyId: string, hide: boolean): void; abstract propertyHideSet(propertyId: string, hide: boolean): void;
propertyIconGet(type: string): UniComponent | undefined { propertyIconGet(type: string): UniComponent | undefined {
return this.dataSource.propertyMetaGet(type).renderer.icon; return this.dataSource.propertyMetaGet(type)?.renderer.icon;
} }
propertyIdGetByIndex(index: number): string | undefined { propertyIdGetByIndex(index: number): string | undefined {
@@ -413,7 +429,7 @@ export abstract class SingleViewBase<
return this.propertyIds$.value.indexOf(propertyId); return this.propertyIds$.value.indexOf(propertyId);
} }
propertyMetaGet(type: string): PropertyMetaConfig { propertyMetaGet(type: string): PropertyMetaConfig | undefined {
return this.dataSource.propertyMetaGet(type); return this.dataSource.propertyMetaGet(type);
} }
@@ -439,13 +455,16 @@ export abstract class SingleViewBase<
if (!type) { if (!type) {
return; return;
} }
return ( const fromString =
this.dataSource.propertyMetaGet(type).config.cellFromString({ this.dataSource.propertyMetaGet(type)?.config.rawValue.fromString;
value: cellData, if (!fromString) {
data: this.propertyDataGet(propertyId), return;
dataSource: this.dataSource, }
}) ?? '' return fromString({
); value: cellData,
data: this.propertyDataGet(propertyId),
dataSource: this.dataSource,
});
} }
propertyPreGet(propertyId: string): Property | undefined { propertyPreGet(propertyId: string): Property | undefined {
+2 -2
View File
@@ -15,7 +15,7 @@ import { AffineLitIcon, UniAnyRender, UniLit } from './core/index.js';
import { AnyRender } from './core/utils/uni-component/render-template.js'; import { AnyRender } from './core/utils/uni-component/render-template.js';
import { CheckboxCell } from './property-presets/checkbox/cell-renderer.js'; import { CheckboxCell } from './property-presets/checkbox/cell-renderer.js';
import { DateCell } from './property-presets/date/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 { MultiSelectCell } from './property-presets/multi-select/cell-renderer.js';
import { NumberCell } from './property-presets/number/cell-renderer.js'; import { NumberCell } from './property-presets/number/cell-renderer.js';
import { ProgressCell } from './property-presets/progress/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('mobile-table-cell', MobileTableCell);
customElements.define('affine-data-view-renderer', DataViewRenderer); customElements.define('affine-data-view-renderer', DataViewRenderer);
customElements.define('any-render', AnyRender); 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('affine-database-date-cell', DateCell);
customElements.define( customElements.define(
'data-view-properties-setting', 'data-view-properties-setting',
@@ -21,15 +21,24 @@ const FALSE_VALUES = new Set([
export const checkboxPropertyModelConfig = checkboxPropertyType.modelConfig({ export const checkboxPropertyModelConfig = checkboxPropertyType.modelConfig({
name: 'Checkbox', name: 'Checkbox',
valueSchema: zod.boolean().optional(), propertyData: {
type: () => t.boolean.instance(), schema: zod.object({}),
defaultData: () => ({}), default: () => ({}),
cellToString: ({ value }) => (value ? 'True' : 'False'), },
cellFromString: ({ value }) => ({ jsonValue: {
value: !FALSE_VALUES.has((value?.trim() ?? '').toLowerCase()), schema: zod.boolean(),
}), isEmpty: () => false,
cellToJson: ({ value }) => value ?? null, type: () => t.boolean.instance(),
cellFromJson: ({ value }) => (typeof value !== 'boolean' ? undefined : value), },
isEmpty: () => false, 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, minWidth: 34,
}); });
@@ -20,7 +20,7 @@ import {
} from './cell-renderer.css.js'; } from './cell-renderer.css.js';
import { datePropertyModelConfig } from './define.js'; import { datePropertyModelConfig } from './define.js';
export class DateCell extends BaseCellRenderer<number> { export class DateCell extends BaseCellRenderer<number, number> {
private _prevPortalAbortController: AbortController | null = null; private _prevPortalAbortController: AbortController | null = null;
private readonly openDatePicker = () => { private readonly openDatePicker = () => {
@@ -7,19 +7,24 @@ import { propertyType } from '../../core/property/property-config.js';
export const datePropertyType = propertyType('date'); export const datePropertyType = propertyType('date');
export const datePropertyModelConfig = datePropertyType.modelConfig({ export const datePropertyModelConfig = datePropertyType.modelConfig({
name: 'Date', name: 'Date',
type: () => t.date.instance(), propertyData: {
valueSchema: zod.number().optional(), schema: zod.object({}),
defaultData: () => ({}), default: () => ({}),
cellToString: ({ value }) => },
value != null ? format(value, 'yyyy-MM-dd') : '', jsonValue: {
cellFromString: ({ value }) => { schema: zod.number().nullable(),
const date = parse(value, 'yyyy-MM-dd', new Date()); isEmpty: () => false,
type: () => t.date.instance(),
return { },
value: +date, 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,
}); });
@@ -5,7 +5,7 @@ import { createFromBaseCellRenderer } from '../../core/property/renderer.js';
import { createIcon } from '../../core/utils/uni-icon.js'; import { createIcon } from '../../core/utils/uni-icon.js';
import { imagePropertyModelConfig } from './define.js'; import { imagePropertyModelConfig } from './define.js';
export class TextCell extends BaseCellRenderer<string> { export class ImageCell extends BaseCellRenderer<string, string> {
static override styles = css` static override styles = css`
affine-database-image-cell { affine-database-image-cell {
width: 100%; width: 100%;
@@ -27,6 +27,6 @@ export class TextCell extends BaseCellRenderer<string> {
export const imagePropertyConfig = imagePropertyModelConfig.createPropertyMeta({ export const imagePropertyConfig = imagePropertyModelConfig.createPropertyMeta({
icon: createIcon('ImageIcon'), icon: createIcon('ImageIcon'),
cellRenderer: { cellRenderer: {
view: createFromBaseCellRenderer(TextCell), view: createFromBaseCellRenderer(ImageCell),
}, },
}); });
@@ -2,21 +2,31 @@ 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({ export const imagePropertyModelConfig = imagePropertyType.modelConfig({
name: 'image', name: 'image',
valueSchema: zod.string().optional(), propertyData: {
hide: true, schema: zod.object({}),
type: () => t.image.instance(), default: () => ({}),
defaultData: () => ({}),
cellToString: ({ value }) => value ?? '',
cellFromString: ({ value }) => {
return {
value: value,
};
}, },
cellToJson: ({ value }) => value ?? null, jsonValue: {
cellFromJson: ({ value }) => (typeof value !== 'string' ? undefined : value), schema: zod.string().nullable(),
isEmpty: ({ value }) => value == null, 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,
}); });
@@ -13,6 +13,7 @@ import { multiSelectStyle } from './cell-renderer.css.js';
import { multiSelectPropertyModelConfig } from './define.js'; import { multiSelectPropertyModelConfig } from './define.js';
export class MultiSelectCell extends BaseCellRenderer< export class MultiSelectCell extends BaseCellRenderer<
string[],
string[], string[],
SelectPropertyData SelectPropertyData
> { > {
@@ -4,40 +4,29 @@ 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 { SelectPropertySchema } 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[] | undefined, SelectPropertyData>( multiSelectPropertyType.modelConfig({
{ name: 'Multi-select',
name: 'Multi-select', propertyData: {
valueSchema: zod.array(zod.string()).optional(), schema: SelectPropertySchema,
type: ({ data }) => t.array.instance(t.tag.instance(data.options)), default: () => ({
defaultData: () => ({
options: [], options: [],
}), }),
addGroup: ({ text, oldData }) => { },
return { jsonValue: {
options: [ schema: zod.array(zod.string()),
...(oldData.options ?? []), isEmpty: ({ value }) => value.length === 0,
{ type: ({ data }) => t.array.instance(t.tag.instance(data.options)),
id: nanoid(), },
value: text, rawValue: {
color: getTagColor(), 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 }) => {
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( const optionMap = Object.fromEntries(
data.options.map(v => [v.value, v]) data.options.map(v => [v.value, v])
); );
@@ -66,11 +55,22 @@ export const multiSelectPropertyModelConfig =
data: data, data: data,
}; };
}, },
cellToJson: ({ value }) => value ?? null, toJson: ({ value }) => value ?? null,
cellFromJson: ({ value }) => fromJson: ({ value }) =>
Array.isArray(value) && value.every(v => typeof v === 'string') Array.isArray(value) && value.every(v => typeof v === 'string')
? value ? value
: undefined, : undefined,
isEmpty: ({ value }) => value == null || value.length === 0, },
} addGroup: ({ text, oldData }) => {
); return {
options: [
...(oldData.options ?? []),
{
id: nanoid(),
value: text,
color: getTagColor(),
},
],
};
},
});
@@ -16,6 +16,7 @@ import {
} from './utils/formatter.js'; } from './utils/formatter.js';
export class NumberCell extends BaseCellRenderer< export class NumberCell extends BaseCellRenderer<
number,
number, number,
NumberPropertyDataType NumberPropertyDataType
> { > {
@@ -2,25 +2,29 @@ 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 { NumberPropertySchema } from './types.js';
export const numberPropertyType = propertyType('number'); export const numberPropertyType = propertyType('number');
export const numberPropertyModelConfig = numberPropertyType.modelConfig< export const numberPropertyModelConfig = numberPropertyType.modelConfig({
number | undefined,
NumberPropertyDataType
>({
name: 'Number', name: 'Number',
valueSchema: zod.number().optional(), propertyData: {
type: () => t.number.instance(), schema: NumberPropertySchema,
defaultData: () => ({ decimal: 0, format: 'number' }), default: () => ({ decimal: 0, format: 'number' }) as const,
cellToString: ({ value }) => value?.toString() ?? '', },
cellFromString: ({ value }) => { jsonValue: {
const num = value ? Number(value) : NaN; schema: zod.number().nullable(),
return { isEmpty: ({ value }) => value == null,
value: isNaN(num) ? null : num, 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,
}); });
@@ -1,6 +1,9 @@
import type { NumberFormat } from './utils/formatter.js'; import zod from 'zod';
export type NumberPropertyDataType = { import { NumberFormatSchema } from './utils/formatter.js';
decimal?: number;
format?: NumberFormat; export const NumberPropertySchema = zod.object({
}; decimal: zod.number().optional(),
format: NumberFormatSchema,
});
export type NumberPropertyDataType = zod.infer<typeof NumberPropertySchema>;
@@ -1,13 +1,16 @@
export type NumberFormat = import zod from 'zod';
| 'number' export const NumberFormatSchema = zod.enum([
| 'numberWithCommas' 'number',
| 'percent' 'numberWithCommas',
| 'currencyYen' 'percent',
| 'currencyINR' 'currencyYen',
| 'currencyCNY' 'currencyINR',
| 'currencyUSD' 'currencyCNY',
| 'currencyEUR' 'currencyUSD',
| 'currencyGBP'; 'currencyEUR',
'currencyGBP',
]);
export type NumberFormat = zod.infer<typeof NumberFormatSchema>;
const currency = (currency: string): Intl.NumberFormatOptions => ({ const currency = (currency: string): Intl.NumberFormatOptions => ({
style: 'currency', style: 'currency',
@@ -23,7 +23,7 @@ const progressColors = {
success: 'var(--affine-success-color)', success: 'var(--affine-success-color)',
}; };
export class ProgressCell extends BaseCellRenderer<number> { export class ProgressCell extends BaseCellRenderer<number, number> {
startDrag = (event: MouseEvent) => { startDrag = (event: MouseEvent) => {
if (!this.isEditing$.value) return; if (!this.isEditing$.value) return;
@@ -6,20 +6,24 @@ export const progressPropertyType = propertyType('progress');
export const progressPropertyModelConfig = progressPropertyType.modelConfig({ export const progressPropertyModelConfig = progressPropertyType.modelConfig({
name: 'Progress', name: 'Progress',
valueSchema: zod.number().optional(), propertyData: {
type: () => t.number.instance(), schema: zod.object({}),
defaultData: () => ({}), default: () => ({}),
cellToString: ({ value }) => value?.toString() ?? '',
cellFromString: ({ value }) => {
const num = value ? Number(value) : NaN;
return {
value: isNaN(num) ? null : num,
};
}, },
cellToJson: ({ value }) => value ?? null, jsonValue: {
cellFromJson: ({ value }) => { schema: zod.number(),
if (typeof value !== 'number') return undefined; isEmpty: () => false,
return value; 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,
}); });
@@ -13,7 +13,11 @@ import {
selectPropertyModelConfig, selectPropertyModelConfig,
} from './define.js'; } from './define.js';
export class SelectCell extends BaseCellRenderer<string, SelectPropertyData> { export class SelectCell extends BaseCellRenderer<
string,
string,
SelectPropertyData
> {
closePopup?: () => void; closePopup?: () => void;
private readonly popTagSelect = () => { private readonly popTagSelect = () => {
this.closePopup = popTagSelect(popupTargetFromElement(this), { this.closePopup = popTagSelect(popupTargetFromElement(this), {
@@ -2,23 +2,66 @@ import { nanoid } from '@blocksuite/store';
import zod from 'zod'; 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, SelectTagSchema, 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 const SelectPropertySchema = zod.object({
export type SelectPropertyData = { options: zod.array(SelectTagSchema),
options: SelectTag[]; });
}; export type SelectPropertyData = zod.infer<typeof SelectPropertySchema>;
export const selectPropertyModelConfig = selectPropertyType.modelConfig< export const selectPropertyModelConfig = selectPropertyType.modelConfig({
string | undefined,
SelectPropertyData
>({
name: 'Select', name: 'Select',
valueSchema: zod.string().optional(), propertyData: {
type: ({ data }) => t.tag.instance(data.options), schema: SelectPropertySchema,
defaultData: () => ({ default: () => ({
options: [], 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 }) => { addGroup: ({ text, oldData }) => {
return { return {
options: [ 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,
}); });
@@ -7,7 +7,7 @@ import { createIcon } from '../../core/utils/uni-icon.js';
import { textInputStyle, textStyle } from './cell-renderer.css.js'; import { textInputStyle, textStyle } from './cell-renderer.css.js';
import { textPropertyModelConfig } from './define.js'; import { textPropertyModelConfig } from './define.js';
export class TextCell extends BaseCellRenderer<string> { export class TextCell extends BaseCellRenderer<string, string> {
@query('input') @query('input')
private accessor _inputEle!: HTMLInputElement; private accessor _inputEle!: HTMLInputElement;
@@ -6,16 +6,24 @@ export const textPropertyType = propertyType('text');
export const textPropertyModelConfig = textPropertyType.modelConfig({ export const textPropertyModelConfig = textPropertyType.modelConfig({
name: 'Plain-Text', name: 'Plain-Text',
valueSchema: zod.string().optional(), propertyData: {
type: () => t.string.instance(), schema: zod.object({}),
defaultData: () => ({}), default: () => ({}),
cellToString: ({ value }) => value ?? '',
cellFromString: ({ value }) => {
return {
value: value,
};
}, },
cellToJson: ({ value }) => value ?? null, jsonValue: {
cellFromJson: ({ value }) => (typeof value !== 'string' ? undefined : value), schema: zod.string(),
isEmpty: ({ value }) => value == null || value.length === 0, 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,
}); });
@@ -14,6 +14,7 @@ import {
groupTraitKey, groupTraitKey,
sortByManually, sortByManually,
} from '../../core/group-by/trait.js'; } from '../../core/group-by/trait.js';
import { fromJson } from '../../core/property/utils';
import { PropertyBase } from '../../core/view-manager/property.js'; import { PropertyBase } from '../../core/view-manager/property.js';
import { SingleViewBase } from '../../core/view-manager/single-view.js'; import { SingleViewBase } from '../../core/view-manager/single-view.js';
import type { KanbanViewData } from './define.js'; import type { KanbanViewData } from './define.js';
@@ -183,14 +184,15 @@ export class KanbanSingleView extends SingleViewBase<KanbanViewData> {
Object.entries(defaultValues).forEach(([propertyId, jsonValue]) => { Object.entries(defaultValues).forEach(([propertyId, jsonValue]) => {
const property = this.propertyGet(propertyId); const property = this.propertyGet(propertyId);
const propertyMeta = this.propertyMetaGet(property.type$.value); const propertyMeta = this.propertyMetaGet(property.type$.value);
if (propertyMeta?.config.cellFromJson) { if (!propertyMeta) {
const value = propertyMeta.config.cellFromJson({ return;
value: jsonValue,
data: property.data$.value,
dataSource: this.dataSource,
});
this.cellValueSet(id, propertyId, value);
} }
const value = fromJson(propertyMeta.config, {
value: jsonValue,
data: property.data$.value,
dataSource: this.dataSource,
});
this.cellValueSet(id, propertyId, value);
}); });
} }
@@ -23,7 +23,6 @@ import { html } from 'lit/static-html.js';
import { inputConfig, typeConfig } from '../../../core/common/property-menu.js'; import { inputConfig, typeConfig } from '../../../core/common/property-menu.js';
import type { Property } from '../../../core/view-manager/property.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 { numberFormats } from '../../../property-presets/number/utils/formats.js';
import { DEFAULT_COLUMN_TITLE_HEIGHT } from '../consts.js'; import { DEFAULT_COLUMN_TITLE_HEIGHT } from '../consts.js';
import type { TableColumn, TableSingleView } from '../table-view-manager.js'; import type { TableColumn, TableSingleView } from '../table-view-manager.js';
@@ -91,12 +90,7 @@ export class MobileTableColumnHeader extends SignalWatcher(
items: [ items: [
numberFormatConfig(this.column), numberFormatConfig(this.column),
...numberFormats.map(format => { ...numberFormats.map(format => {
const data = ( const data = this.column.data$.value;
this.column as Property<
number,
NumberPropertyDataType
>
).data$.value;
return menu.action({ return menu.action({
isSelected: data.format === format.type, isSelected: data.format === format.type,
prefix: html`<span prefix: html`<span
@@ -39,7 +39,6 @@ import {
droppable, droppable,
} from '../../../../core/utils/wc-dnd/dnd-context.js'; } from '../../../../core/utils/wc-dnd/dnd-context.js';
import type { Property } from '../../../../core/view-manager/property.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 { numberFormats } from '../../../../property-presets/number/utils/formats.js';
import { ShowQuickSettingBarContextKey } from '../../../../widget-presets/quick-setting-bar/context.js'; import { ShowQuickSettingBarContextKey } from '../../../../widget-presets/quick-setting-bar/context.js';
import { DEFAULT_COLUMN_TITLE_HEIGHT } from '../../consts.js'; import { DEFAULT_COLUMN_TITLE_HEIGHT } from '../../consts.js';
@@ -222,12 +221,7 @@ export class DatabaseHeaderColumn extends SignalWatcher(
items: [ items: [
numberFormatConfig(this.column), numberFormatConfig(this.column),
...numberFormats.map(format => { ...numberFormats.map(format => {
const data = ( const data = this.column.data$.value;
this.column as Property<
number,
NumberPropertyDataType
>
).data$.value;
return menu.action({ return menu.action({
isSelected: data.format === format.type, isSelected: data.format === format.type,
prefix: html`<span prefix: html`<span
@@ -81,7 +81,7 @@ export class DatabaseColumnStatsCell extends SignalWatcher(
return this.column.valueGet(id); return this.column.valueGet(id);
}); });
} }
return this.column.cells$.value.map(cell => cell.value$.value); return this.column.cells$.value.map(cell => cell.jsonValue$.value);
}); });
groups$ = computed(() => { groups$ = computed(() => {
@@ -14,6 +14,7 @@ import {
groupTraitKey, groupTraitKey,
sortByManually, sortByManually,
} from '../../core/group-by/trait.js'; } from '../../core/group-by/trait.js';
import { fromJson } from '../../core/property/utils';
import { SortManager, sortTraitKey } from '../../core/sort/manager.js'; import { SortManager, sortTraitKey } from '../../core/sort/manager.js';
import { PropertyBase } from '../../core/view-manager/property.js'; import { PropertyBase } from '../../core/view-manager/property.js';
import { import {
@@ -327,8 +328,8 @@ export class TableSingleView extends SingleViewBase<TableViewData> {
Object.entries(defaultValues).forEach(([propertyId, jsonValue]) => { Object.entries(defaultValues).forEach(([propertyId, jsonValue]) => {
const property = this.propertyGet(propertyId); const property = this.propertyGet(propertyId);
const propertyMeta = this.propertyMetaGet(property.type$.value); const propertyMeta = this.propertyMetaGet(property.type$.value);
if (propertyMeta?.config.cellFromJson) { if (propertyMeta) {
const value = propertyMeta.config.cellFromJson({ const value = fromJson(propertyMeta.config, {
value: jsonValue, value: jsonValue,
data: property.data$.value, data: property.data$.value,
dataSource: this.dataSource, dataSource: this.dataSource,
@@ -79,11 +79,13 @@ export const database: InitFn = (collection: Workspace, id: string) => {
{ type: type, text: new Text(`Paragraph type ${type}`) }, { type: type, text: new Text(`Paragraph type ${type}`) },
databaseId databaseId
); );
datasource.cellValueChange( if (richTextId) {
id, datasource.cellValueChange(
richTextId, id,
new Text(`Paragraph type ${type}`) richTextId,
); new Text(`Paragraph type ${type}`)
);
}
}); });
const listTypes: ListType[] = ['numbered', 'bulleted', 'todo', 'toggle']; 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}`) }, { type: type, text: new Text(`List type ${type}`) },
databaseId databaseId
); );
datasource.cellValueChange( if (richTextId) {
id, datasource.cellValueChange(
richTextId, id,
new Text(`List type ${type}`) richTextId,
); new Text(`List type ${type}`)
);
}
}); });
// Add a paragraph after database // Add a paragraph after database
doc.addBlock('affine:paragraph', {}, noteId); doc.addBlock('affine:paragraph', {}, noteId);
@@ -531,8 +531,11 @@ test.describe('readonly mode', () => {
}); });
await cell.click(); await cell.click();
await pressEnter(page); await pressEnter(page);
await waitNextFrame(page, 100);
await type(page, '123'); await type(page, '123');
await waitNextFrame(page, 100);
await pressEnter(page); await pressEnter(page);
await waitNextFrame(page, 100);
await assertDatabaseCellRichTexts(page, { text: '123' }); await assertDatabaseCellRichTexts(page, { text: '123' });
await switchReadonly(page); await switchReadonly(page);
@@ -386,6 +386,9 @@ export async function initKanbanViewState(
}); });
config.columns.forEach(column => { config.columns.forEach(column => {
const columnId = datasource.propertyAdd('end', column.type); const columnId = datasource.propertyAdd('end', column.type);
if (!columnId) {
return;
}
datasource.propertyNameSet(columnId, column.type); datasource.propertyNameSet(columnId, column.type);
rowIds.forEach((rowId, index) => { rowIds.forEach((rowId, index) => {
const value = column.value?.[index]; const value = column.value?.[index];