feat(editor): add created-time and created-by property for database block (#12156)

close: BS-3431

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Added "Created By" property type and cell renderer, displaying creator's avatar and name in database blocks.
  - Introduced "Created Time" property type and cell renderer, showing formatted creation timestamps.

- **Improvements**
  - Enhanced table and Kanban views with improved column and row movement, hiding, and statistics capabilities.
  - Streamlined property and row management with unified object handling and reactive signals for better performance and reliability.
  - Improved avatar display logic to handle removed or unnamed users gracefully.
  - Refactored property and row APIs to consolidate access patterns and support reactive updates.
  - Updated icon retrieval and reactive value access for improved UI responsiveness in database and Kanban cells.
  - Consolidated property and cell access methods to use "get or create" patterns ensuring consistent data availability.
  - Added locking mechanism to stabilize computed signals during locked states.
  - Modularized table and Kanban column and row abstractions for better encapsulation and maintainability.

- **Bug Fixes**
  - Corrected row and column deletion, movement, and selection behaviors across table and Kanban views.
  - Fixed issues with property and row referencing, ensuring consistent handling of identifiers and objects.
  - Removed debugging logs and fixed method calls to align with updated APIs.

- **Style**
  - Updated and simplified table column header styles for a cleaner appearance.

- **Chores**
  - Added `@emotion/css` dependency for styling support.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
zzj3720
2025-05-08 11:35:36 +00:00
parent 7c8b977bf9
commit 6689bd1914
76 changed files with 1168 additions and 1042 deletions

View File

@@ -134,7 +134,7 @@ export class DataViewPropertiesSettingView extends SignalWatcher(
accessor view!: SingleView;
items$ = computed(() => {
return this.view.propertiesWithoutFilter$.value;
return this.view.propertiesRaw$.value.map(property => property.id);
});
renderProperty = (property: Property) => {
@@ -171,8 +171,7 @@ export class DataViewPropertiesSettingView extends SignalWatcher(
const activeIndex = properties.findIndex(id => id === activeId);
const overIndex = properties.findIndex(id => id === over.id);
this.view.propertyMove(
activeId,
this.view.propertyGetOrCreate(activeId).move(
activeIndex > overIndex
? {
before: true,
@@ -198,9 +197,7 @@ export class DataViewPropertiesSettingView extends SignalWatcher(
});
private itemsGroup() {
return this.view.propertiesWithoutFilter$.value.map(id =>
this.view.propertyGet(id)
);
return this.view.propertiesRaw$.value;
}
override connectedCallback() {
@@ -246,14 +243,12 @@ export const popPropertiesSetting = (
text: 'Properties',
onBack: props.onBack,
postfix: () => {
const items = props.view.propertiesWithoutFilter$.value.map(id =>
props.view.propertyGet(id)
);
const isAllShowed = items.every(v => !v.hide$.value);
const items = props.view.propertiesRaw$.value;
const isAllShowed = items.every(property => !property.hide$.value);
const clickChangeAll = () => {
props.view.propertiesWithoutFilter$.value.forEach(id => {
if (props.view.propertyCanHide(id)) {
props.view.propertyHideSet(id, isAllShowed);
items.forEach(property => {
if (property.hideCanSet) {
property.hideSet(isAllShowed);
}
});
};

View File

@@ -58,9 +58,7 @@ export const typeConfig = (property: Property) => {
return menu.action({
isSelected: config.type === property.type$.value,
name: config.config.name,
prefix: renderUniLit(
property.view.propertyIconGet(config.type)
),
prefix: renderUniLit(config.renderer.icon),
select: () => {
if (property.type$.value === config.type) {
return;

View File

@@ -120,7 +120,7 @@ export class RecordDetail extends SignalWatcher(
items: this.view.propertyMetas$.value.map(meta => {
return menu.action({
name: meta.config.name,
prefix: renderUniLit(this.view.propertyIconGet(meta.type)),
prefix: renderUniLit(meta.renderer.icon),
select: () => {
this.view.propertyAdd('end', meta.type);
},
@@ -136,9 +136,7 @@ export class RecordDetail extends SignalWatcher(
accessor view!: SingleView;
properties$ = computed(() => {
return this.view.detailProperties$.value.map(id =>
this.view.propertyGet(id)
);
return this.view.detailProperties$.value;
});
selection = new DetailSelection(this);
@@ -184,16 +182,20 @@ export class RecordDetail extends SignalWatcher(
this.dataset.widgetId = 'affine-detail-widget';
}
row$ = computed(() => {
return this.view.rowGetOrCreate(this.rowId);
});
hasNext() {
return this.view.rowNextGet(this.rowId) != null;
return this.row$.value.next$.value != null;
}
hasPrev() {
return this.view.rowPrevGet(this.rowId) != null;
return this.row$.value.prev$.value != null;
}
nextRow() {
const rowId = this.view.rowNextGet(this.rowId);
const rowId = this.row$.value.next$.value?.rowId;
if (rowId == null) {
return;
}
@@ -202,7 +204,7 @@ export class RecordDetail extends SignalWatcher(
}
prevRow() {
const rowId = this.view.rowPrevGet(this.rowId);
const rowId = this.row$.value.prev$.value?.rowId;
if (rowId == null) {
return;
}

View File

@@ -140,15 +140,16 @@ export class RecordField extends SignalWatcher(
${MoveLeftIcon()}
</div>`,
hide: () =>
properties.findIndex(v => v === this.column.id) === 0,
properties.findIndex(
property => property.id === this.column.id
) === 0,
select: () => {
const index = properties.findIndex(v => v === this.column.id);
const targetId = properties[index - 1];
if (!targetId) {
const prev = this.column.prev$.value;
if (!prev) {
return;
}
this.view.propertyMove(this.column.id, {
id: targetId,
this.column.move({
id: prev.id,
before: true,
});
},
@@ -161,16 +162,17 @@ export class RecordField extends SignalWatcher(
${MoveRightIcon()}
</div>`,
hide: () =>
properties.findIndex(v => v === this.column.id) ===
properties.findIndex(
property => property.id === this.column.id
) ===
properties.length - 1,
select: () => {
const index = properties.findIndex(v => v === this.column.id);
const targetId = properties[index + 1];
if (!targetId) {
const next = this.column.next$.value;
if (!next) {
return;
}
this.view.propertyMove(this.column.id, {
id: targetId,
this.column.move({
id: next.id,
before: false,
});
},
@@ -211,7 +213,7 @@ export class RecordField extends SignalWatcher(
accessor rowId!: string;
cell$ = computed(() => {
return this.column.cellGet(this.rowId);
return this.column.cellGetOrCreate(this.rowId);
});
changeEditing = (editing: boolean) => {

View File

@@ -37,7 +37,11 @@ const GroupTitleMobile = (
value: groupData.value,
data: groupData.property.data$.value,
updateData: groupData.manager.updateData,
updateValue: value => groupData.manager.updateValue(groupData.rows, value),
updateValue: value =>
groupData.manager.updateValue(
groupData.rows.map(row => row.rowId),
value
),
readonly: ops.readonly,
};
@@ -140,7 +144,11 @@ export const GroupTitle = (
value: groupData.value,
data: groupData.property.data$.value,
updateData: groupData.manager.updateData,
updateValue: value => groupData.manager.updateValue(groupData.rows, value),
updateValue: value =>
groupData.manager.updateValue(
groupData.rows.map(row => row.rowId),
value
),
readonly: ops.readonly,
};

View File

@@ -189,22 +189,25 @@ export const selectGroupByProperty = (
},
items: [
menu.group({
items: view.propertiesWithoutFilter$.value
.filter(id => {
if (view.propertyGet(id).type$.value === 'title') {
items: view.propertiesRaw$.value
.filter(property => {
if (property.type$.value === 'title') {
return false;
}
return !!groupByMatcher.match(view.propertyGet(id).dataType$.value);
const dataType = property.dataType$.value;
if (!dataType) {
return false;
}
return !!groupByMatcher.match(dataType);
})
.map<MenuConfig>(id => {
const property = view.propertyGet(id);
.map<MenuConfig>(property => {
return menu.action({
name: property.name$.value,
isSelected: group.property$.value?.id === id,
isSelected: group.property$.value?.id === property.id,
prefix: html` <uni-lit .uni="${property.icon}"></uni-lit>`,
select: () => {
group.changeGroup(id);
ops?.onSelect?.(id);
group.changeGroup(property.id);
ops?.onSelect?.(property.id);
},
});
}),
@@ -254,7 +257,7 @@ export const popGroupSetting = (
if (!type) {
return;
}
const icon = view.propertyIconGet(type);
const icon = groupProperty.icon;
const menuHandler = popMenu(target, {
options: {
title: {

View File

@@ -7,11 +7,12 @@ import { computed, type ReadonlySignal } from '@preact/signals-core';
import type { GroupBy, GroupProperty } from '../common/types.js';
import type { TypeInstance } from '../logical/type.js';
import { createTraitKey } from '../traits/key.js';
import { computedLock } from '../utils/lock.js';
import type { Property } from '../view-manager/property.js';
import type { Row } from '../view-manager/row.js';
import type { SingleView } from '../view-manager/single-view.js';
import { defaultGroupBy } from './default.js';
import { groupByMatcher } from './matcher.js';
export type GroupData = {
manager: GroupTrait;
property: Property;
@@ -19,12 +20,10 @@ export type GroupData = {
name: string;
type: TypeInstance;
value: unknown;
rows: string[];
rows: Row[];
};
export class GroupTrait {
private preDataList: GroupData[] | undefined;
config$ = computed(() => {
const groupBy = this.groupBy$.value;
if (!groupBy) {
@@ -42,7 +41,7 @@ export class GroupTrait {
if (!groupBy) {
return;
}
return this.view.propertyGet(groupBy.columnId);
return this.view.propertyGetOrCreate(groupBy.columnId);
});
staticGroupDataMap$ = computed<
@@ -81,8 +80,9 @@ export class GroupTrait {
const groupMap: Record<string, GroupData> = Object.fromEntries(
Object.entries(staticGroupMap).map(([k, v]) => [k, { ...v, rows: [] }])
);
this.view.rows$.value.forEach(id => {
const value = this.view.cellJsonValueGet(id, groupBy.columnId);
this.view.rows$.value.forEach(row => {
const value = this.view.cellGetOrCreate(row.rowId, groupBy.columnId)
.jsonValue$.value;
const keys = config.valuesGroup(value, tType);
keys.forEach(({ key, value }) => {
if (!groupMap[key]) {
@@ -96,40 +96,36 @@ export class GroupTrait {
type: tType,
};
}
groupMap[key].rows.push(id);
groupMap[key].rows.push(row);
});
});
return groupMap;
});
private readonly _groupsDataList$ = computed(() => {
const groupMap = this.groupDataMap$.value;
if (!groupMap) {
return;
}
const sortedGroup = this.ops.sortGroup(Object.keys(groupMap));
sortedGroup.forEach(key => {
if (!groupMap[key]) return;
groupMap[key].rows = this.ops.sortRow(key, groupMap[key].rows);
});
return (this.preDataList = sortedGroup
.map(key => groupMap[key])
.filter((v): v is GroupData => v != null));
});
groupsDataList$ = computed(() => {
if (this.view.isLocked$.value) {
return this.preDataList;
}
return (this.preDataList = this._groupsDataList$.value);
});
groupsDataList$ = computedLock(
computed(() => {
const groupMap = this.groupDataMap$.value;
if (!groupMap) {
return;
}
const sortedGroup = this.ops.sortGroup(Object.keys(groupMap));
sortedGroup.forEach(key => {
if (!groupMap[key]) return;
groupMap[key].rows = this.ops.sortRow(key, groupMap[key].rows);
});
return sortedGroup
.map(key => groupMap[key])
.filter((v): v is GroupData => v != null);
}),
this.view.isLocked$
);
updateData = (data: NonNullable<unknown>) => {
const propertyId = this.propertyId;
if (!propertyId) {
return;
}
this.view.propertyDataSet(propertyId, data);
this.view.propertyGetOrCreate(propertyId).dataUpdate(() => data);
};
get addGroup() {
@@ -137,7 +133,7 @@ export class GroupTrait {
if (!type) {
return;
}
return this.view.propertyMetaGet(type)?.config.addGroup;
return this.view.manager.dataSource.propertyMetaGet(type)?.config.addGroup;
}
get propertyId() {
@@ -150,7 +146,7 @@ export class GroupTrait {
private readonly ops: {
groupBySet: (groupBy: GroupBy | undefined) => void;
sortGroup: (keys: string[]) => string[];
sortRow: (groupKey: string, rowIds: string[]) => string[];
sortRow: (groupKey: string, rows: Row[]) => Row[];
changeGroupSort: (keys: string[]) => void;
changeRowSort: (
groupKeys: string[],
@@ -169,8 +165,11 @@ export class GroupTrait {
const addTo = this.config$.value?.addToGroup ?? (value => value);
const v = groupMap[key]?.value;
if (v != null) {
const newValue = addTo(v, this.view.cellJsonValueGet(rowId, propertyId));
this.view.cellJsonValueSet(rowId, propertyId, newValue);
const newValue = addTo(
v,
this.view.cellGetOrCreate(rowId, propertyId).jsonValue$.value
);
this.view.cellGetOrCreate(rowId, propertyId).valueSet(newValue);
}
}
@@ -191,8 +190,10 @@ export class GroupTrait {
this.ops.groupBySet(undefined);
return;
}
const column = this.view.propertyGet(columnId);
const propertyMeta = this.view.propertyMetaGet(column.type$.value);
const column = this.view.propertyGetOrCreate(columnId);
const propertyMeta = this.view.manager.dataSource.propertyMetaGet(
column.type$.value
);
if (propertyMeta) {
this.ops.groupBySet(
defaultGroupBy(
@@ -238,15 +239,18 @@ export class GroupTrait {
if (group) {
newValue = remove(
group.value,
this.view.cellJsonValueGet(rowId, propertyId)
this.view.cellGetOrCreate(rowId, propertyId).jsonValue$.value
);
}
const addTo = this.config$.value?.addToGroup ?? (value => value);
newValue = addTo(groupMap[toGroupKey]?.value ?? null, newValue);
this.view.cellJsonValueSet(rowId, propertyId, newValue);
this.view.cellGetOrCreate(rowId, propertyId).jsonValueSet(newValue);
}
const rows = groupMap[toGroupKey]?.rows.filter(id => id !== rowId) ?? [];
const index = insertPositionToIndex(position, rows, id => id);
const rows =
groupMap[toGroupKey]?.rows
.filter(row => row.rowId !== rowId)
.map(row => row.rowId) ?? [];
const index = insertPositionToIndex(position, rows, row => row);
rows.splice(index, 0, rowId);
this.changeCardSort(toGroupKey, rows);
}
@@ -278,9 +282,9 @@ export class GroupTrait {
const remove = this.config$.value?.removeFromGroup ?? (() => undefined);
const newValue = remove(
groupMap[key]?.value ?? null,
this.view.cellJsonValueGet(rowId, propertyId)
this.view.cellGetOrCreate(rowId, propertyId).jsonValue$.value
);
this.view.cellValueSet(rowId, propertyId, newValue);
this.view.cellGetOrCreate(rowId, propertyId).valueSet(newValue);
}
updateValue(rows: string[], value: unknown) {
@@ -288,8 +292,8 @@ export class GroupTrait {
if (!propertyId) {
return;
}
rows.forEach(id => {
this.view.cellJsonValueSet(id, propertyId, value);
rows.forEach(rowId => {
this.view.cellGetOrCreate(rowId, propertyId).jsonValueSet(value);
});
}
}

View File

@@ -76,7 +76,7 @@ export type PropertyConfig<Data, RawValue = unknown, JsonValue = unknown> = {
};
fixed?: {
defaultData: Data;
defaultOrder?: string;
defaultOrder?: 'start' | 'end';
defaultShow?: boolean;
};
minWidth?: number;

View File

@@ -4,7 +4,7 @@ import type { DataTypeOf } from '../logical/data-type.js';
import { t } from '../logical/index.js';
import type { TypeInstance } from '../logical/type.js';
import { typeSystem } from '../logical/type-system.js';
import type { SingleView } from '../view-manager/index.js';
import type { Row, SingleView } from '../view-manager/index.js';
import type { Sort } from './types.js';
export const Compare = {
@@ -16,14 +16,14 @@ const evalRef = (
view: SingleView,
ref: VariableRef
):
| ((row: string) => {
| ((row: Row) => {
value: unknown;
ttype?: TypeInstance;
})
| undefined => {
const ttype = view.propertyDataTypeGet(ref.name);
const ttype = view.propertyGetOrCreate(ref.name).dataType$.value;
return row => ({
value: view.cellJsonValueGet(row, ref.name),
value: view.cellGetOrCreate(row.rowId, ref.name).jsonValue$.value,
ttype,
});
};
@@ -153,7 +153,7 @@ const compare = (type: TypeInstance, a: unknown, b: unknown): CompareType => {
export const evalSort = (
sort: Sort,
view: SingleView
): ((rowA: string, rowB: string) => number) | undefined => {
): ((rowA: Row, rowB: Row) => number) | undefined => {
if (sort.sortBy.length) {
const sortBy = sort.sortBy.map(sort => {
return {

View File

@@ -1,7 +1,7 @@
import { computed, type ReadonlySignal } from '@preact/signals-core';
import { createTraitKey } from '../traits/key.js';
import type { SingleView } from '../view-manager/index.js';
import type { Row, SingleView } from '../view-manager/index.js';
import { evalSort } from './eval.js';
import type { Sort, SortBy } from './types.js';
@@ -16,7 +16,7 @@ export class SortManager {
});
};
sort = (rows: string[]) => {
sort = (rows: Row[]) => {
if (!this.sort$.value) {
return rows;
}

View File

@@ -0,0 +1,15 @@
import { computed, type ReadonlySignal } from '@preact/signals-core';
export const computedLock = <T>(
value$: ReadonlySignal<T>,
lock$: ReadonlySignal<boolean>
): ReadonlySignal<T> => {
let previousValue: T;
return computed(() => {
if (lock$.value) {
return previousValue ?? value$.value;
}
previousValue = value$.value;
return previousValue;
});
};

View File

@@ -1,5 +1,6 @@
import { computed, type ReadonlySignal } from '@preact/signals-core';
import { fromJson } from '../property/utils.js';
import type { Property } from './property.js';
import type { Row } from './row.js';
import type { SingleView } from './single-view.js';
@@ -9,18 +10,19 @@ export interface Cell<
JsonValue = unknown,
Data extends Record<string, unknown> = Record<string, unknown>,
> {
readonly rowId: string;
readonly view: SingleView;
readonly rowId: string;
readonly row: Row;
readonly propertyId: string;
readonly property: Property<RawValue, JsonValue, Data>;
readonly isEmpty$: ReadonlySignal<boolean>;
readonly stringValue$: ReadonlySignal<string>;
readonly jsonValue$: ReadonlySignal<JsonValue>;
readonly isEmpty$: ReadonlySignal<boolean>;
readonly value$: ReadonlySignal<RawValue | undefined>;
readonly jsonValue$: ReadonlySignal<JsonValue | undefined>;
readonly stringValue$: ReadonlySignal<string | undefined>;
valueSet(value: RawValue | undefined): void;
jsonValueSet(value: JsonValue | undefined): void;
}
export class CellBase<
@@ -29,10 +31,12 @@ export class CellBase<
Data extends Record<string, unknown> = Record<string, unknown>,
> implements Cell<RawValue, JsonValue, Data>
{
get dataSource() {
return this.view.manager.dataSource;
}
meta$ = computed(() => {
return this.view.manager.dataSource.propertyMetaGet(
this.property.type$.value
);
return this.dataSource.propertyMetaGet(this.property.type$.value);
});
value$ = computed(() => {
@@ -51,20 +55,37 @@ export class CellBase<
);
});
jsonValue$: ReadonlySignal<JsonValue> = computed(() => {
return this.view.cellJsonValueGet(this.rowId, this.propertyId) as JsonValue;
jsonValue$: ReadonlySignal<JsonValue | undefined> = computed(() => {
const toJson = this.property.meta$.value?.config.rawValue.toJson;
if (!toJson) {
return undefined;
}
return (
(toJson({
value: this.value$.value,
data: this.property.data$.value,
dataSource: this.dataSource,
}) as JsonValue) ?? undefined
);
});
property$ = computed(() => {
return this.view.propertyGet(this.propertyId) as Property<
return this.view.propertyGetOrCreate(this.propertyId) as Property<
RawValue,
JsonValue,
Data
>;
});
stringValue$: ReadonlySignal<string> = computed(() => {
return this.view.cellStringValueGet(this.rowId, this.propertyId)!;
stringValue$: ReadonlySignal<string | undefined> = computed(() => {
const toString = this.property.meta$.value?.config.rawValue.toString;
if (!toString) {
return;
}
return toString({
value: this.value$.value,
data: this.property.data$.value,
});
});
get property(): Property<RawValue, JsonValue, Data> {
@@ -72,7 +93,7 @@ export class CellBase<
}
get row(): Row {
return this.view.rowGet(this.rowId);
return this.view.rowGetOrCreate(this.rowId);
}
constructor(
@@ -88,4 +109,17 @@ export class CellBase<
value
);
}
jsonValueSet(value: JsonValue | undefined): void {
const meta = this.property.meta$.value;
if (!meta) {
return;
}
const rawValue = fromJson(meta.config, {
value: value,
data: this.property.data$.value,
dataSource: this.view.manager.dataSource,
});
this.dataSource.cellValueChange(this.rowId, this.propertyId, rawValue);
}
}

View File

@@ -1,8 +1,9 @@
import type { UniComponent } from '@blocksuite/affine-shared/types';
import type { InsertToPosition } from '@blocksuite/affine-shared/utils';
import { computed, type ReadonlySignal } from '@preact/signals-core';
import type { TypeInstance } from '../logical/type.js';
import type { CellRenderer } from '../property/index.js';
import type { CellRenderer, PropertyMetaConfig } from '../property/index.js';
import type { PropertyDataUpdater } from '../types.js';
import type { Cell } from './cell.js';
import type { SingleView } from './single-view.js';
@@ -13,14 +14,17 @@ export interface Property<
Data extends Record<string, unknown> = Record<string, unknown>,
> {
readonly id: string;
readonly index: number;
readonly index$: ReadonlySignal<number | undefined>;
readonly view: SingleView;
readonly isFirst: boolean;
readonly isLast: boolean;
readonly isFirst$: ReadonlySignal<boolean>;
readonly isLast$: ReadonlySignal<boolean>;
readonly next$: ReadonlySignal<Property | undefined>;
readonly prev$: ReadonlySignal<Property | undefined>;
readonly readonly$: ReadonlySignal<boolean>;
readonly renderer$: ReadonlySignal<CellRenderer | undefined>;
readonly cells$: ReadonlySignal<Cell[]>;
readonly dataType$: ReadonlySignal<TypeInstance>;
readonly dataType$: ReadonlySignal<TypeInstance | undefined>;
readonly meta$: ReadonlySignal<PropertyMetaConfig | undefined>;
readonly icon?: UniComponent;
readonly delete?: () => void;
@@ -29,7 +33,7 @@ export interface Property<
readonly duplicate?: () => void;
get canDuplicate(): boolean;
cellGet(rowId: string): Cell<RawValue, JsonValue, Data>;
cellGetOrCreate(rowId: string): Cell<RawValue, JsonValue, Data>;
readonly data$: ReadonlySignal<Data>;
dataUpdate(updater: PropertyDataUpdater<Data>): void;
@@ -48,8 +52,16 @@ export interface Property<
valueGet(rowId: string): RawValue | undefined;
valueSet(rowId: string, value: RawValue | undefined): void;
stringValueGet(rowId: string): string;
stringValueGet(rowId: string): string | undefined;
valueSetFromString(rowId: string, value: string): void;
parseValueFromString(value: string):
| {
value: unknown;
data?: Record<string, unknown>;
}
| undefined;
move(position: InsertToPosition): void;
}
export abstract class PropertyBase<
@@ -58,125 +70,195 @@ export abstract class PropertyBase<
Data extends Record<string, unknown> = Record<string, unknown>,
> implements Property<RawValue, JsonValue, Data>
{
meta$ = computed(() => {
return this.dataSource.propertyMetaGet(this.type$.value);
});
cells$ = computed(() => {
return this.view.rows$.value.map(id => this.cellGet(id));
return this.view.rows$.value.map(row =>
this.view.cellGetOrCreate(row.rowId, this.id)
);
});
data$ = computed(() => {
return this.view.propertyDataGet(this.id) as Data;
return this.dataSource.propertyDataGet(this.id) as Data;
});
dataType$ = computed(() => {
return this.view.propertyDataTypeGet(this.id)!;
const type = this.type$.value;
if (!type) {
return;
}
const meta = this.dataSource.propertyMetaGet(type);
if (!meta) {
return;
}
return meta.config.jsonValue.type({
data: this.data$.value,
dataSource: this.dataSource,
});
});
hide$ = computed(() => {
return this.view.propertyHideGet(this.id);
});
abstract hide$: ReadonlySignal<boolean>;
name$ = computed(() => {
return this.view.propertyNameGet(this.id);
return this.dataSource.propertyNameGet(this.id);
});
readonly$ = computed(() => {
return this.view.readonly$.value || this.view.propertyReadonlyGet(this.id);
return (
this.view.readonly$.value || this.dataSource.propertyReadonlyGet(this.id)
);
});
type$ = computed(() => {
return this.view.propertyTypeGet(this.id)!;
return this.dataSource.propertyTypeGet(this.id)!;
});
renderer$ = computed(() => {
return this.view.propertyMetaGet(this.type$.value)?.renderer.cellRenderer;
return this.meta$.value?.renderer.cellRenderer;
});
get delete(): (() => void) | undefined {
return () => this.view.propertyDelete(this.id);
return () => this.dataSource.propertyDelete(this.id);
}
get duplicate(): (() => void) | undefined {
return () => this.view.propertyDuplicate(this.id);
return () => {
const id = this.dataSource.propertyDuplicate(this.id);
if (!id) {
return;
}
const property = this.view.propertyGetOrCreate(id);
property.move({
before: false,
id: this.id,
});
};
}
abstract move(position: InsertToPosition): void;
get icon(): UniComponent | undefined {
if (!this.type$.value) return undefined;
return this.view.propertyIconGet(this.type$.value);
return this.dataSource.propertyMetaGet(this.type$.value)?.renderer.icon;
}
get id(): string {
return this.propertyId;
}
get index(): number {
return this.view.propertyIndexGet(this.id);
}
index$ = computed(() => {
const index = this.view.propertyIds$.value.indexOf(this.id);
return index >= 0 ? index : undefined;
});
get isFirst(): boolean {
return this.view.propertyIndexGet(this.id) === 0;
}
isFirst$ = computed(() => {
return this.index$.value === 0;
});
get isLast(): boolean {
return (
this.view.propertyIndexGet(this.id) ===
this.view.properties$.value.length - 1
);
}
isLast$ = computed(() => {
return this.index$.value === this.view.propertyIds$.value.length - 1;
});
next$ = computed(() => {
const properties = this.view.properties$.value;
if (this.index$.value == null) {
return;
}
return properties[this.index$.value + 1];
});
prev$ = computed(() => {
const properties = this.view.properties$.value;
if (this.index$.value == null) {
return;
}
return properties[this.index$.value - 1];
});
get typeSet(): undefined | ((type: string) => void) {
return type => this.view.propertyTypeSet(this.id, type);
return type => this.dataSource.propertyTypeSet(this.id, type);
}
constructor(
public view: SingleView,
public propertyId: string
) {}
protected get dataSource() {
return this.view.manager.dataSource;
}
get canDelete(): boolean {
return this.view.propertyCanDelete(this.id);
return this.dataSource.propertyCanDelete(this.id);
}
get canDuplicate(): boolean {
return this.view.propertyCanDuplicate(this.id);
return this.dataSource.propertyCanDuplicate(this.id);
}
get typeCanSet(): boolean {
return this.view.propertyTypeCanSet(this.id);
return this.dataSource.propertyTypeCanSet(this.id);
}
get hideCanSet(): boolean {
return this.view.propertyCanHide(this.id);
return this.type$.value !== 'title';
}
cellGet(rowId: string): Cell<RawValue, JsonValue, Data> {
return this.view.cellGet(rowId, this.id) as Cell<RawValue, JsonValue, Data>;
cellGetOrCreate(rowId: string): Cell<RawValue, JsonValue, Data> {
return this.view.cellGetOrCreate(rowId, this.id) as Cell<
RawValue,
JsonValue,
Data
>;
}
dataUpdate(updater: PropertyDataUpdater<Data>): void {
const data = this.data$.value;
this.view.propertyDataSet(this.id, {
this.dataSource.propertyDataSet(this.id, {
...data,
...updater(data),
});
}
hideSet(hide: boolean): void {
this.view.propertyHideSet(this.id, hide);
}
abstract hideSet(hide: boolean): void;
nameSet(name: string): void {
this.view.propertyNameSet(this.id, name);
this.dataSource.propertyNameSet(this.id, name);
}
stringValueGet(rowId: string): string {
return this.cellGet(rowId).stringValue$.value;
stringValueGet(rowId: string): string | undefined {
return this.cellGetOrCreate(rowId).stringValue$.value;
}
valueGet(rowId: string): RawValue | undefined {
return this.cellGet(rowId).value$.value;
return this.cellGetOrCreate(rowId).value$.value;
}
valueSet(rowId: string, value: RawValue | undefined): void {
return this.cellGet(rowId).valueSet(value);
return this.cellGetOrCreate(rowId).valueSet(value);
}
parseValueFromString(value: string):
| {
value: unknown;
data?: Record<string, unknown>;
}
| undefined {
const type = this.type$.value;
if (!type) {
return;
}
const fromString =
this.dataSource.propertyMetaGet(type)?.config.rawValue.fromString;
if (!fromString) {
return;
}
return fromString({
value,
data: this.data$.value,
dataSource: this.dataSource,
});
}
valueSetFromString(rowId: string, value: string): void {
const data = this.view.propertyParseValueFromString(this.id, value);
const data = this.parseValueFromString(value);
if (!data) {
return;
}

View File

@@ -1,3 +1,4 @@
import type { InsertToPosition } from '@blocksuite/affine-shared/utils';
import { computed, type ReadonlySignal } from '@preact/signals-core';
import { type Cell, CellBase } from './cell.js';
@@ -6,17 +7,59 @@ import type { SingleView } from './single-view.js';
export interface Row {
readonly cells$: ReadonlySignal<Cell[]>;
readonly rowId: string;
index$: ReadonlySignal<number | undefined>;
prev$: ReadonlySignal<Row | undefined>;
next$: ReadonlySignal<Row | undefined>;
delete(): void;
move(position: InsertToPosition): void;
}
export class RowBase implements Row {
cells$ = computed(() => {
return this.singleView.propertyIds$.value.map(propertyId => {
return new CellBase(this.singleView, propertyId, this.rowId);
return this.singleView.propertiesRaw$.value.map(property => {
return new CellBase(this.singleView, property.id, this.rowId);
});
});
index$ = computed(() => {
const idx = this.singleView.rowIds$.value.indexOf(this.rowId);
return idx >= 0 ? idx : undefined;
});
prev$ = computed(() => {
const index = this.index$.value;
if (index == null) {
return;
}
return this.singleView.rows$.value[index - 1];
});
next$ = computed(() => {
const index = this.index$.value;
if (index == null) {
return;
}
return this.singleView.rows$.value[index + 1];
});
constructor(
readonly singleView: SingleView,
readonly rowId: string
) {}
get dataSource() {
return this.singleView.manager.dataSource;
}
delete(): void {
this.dataSource.rowDelete([this.rowId]);
}
move(position: InsertToPosition): void {
this.dataSource.rowMove(this.rowId, position);
}
}

View File

@@ -1,14 +1,12 @@
import type { UniComponent } from '@blocksuite/affine-shared/types';
import type { InsertToPosition } from '@blocksuite/affine-shared/utils';
import { computed, type ReadonlySignal, signal } from '@preact/signals-core';
import type { DataViewContextKey } from '../data-source/context.js';
import type { Variable } from '../expression/types.js';
import type { TypeInstance } from '../logical/type.js';
import type { PropertyMetaConfig } from '../property/property-config.js';
import { fromJson } from '../property/utils';
import type { TraitKey } from '../traits/key.js';
import type { DatabaseFlags } from '../types.js';
import { computedLock } from '../utils/lock.js';
import type { DataViewDataType, ViewMeta } from '../view/data-view.js';
import { type Cell, CellBase } from './cell.js';
import type { Property } from './property.js';
@@ -36,49 +34,25 @@ export interface SingleView {
nameSet(name: string): void;
readonly propertyIds$: ReadonlySignal<string[]>;
readonly propertiesWithoutFilter$: ReadonlySignal<string[]>;
readonly propertiesRaw$: ReadonlySignal<Property[]>;
readonly propertyMap$: ReadonlySignal<Record<string, Property>>;
readonly properties$: ReadonlySignal<Property[]>;
readonly detailProperties$: ReadonlySignal<string[]>;
readonly rows$: ReadonlySignal<string[]>;
readonly propertyIds$: ReadonlySignal<string[]>;
readonly detailProperties$: ReadonlySignal<Property[]>;
readonly rowsRaw$: ReadonlySignal<Row[]>;
readonly rows$: ReadonlySignal<Row[]>;
readonly rowIds$: ReadonlySignal<string[]>;
readonly vars$: ReadonlySignal<Variable[]>;
readonly featureFlags$: ReadonlySignal<DatabaseFlags>;
cellValueGet(rowId: string, propertyId: string): unknown;
cellValueSet(rowId: string, propertyId: string, value: unknown): void;
cellJsonValueGet(rowId: string, propertyId: string): unknown | null;
cellJsonValueSet(rowId: string, propertyId: string, value: unknown): void;
cellStringValueGet(rowId: string, propertyId: string): string | undefined;
cellGet(rowId: string, propertyId: string): Cell;
propertyParseValueFromString(
propertyId: string,
value: string
):
| {
value: unknown;
data?: Record<string, unknown>;
}
| undefined;
propertyGetOrCreate(propertyId: string): Property;
rowGetOrCreate(rowId: string): Row;
cellGetOrCreate(rowId: string, propertyId: string): Cell;
rowAdd(insertPosition: InsertToPosition): string;
rowDelete(ids: string[]): void;
rowMove(rowId: string, position: InsertToPosition): void;
rowGet(rowId: string): Row;
rowPrevGet(rowId: string): string | undefined;
rowNextGet(rowId: string): string | undefined;
rowsDelete(rows: string[]): void;
readonly propertyMetas$: ReadonlySignal<PropertyMetaConfig[]>;
@@ -87,54 +61,6 @@ export interface SingleView {
type?: string
): string | undefined;
propertyDelete(propertyId: string): void;
propertyCanDelete(propertyId: string): boolean;
propertyDuplicate(propertyId: string): void;
propertyCanDuplicate(propertyId: string): boolean;
propertyGet(propertyId: string): Property;
propertyMetaGet(type: string): PropertyMetaConfig | undefined;
propertyPreGet(propertyId: string): Property | undefined;
propertyNextGet(propertyId: string): Property | undefined;
propertyNameGet(propertyId: string): string;
propertyNameSet(propertyId: string, name: string): void;
propertyTypeGet(propertyId: string): string | undefined;
propertyTypeSet(propertyId: string, type: string): void;
propertyTypeCanSet(propertyId: string): boolean;
propertyHideGet(propertyId: string): boolean;
propertyHideSet(propertyId: string, hide: boolean): void;
propertyCanHide(propertyId: string): boolean;
propertyDataGet(propertyId: string): Record<string, unknown>;
propertyDataSet(propertyId: string, data: Record<string, unknown>): void;
propertyDataTypeGet(propertyId: string): TypeInstance | undefined;
propertyIndexGet(propertyId: string): number;
propertyIdGetByIndex(index: number): string | undefined;
propertyReadonlyGet(propertyId: string): boolean;
propertyMove(propertyId: string, position: InsertToPosition): void;
propertyIconGet(type: string): UniComponent | undefined;
contextGet<T>(key: DataViewContextKey<T>): T;
traitGet<T>(key: TraitKey<T>): T | undefined;
@@ -158,7 +84,7 @@ export abstract class SingleViewBase<
return this.dataSource.viewDataGet(this.id) as ViewData | undefined;
});
abstract detailProperties$: ReadonlySignal<string[]>;
abstract detailProperties$: ReadonlySignal<Property[]>;
protected lockRows$ = signal(false);
@@ -172,43 +98,56 @@ export abstract class SingleViewBase<
return this.data$.value?.name ?? '';
});
preRows: string[] = [];
abstract propertyIds$: ReadonlySignal<string[]>;
properties$ = computed(() => {
return this.propertyIds$.value.map(
id => this.propertyGet(id) as ReturnType<this['propertyGet']>
);
propertyIds$: ReadonlySignal<string[]> = computed(() => {
return this.properties$.value.map(v => v.id);
});
abstract propertiesWithoutFilter$: ReadonlySignal<string[]>;
propertyMap$: ReadonlySignal<Record<string, Property>> = computed(() => {
return Object.fromEntries(this.properties$.value.map(v => [v.id, v]));
});
abstract properties$: ReadonlySignal<Property[]>;
abstract propertiesRaw$: ReadonlySignal<Property[]>;
abstract readonly$: ReadonlySignal<boolean>;
rows$ = computed(() => {
if (this.lockRows$.value) {
return this.preRows;
}
return (this.preRows = this.rowsMapping(this.dataSource.rows$.value));
rowsRaw$ = computed(() => {
return this.dataSource.rows$.value.map(id => this.rowGetOrCreate(id));
});
rows$ = computedLock(
computed(() => {
return this.rowsMapping(this.rowsRaw$.value);
}),
this.isLocked$
);
rowsDelete(rows: string[]): void {
this.dataSource.rowDelete(rows);
}
rowIds$ = computed(() => {
return this.rowsRaw$.value.map(v => v.rowId);
});
vars$ = computed(() => {
return this.propertiesWithoutFilter$.value.flatMap(id => {
const v = this.propertyGet(id);
const propertyMeta = this.dataSource.propertyMetaGet(v.type$.value);
return this.propertiesRaw$.value.flatMap(property => {
const propertyMeta = this.dataSource.propertyMetaGet(
property.type$.value
);
if (!propertyMeta) {
return [];
}
return {
id: v.id,
name: v.name$.value,
id: property.id,
name: property.name$.value,
type: propertyMeta.config.jsonValue.type({
data: v.data$.value,
data: property.data$.value,
dataSource: this.dataSource,
}),
icon: v.icon,
propertyType: v.type$.value,
icon: property.icon,
propertyType: property.type$.value,
};
});
});
@@ -240,29 +179,13 @@ export abstract class SingleViewBase<
public id: string
) {}
propertyCanDelete(propertyId: string): boolean {
return this.dataSource.propertyCanDelete(propertyId);
}
propertyCanDuplicate(propertyId: string): boolean {
return this.dataSource.propertyCanDuplicate(propertyId);
}
propertyTypeCanSet(propertyId: string): boolean {
return this.dataSource.propertyTypeCanSet(propertyId);
}
propertyCanHide(propertyId: string): boolean {
return this.propertyTypeGet(propertyId) !== 'title';
}
private searchRowsMapping(rows: string[], searchString: string): string[] {
return rows.filter(id => {
private searchRowsMapping(rows: Row[], searchString: string): Row[] {
return rows.filter(row => {
if (searchString) {
const containsSearchString = this.propertyIds$.value.some(
propertyId => {
return this.cellStringValueGet(id, propertyId)
?.toLowerCase()
return this.cellGetOrCreate(row.rowId, propertyId)
.stringValue$.value?.toLowerCase()
.includes(searchString?.toLowerCase());
}
);
@@ -270,66 +193,14 @@ export abstract class SingleViewBase<
return false;
}
}
return this.isShow(id);
return this.isShow(row.rowId);
});
}
cellGet(rowId: string, propertyId: string): Cell {
cellGetOrCreate(rowId: string, propertyId: string): Cell {
return new CellBase(this, propertyId, rowId);
}
cellJsonValueGet(rowId: string, propertyId: string): unknown | null {
const type = this.propertyTypeGet(propertyId);
if (!type) {
return null;
}
return (
this.dataSource.propertyMetaGet(type)?.config.rawValue.toJson({
value: this.dataSource.cellValueGet(rowId, propertyId),
data: this.propertyDataGet(propertyId),
dataSource: this.dataSource,
}) ?? null
);
}
cellJsonValueSet(rowId: string, propertyId: string, value: unknown): void {
const type = this.propertyTypeGet(propertyId);
if (!type) {
return;
}
const config = this.dataSource.propertyMetaGet(type)?.config;
if (!config) {
return;
}
const rawValue = fromJson(config, {
value: value,
data: this.propertyDataGet(propertyId),
dataSource: this.dataSource,
});
this.dataSource.cellValueChange(rowId, propertyId, rawValue);
}
cellStringValueGet(rowId: string, propertyId: string): string | undefined {
const type = this.propertyTypeGet(propertyId);
if (!type) {
return;
}
return (
this.dataSource.propertyMetaGet(type)?.config.rawValue.toString({
value: this.dataSource.cellValueGet(rowId, propertyId),
data: this.propertyDataGet(propertyId),
}) ?? ''
);
}
cellValueGet(rowId: string, propertyId: string): unknown {
return this.dataSource.cellValueGet(rowId, propertyId);
}
cellValueSet(rowId: string, propertyId: string, value: unknown): void {
this.dataSource.cellValueChange(rowId, propertyId, value);
}
contextGet<T>(key: DataViewContextKey<T>): T {
return this.dataSource.contextGet(key);
}
@@ -365,144 +236,22 @@ export abstract class SingleViewBase<
if (!id) {
return;
}
this.propertyMove(id, position);
const property = this.propertyGetOrCreate(id);
property.move(position);
return id;
}
propertyDataGet(propertyId: string): Record<string, unknown> {
return this.dataSource.propertyDataGet(propertyId);
}
propertyDataSet(propertyId: string, data: Record<string, unknown>): void {
this.dataSource.propertyDataSet(propertyId, data);
}
propertyDataTypeGet(propertyId: string): TypeInstance | undefined {
const type = this.propertyTypeGet(propertyId);
if (!type) {
return;
}
const meta = this.dataSource.propertyMetaGet(type);
if (!meta) {
return;
}
return meta.config.jsonValue.type({
data: this.propertyDataGet(propertyId),
dataSource: this.dataSource,
});
}
propertyDelete(propertyId: string): void {
this.dataSource.propertyDelete(propertyId);
}
propertyDuplicate(propertyId: string): void {
const id = this.dataSource.propertyDuplicate(propertyId);
if (!id) {
return;
}
this.propertyMove(id, {
before: false,
id: propertyId,
});
}
abstract propertyGet(propertyId: string): Property;
abstract propertyHideGet(propertyId: string): boolean;
abstract propertyHideSet(propertyId: string, hide: boolean): void;
propertyIconGet(type: string): UniComponent | undefined {
return this.dataSource.propertyMetaGet(type)?.renderer.icon;
}
propertyIdGetByIndex(index: number): string | undefined {
return this.propertyIds$.value[index];
}
propertyIndexGet(propertyId: string): number {
return this.propertyIds$.value.indexOf(propertyId);
}
propertyMetaGet(type: string): PropertyMetaConfig | undefined {
return this.dataSource.propertyMetaGet(type);
}
abstract propertyMove(propertyId: string, position: InsertToPosition): void;
propertyNameGet(propertyId: string): string {
return this.dataSource.propertyNameGet(propertyId);
}
propertyNameSet(propertyId: string, name: string): void {
this.dataSource.propertyNameSet(propertyId, name);
}
propertyNextGet(propertyId: string): Property | undefined {
const index = this.propertyIndexGet(propertyId);
const nextId = this.propertyIdGetByIndex(index + 1);
if (!nextId) return;
return this.propertyGet(nextId);
}
propertyParseValueFromString(propertyId: string, cellData: string) {
const type = this.propertyTypeGet(propertyId);
if (!type) {
return;
}
const fromString =
this.dataSource.propertyMetaGet(type)?.config.rawValue.fromString;
if (!fromString) {
return;
}
return fromString({
value: cellData,
data: this.propertyDataGet(propertyId),
dataSource: this.dataSource,
});
}
propertyPreGet(propertyId: string): Property | undefined {
const index = this.propertyIndexGet(propertyId);
const prevId = this.propertyIdGetByIndex(index - 1);
if (!prevId) return;
return this.propertyGet(prevId);
}
propertyReadonlyGet(propertyId: string): boolean {
return this.dataSource.propertyReadonlyGet(propertyId);
}
propertyTypeGet(propertyId: string): string | undefined {
return this.dataSource.propertyTypeGet(propertyId);
}
propertyTypeSet(propertyId: string, type: string): void {
this.dataSource.propertyTypeSet(propertyId, type);
}
abstract propertyGetOrCreate(propertyId: string): Property;
rowAdd(insertPosition: InsertToPosition | number): string {
return this.dataSource.rowAdd(insertPosition);
}
rowDelete(ids: string[]): void {
this.dataSource.rowDelete(ids);
}
rowGet(rowId: string): Row {
rowGetOrCreate(rowId: string): Row {
return new RowBase(this, rowId);
}
rowMove(rowId: string, position: InsertToPosition): void {
this.dataSource.rowMove(rowId, position);
}
abstract rowNextGet(rowId: string): string | undefined;
abstract rowPrevGet(rowId: string): string | undefined;
protected rowsMapping(rows: string[]): string[] {
protected rowsMapping(rows: Row[]): Row[] {
return this.searchRowsMapping(rows, this.searchString.value);
}