From 6689bd19145b4b23cf8a721d0c16e046ad40aff8 Mon Sep 17 00:00:00 2001 From: zzj3720 <17165520+zzj3720@users.noreply.github.com> Date: Thu, 8 May 2025 11:35:36 +0000 Subject: [PATCH] feat(editor): add created-time and created-by property for database block (#12156) close: BS-3431 ## 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. --- .../affine/blocks/database/package.json | 1 + .../affine/blocks/database/src/data-source.ts | 64 ++- .../src/detail-panel/block-renderer.ts | 2 +- .../affine/blocks/database/src/effects.ts | 2 + .../properties/created-time/cell-renderer.ts | 55 +++ .../src/properties/created-time/define.ts | 35 ++ .../blocks/database/src/properties/index.ts | 2 + .../src/properties/rich-text/cell-renderer.ts | 1 - .../database/src/properties/title/text.ts | 3 +- .../data-view/src/core/common/properties.ts | 21 +- .../src/core/common/property-menu.ts | 4 +- .../data-view/src/core/detail/detail.ts | 18 +- .../affine/data-view/src/core/detail/field.ts | 28 +- .../src/core/group-by/group-title.ts | 12 +- .../data-view/src/core/group-by/setting.ts | 23 +- .../data-view/src/core/group-by/trait.ts | 92 ++--- .../data-view/src/core/property/types.ts | 2 +- .../affine/data-view/src/core/sort/eval.ts | 10 +- .../affine/data-view/src/core/sort/manager.ts | 4 +- .../affine/data-view/src/core/utils/lock.ts | 15 + .../data-view/src/core/view-manager/cell.ts | 60 ++- .../src/core/view-manager/property.ts | 180 ++++++--- .../data-view/src/core/view-manager/row.ts | 47 ++- .../src/core/view-manager/single-view.ts | 369 +++--------------- .../src/view-presets/kanban/define.ts | 1 - .../kanban/kanban-view-manager.ts | 142 +++---- .../src/view-presets/kanban/mobile/card.ts | 2 +- .../src/view-presets/kanban/mobile/cell.ts | 2 +- .../src/view-presets/kanban/mobile/group.ts | 14 +- .../src/view-presets/kanban/mobile/menu.ts | 2 +- .../src/view-presets/kanban/pc/card.ts | 2 +- .../src/view-presets/kanban/pc/cell.ts | 2 +- .../kanban/pc/controller/selection.ts | 7 +- .../src/view-presets/kanban/pc/group.ts | 14 +- .../src/view-presets/table/mobile/cell.ts | 6 +- .../table/mobile/column-header.ts | 28 +- .../src/view-presets/table/mobile/group.ts | 19 +- .../src/view-presets/table/mobile/menu.ts | 2 +- .../table/pc-virtual/controller/clipboard.ts | 18 +- .../pc-virtual/controller/drag-to-fill.ts | 2 +- .../table/pc-virtual/controller/hotkeys.ts | 11 +- .../table/pc-virtual/controller/selection.ts | 4 +- .../group/bottom/stats/column-stats-column.ts | 10 +- .../pc-virtual/group/top/group-header.ts | 6 +- .../table/pc-virtual/group/top/group-title.ts | 6 +- .../group/top/header/column-header.css.ts | 130 +----- .../group/top/header/column-header.ts | 6 - .../group/top/header/column-move-preview.ts | 9 +- .../group/top/header/single-column-header.ts | 32 +- .../group/top/header/vertical-indicator.ts | 4 +- .../view-presets/table/pc-virtual/row/cell.ts | 2 +- .../view-presets/table/pc-virtual/row/menu.ts | 2 +- .../table/pc-virtual/table-view.ts | 15 +- .../src/view-presets/table/pc/cell.ts | 6 +- .../table/pc/controller/clipboard.ts | 18 +- .../table/pc/controller/drag-to-fill.ts | 4 +- .../view-presets/table/pc/controller/drag.ts | 24 +- .../table/pc/controller/hotkeys.ts | 11 +- .../table/pc/controller/selection.ts | 8 +- .../src/view-presets/table/pc/effect.ts | 4 +- .../src/view-presets/table/pc/group.ts | 23 +- .../table/pc/header/column-renderer.ts | 12 +- .../table/pc/header/database-header-column.ts | 35 +- .../table/pc/header/vertical-indicator.ts | 4 +- .../src/view-presets/table/pc/menu.ts | 2 +- .../src/view-presets/table/pc/row/row.ts | 6 +- .../src/view-presets/table/pc/table-view.ts | 8 +- .../table/stats/column-stats-column.ts | 10 +- .../view-presets/table/table-view-manager.ts | 296 +++++++------- packages/frontend/core/package.json | 1 + .../properties/created-by/define.ts | 31 ++ .../properties/created-by/view.tsx | 130 ++++++ .../database-block/properties/index.ts | 2 + .../member/multi-member-select/index.tsx | 7 +- .../database-block/properties/member/view.tsx | 1 + yarn.lock | 17 +- 76 files changed, 1168 insertions(+), 1042 deletions(-) create mode 100644 blocksuite/affine/blocks/database/src/properties/created-time/cell-renderer.ts create mode 100644 blocksuite/affine/blocks/database/src/properties/created-time/define.ts create mode 100644 blocksuite/affine/data-view/src/core/utils/lock.ts create mode 100644 packages/frontend/core/src/blocksuite/database-block/properties/created-by/define.ts create mode 100644 packages/frontend/core/src/blocksuite/database-block/properties/created-by/view.tsx diff --git a/blocksuite/affine/blocks/database/package.json b/blocksuite/affine/blocks/database/package.json index 44b01a5d87..949e32591f 100644 --- a/blocksuite/affine/blocks/database/package.json +++ b/blocksuite/affine/blocks/database/package.json @@ -24,6 +24,7 @@ "@blocksuite/icons": "^2.2.12", "@blocksuite/std": "workspace:*", "@blocksuite/store": "workspace:*", + "@emotion/css": "^11.13.5", "@floating-ui/dom": "^1.6.13", "@lit/context": "^1.1.2", "@preact/signals-core": "^1.8.0", diff --git a/blocksuite/affine/blocks/database/src/data-source.ts b/blocksuite/affine/blocks/database/src/data-source.ts index 2e14c1d646..4d8830f81a 100644 --- a/blocksuite/affine/blocks/database/src/data-source.ts +++ b/blocksuite/affine/blocks/database/src/data-source.ts @@ -2,6 +2,7 @@ import type { ColumnDataType, ColumnUpdater, DatabaseBlockModel, + ParagraphBlockModel, } from '@blocksuite/affine-model'; import { getSelectedModelsCommand } from '@blocksuite/affine-shared/commands'; import { FeatureFlagService } from '@blocksuite/affine-shared/services'; @@ -51,7 +52,59 @@ import { databaseBlockViews, } from './views/index.js'; +type SpacialProperty = { + valueSet: (rowId: string, propertyId: string, value: unknown) => void; + valueGet: (rowId: string, propertyId: string) => unknown; +}; export class DatabaseBlockDataSource extends DataSourceBase { + spacialProperties: Record = { + 'created-time': { + valueSet: () => {}, + valueGet: (rowId: string) => { + const model = this.getModelById(rowId) as ParagraphBlockModel; + if (!model) { + return null; + } + return model.props['meta:createdAt']; + }, + }, + 'created-by': { + valueSet: () => {}, + valueGet: (rowId: string) => { + const model = this.getModelById(rowId) as + | ParagraphBlockModel + | undefined; + return model ? model.props['meta:createdBy'] : null; + }, + }, + type: { + valueSet: () => {}, + valueGet: (rowId: string) => { + const model = this.getModelById(rowId); + if (!model) { + return; + } + return getIcon(model); + }, + }, + title: { + valueSet: () => {}, + valueGet: (rowId: string) => { + const model = this.getModelById(rowId); + if (!model) { + return; + } + return model.text; + }, + }, + }; + isSpacialProperty(propertyId: string): boolean { + return this.spacialProperties[propertyId] !== undefined; + } + spacialValueGet(rowId: string, propertyId: string): unknown { + return this.spacialProperties[propertyId]?.valueGet(rowId, propertyId); + } + static externalProperties = signal([]); static propertiesList = computed(() => { return [ @@ -196,20 +249,15 @@ export class DatabaseBlockDataSource extends DataSourceBase { } cellValueGet(rowId: string, propertyId: string): unknown { - if (propertyId === 'type') { - const model = this.getModelById(rowId); - if (!model) { - return; - } - return getIcon(model); + if (this.isSpacialProperty(propertyId)) { + return this.spacialValueGet(rowId, propertyId); } const type = this.propertyTypeGet(propertyId); if (!type) { return; } if (type === 'title') { - const model = this.getModelById(rowId); - return model?.text; + return this.spacialValueGet(rowId, 'title'); } const meta = this.propertyMetaGet(type); if (!meta) { diff --git a/blocksuite/affine/blocks/database/src/detail-panel/block-renderer.ts b/blocksuite/affine/blocks/database/src/detail-panel/block-renderer.ts index 0e670db504..613200af87 100644 --- a/blocksuite/affine/blocks/database/src/detail-panel/block-renderer.ts +++ b/blocksuite/affine/blocks/database/src/detail-panel/block-renderer.ts @@ -140,7 +140,7 @@ export class BlockRenderer return; } return html`
- ${this.view.cellValueGet(this.rowId, iconColumn)} + ${this.view.cellGetOrCreate(this.rowId, iconColumn).value$.value}
`; } diff --git a/blocksuite/affine/blocks/database/src/effects.ts b/blocksuite/affine/blocks/database/src/effects.ts index dd3541c0f0..bc71b4d062 100644 --- a/blocksuite/affine/blocks/database/src/effects.ts +++ b/blocksuite/affine/blocks/database/src/effects.ts @@ -4,6 +4,7 @@ import { DatabaseBlockComponent } from './database-block'; import { DatabaseDndPreviewBlockComponent } from './database-dnd-preview-block'; import { BlockRenderer } from './detail-panel/block-renderer'; import { NoteRenderer } from './detail-panel/note-renderer'; +import { CreatedTimeCell } from './properties/created-time/cell-renderer'; import { LinkCell } from './properties/link/cell-renderer'; import { RichTextCell } from './properties/rich-text/cell-renderer'; import { IconCell } from './properties/title/icon'; @@ -15,6 +16,7 @@ export function effects() { customElements.define('affine-database-link-cell', LinkCell); customElements.define('data-view-header-area-text', HeaderAreaTextCell); customElements.define('affine-database-rich-text-cell', RichTextCell); + customElements.define('affine-database-created-time-cell', CreatedTimeCell); customElements.define('center-peek', CenterPeek); customElements.define('database-datasource-note-renderer', NoteRenderer); customElements.define('database-datasource-block-renderer', BlockRenderer); diff --git a/blocksuite/affine/blocks/database/src/properties/created-time/cell-renderer.ts b/blocksuite/affine/blocks/database/src/properties/created-time/cell-renderer.ts new file mode 100644 index 0000000000..42d60dd577 --- /dev/null +++ b/blocksuite/affine/blocks/database/src/properties/created-time/cell-renderer.ts @@ -0,0 +1,55 @@ +import { + BaseCellRenderer, + createFromBaseCellRenderer, + createIcon, +} from '@blocksuite/data-view'; +import { css } from '@emotion/css'; +import { format } from 'date-fns/format'; +import { html } from 'lit'; + +import { createdTimePropertyModelConfig } from './define.js'; +const createdTimeCellStyle = css({ + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + width: '100%', + height: '100%', +}); + +const textStyle = css({ + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + width: '100%', + height: '100%', +}); + +export class CreatedTimeCell extends BaseCellRenderer { + renderContent() { + const formattedDate = this.value + ? format(this.value, 'yyyy-MM-dd HH:mm:ss') + : ''; + return html`
${formattedDate}
`; + } + + override connectedCallback(): void { + super.connectedCallback(); + this.classList.add(createdTimeCellStyle); + } + + override beforeEnterEditMode() { + return false; + } + + override render() { + return html`
${this.renderContent()}
`; + } +} + +export const createdTimeColumnConfig = + createdTimePropertyModelConfig.createPropertyMeta({ + icon: createIcon('DateTimeIcon'), + cellRenderer: { + view: createFromBaseCellRenderer(CreatedTimeCell), + }, + }); diff --git a/blocksuite/affine/blocks/database/src/properties/created-time/define.ts b/blocksuite/affine/blocks/database/src/properties/created-time/define.ts new file mode 100644 index 0000000000..dafc264ad7 --- /dev/null +++ b/blocksuite/affine/blocks/database/src/properties/created-time/define.ts @@ -0,0 +1,35 @@ +import { propertyType, t } from '@blocksuite/data-view'; +import { format } from 'date-fns/format'; +import zod from 'zod'; + +export const createdTimeColumnType = propertyType('created-time'); +export const createdTimePropertyModelConfig = createdTimeColumnType.modelConfig( + { + name: 'Created Time', + propertyData: { + schema: zod.object({}), + default: () => ({}), + }, + jsonValue: { + schema: zod.number().nullable(), + isEmpty: () => false, + type: () => t.date.instance(), + }, + rawValue: { + schema: zod.number().nullable(), + default: () => null, + toString: ({ value }) => + value != null ? format(value, 'yyyy-MM-dd HH:mm:ss') : '', + fromString: () => { + return { value: null }; + }, + toJson: ({ value }) => value, + fromJson: ({ value }) => value, + }, + fixed: { + defaultData: {}, + defaultShow: false, + defaultOrder: 'end', + }, + } +); diff --git a/blocksuite/affine/blocks/database/src/properties/index.ts b/blocksuite/affine/blocks/database/src/properties/index.ts index 16be391c2d..e63c23d0a4 100644 --- a/blocksuite/affine/blocks/database/src/properties/index.ts +++ b/blocksuite/affine/blocks/database/src/properties/index.ts @@ -1,5 +1,6 @@ import { propertyPresets } from '@blocksuite/data-view/property-presets'; +import { createdTimeColumnConfig } from './created-time/cell-renderer.js'; import { linkColumnConfig } from './link/cell-renderer.js'; import { richTextColumnConfig } from './rich-text/cell-renderer.js'; import { titleColumnConfig } from './title/cell-renderer.js'; @@ -24,4 +25,5 @@ export const databaseBlockProperties = { linkColumnConfig, richTextColumnConfig, titleColumnConfig, + createdTimeColumnConfig, }; diff --git a/blocksuite/affine/blocks/database/src/properties/rich-text/cell-renderer.ts b/blocksuite/affine/blocks/database/src/properties/rich-text/cell-renderer.ts index 0d2b30ced3..4351f386a0 100644 --- a/blocksuite/affine/blocks/database/src/properties/rich-text/cell-renderer.ts +++ b/blocksuite/affine/blocks/database/src/properties/rich-text/cell-renderer.ts @@ -310,7 +310,6 @@ export class RichTextCell extends BaseCellRenderer { }); } } else { - console.log(text); inlineEditor.insertText(inlineRange, text); inlineEditor.setInlineRange({ index: inlineRange.index + text.length, diff --git a/blocksuite/affine/blocks/database/src/properties/title/text.ts b/blocksuite/affine/blocks/database/src/properties/title/text.ts index 8c582b1b23..edea276e5f 100644 --- a/blocksuite/affine/blocks/database/src/properties/title/text.ts +++ b/blocksuite/affine/blocks/database/src/properties/title/text.ts @@ -271,7 +271,8 @@ export class HeaderAreaTextCell extends BaseCellRenderer { const iconColumn = this.view.mainProperties$.value.iconColumn; if (!iconColumn) return; - const icon = this.view.cellValueGet(this.cell.rowId, iconColumn) as string; + const icon = this.view.cellGetOrCreate(this.cell.rowId, iconColumn).value$ + .value; if (!icon) return; return icon; }); diff --git a/blocksuite/affine/data-view/src/core/common/properties.ts b/blocksuite/affine/data-view/src/core/common/properties.ts index ff061754aa..65ce3f1746 100644 --- a/blocksuite/affine/data-view/src/core/common/properties.ts +++ b/blocksuite/affine/data-view/src/core/common/properties.ts @@ -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); } }); }; diff --git a/blocksuite/affine/data-view/src/core/common/property-menu.ts b/blocksuite/affine/data-view/src/core/common/property-menu.ts index 2ed0ed7ea4..16ba75048b 100644 --- a/blocksuite/affine/data-view/src/core/common/property-menu.ts +++ b/blocksuite/affine/data-view/src/core/common/property-menu.ts @@ -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; diff --git a/blocksuite/affine/data-view/src/core/detail/detail.ts b/blocksuite/affine/data-view/src/core/detail/detail.ts index 31d5847a02..1947cc367e 100644 --- a/blocksuite/affine/data-view/src/core/detail/detail.ts +++ b/blocksuite/affine/data-view/src/core/detail/detail.ts @@ -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; } diff --git a/blocksuite/affine/data-view/src/core/detail/field.ts b/blocksuite/affine/data-view/src/core/detail/field.ts index 498aa97822..7c10348d75 100644 --- a/blocksuite/affine/data-view/src/core/detail/field.ts +++ b/blocksuite/affine/data-view/src/core/detail/field.ts @@ -140,15 +140,16 @@ export class RecordField extends SignalWatcher( ${MoveLeftIcon()} `, 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()} `, 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) => { diff --git a/blocksuite/affine/data-view/src/core/group-by/group-title.ts b/blocksuite/affine/data-view/src/core/group-by/group-title.ts index 9add00db08..b4da888d24 100644 --- a/blocksuite/affine/data-view/src/core/group-by/group-title.ts +++ b/blocksuite/affine/data-view/src/core/group-by/group-title.ts @@ -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, }; diff --git a/blocksuite/affine/data-view/src/core/group-by/setting.ts b/blocksuite/affine/data-view/src/core/group-by/setting.ts index 893121524c..951e313a79 100644 --- a/blocksuite/affine/data-view/src/core/group-by/setting.ts +++ b/blocksuite/affine/data-view/src/core/group-by/setting.ts @@ -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(id => { - const property = view.propertyGet(id); + .map(property => { return menu.action({ name: property.name$.value, - isSelected: group.property$.value?.id === id, + isSelected: group.property$.value?.id === property.id, prefix: html` `, 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: { diff --git a/blocksuite/affine/data-view/src/core/group-by/trait.ts b/blocksuite/affine/data-view/src/core/group-by/trait.ts index 1fa35ecd62..43acac0075 100644 --- a/blocksuite/affine/data-view/src/core/group-by/trait.ts +++ b/blocksuite/affine/data-view/src/core/group-by/trait.ts @@ -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 = 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) => { 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); }); } } diff --git a/blocksuite/affine/data-view/src/core/property/types.ts b/blocksuite/affine/data-view/src/core/property/types.ts index 63ca5e52a5..3c4599c125 100644 --- a/blocksuite/affine/data-view/src/core/property/types.ts +++ b/blocksuite/affine/data-view/src/core/property/types.ts @@ -76,7 +76,7 @@ export type PropertyConfig = { }; fixed?: { defaultData: Data; - defaultOrder?: string; + defaultOrder?: 'start' | 'end'; defaultShow?: boolean; }; minWidth?: number; diff --git a/blocksuite/affine/data-view/src/core/sort/eval.ts b/blocksuite/affine/data-view/src/core/sort/eval.ts index a685b77073..71507b6798 100644 --- a/blocksuite/affine/data-view/src/core/sort/eval.ts +++ b/blocksuite/affine/data-view/src/core/sort/eval.ts @@ -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 { diff --git a/blocksuite/affine/data-view/src/core/sort/manager.ts b/blocksuite/affine/data-view/src/core/sort/manager.ts index a89147855d..a7786f1854 100644 --- a/blocksuite/affine/data-view/src/core/sort/manager.ts +++ b/blocksuite/affine/data-view/src/core/sort/manager.ts @@ -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; } diff --git a/blocksuite/affine/data-view/src/core/utils/lock.ts b/blocksuite/affine/data-view/src/core/utils/lock.ts new file mode 100644 index 0000000000..3228ebac45 --- /dev/null +++ b/blocksuite/affine/data-view/src/core/utils/lock.ts @@ -0,0 +1,15 @@ +import { computed, type ReadonlySignal } from '@preact/signals-core'; + +export const computedLock = ( + value$: ReadonlySignal, + lock$: ReadonlySignal +): ReadonlySignal => { + let previousValue: T; + return computed(() => { + if (lock$.value) { + return previousValue ?? value$.value; + } + previousValue = value$.value; + return previousValue; + }); +}; diff --git a/blocksuite/affine/data-view/src/core/view-manager/cell.ts b/blocksuite/affine/data-view/src/core/view-manager/cell.ts index beb24bcb32..25b33c1ce4 100644 --- a/blocksuite/affine/data-view/src/core/view-manager/cell.ts +++ b/blocksuite/affine/data-view/src/core/view-manager/cell.ts @@ -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 = Record, > { - readonly rowId: string; readonly view: SingleView; + readonly rowId: string; readonly row: Row; readonly propertyId: string; readonly property: Property; - readonly isEmpty$: ReadonlySignal; - readonly stringValue$: ReadonlySignal; - readonly jsonValue$: ReadonlySignal; + readonly isEmpty$: ReadonlySignal; readonly value$: ReadonlySignal; + readonly jsonValue$: ReadonlySignal; + readonly stringValue$: ReadonlySignal; valueSet(value: RawValue | undefined): void; + jsonValueSet(value: JsonValue | undefined): void; } export class CellBase< @@ -29,10 +31,12 @@ export class CellBase< Data extends Record = Record, > implements Cell { + 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 = computed(() => { - return this.view.cellJsonValueGet(this.rowId, this.propertyId) as JsonValue; + jsonValue$: ReadonlySignal = 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 = computed(() => { - return this.view.cellStringValueGet(this.rowId, this.propertyId)!; + stringValue$: ReadonlySignal = 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 { @@ -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); + } } diff --git a/blocksuite/affine/data-view/src/core/view-manager/property.ts b/blocksuite/affine/data-view/src/core/view-manager/property.ts index 2523ba79d8..f8294f8cc8 100644 --- a/blocksuite/affine/data-view/src/core/view-manager/property.ts +++ b/blocksuite/affine/data-view/src/core/view-manager/property.ts @@ -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 = Record, > { readonly id: string; - readonly index: number; + readonly index$: ReadonlySignal; readonly view: SingleView; - readonly isFirst: boolean; - readonly isLast: boolean; + readonly isFirst$: ReadonlySignal; + readonly isLast$: ReadonlySignal; + readonly next$: ReadonlySignal; + readonly prev$: ReadonlySignal; readonly readonly$: ReadonlySignal; readonly renderer$: ReadonlySignal; readonly cells$: ReadonlySignal; - readonly dataType$: ReadonlySignal; + readonly dataType$: ReadonlySignal; + readonly meta$: ReadonlySignal; readonly icon?: UniComponent; readonly delete?: () => void; @@ -29,7 +33,7 @@ export interface Property< readonly duplicate?: () => void; get canDuplicate(): boolean; - cellGet(rowId: string): Cell; + cellGetOrCreate(rowId: string): Cell; readonly data$: ReadonlySignal; dataUpdate(updater: PropertyDataUpdater): 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; + } + | undefined; + + move(position: InsertToPosition): void; } export abstract class PropertyBase< @@ -58,125 +70,195 @@ export abstract class PropertyBase< Data extends Record = Record, > implements Property { + 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; 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 { - return this.view.cellGet(rowId, this.id) as Cell; + cellGetOrCreate(rowId: string): Cell { + return this.view.cellGetOrCreate(rowId, this.id) as Cell< + RawValue, + JsonValue, + Data + >; } dataUpdate(updater: PropertyDataUpdater): 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; + } + | 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; } diff --git a/blocksuite/affine/data-view/src/core/view-manager/row.ts b/blocksuite/affine/data-view/src/core/view-manager/row.ts index b32de1f9d0..9eb717a9a5 100644 --- a/blocksuite/affine/data-view/src/core/view-manager/row.ts +++ b/blocksuite/affine/data-view/src/core/view-manager/row.ts @@ -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; readonly rowId: string; + + index$: ReadonlySignal; + + prev$: ReadonlySignal; + next$: ReadonlySignal; + + 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); + } } diff --git a/blocksuite/affine/data-view/src/core/view-manager/single-view.ts b/blocksuite/affine/data-view/src/core/view-manager/single-view.ts index c683f67eaa..aa464dd8ae 100644 --- a/blocksuite/affine/data-view/src/core/view-manager/single-view.ts +++ b/blocksuite/affine/data-view/src/core/view-manager/single-view.ts @@ -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; - readonly propertiesWithoutFilter$: ReadonlySignal; + readonly propertiesRaw$: ReadonlySignal; + readonly propertyMap$: ReadonlySignal>; readonly properties$: ReadonlySignal; - readonly detailProperties$: ReadonlySignal; - readonly rows$: ReadonlySignal; + readonly propertyIds$: ReadonlySignal; + readonly detailProperties$: ReadonlySignal; + readonly rowsRaw$: ReadonlySignal; + readonly rows$: ReadonlySignal; + readonly rowIds$: ReadonlySignal; readonly vars$: ReadonlySignal; readonly featureFlags$: ReadonlySignal; - 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; - } - | 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; @@ -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; - - propertyDataSet(propertyId: string, data: Record): 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(key: DataViewContextKey): T; traitGet(key: TraitKey): T | undefined; @@ -158,7 +84,7 @@ export abstract class SingleViewBase< return this.dataSource.viewDataGet(this.id) as ViewData | undefined; }); - abstract detailProperties$: ReadonlySignal; + abstract detailProperties$: ReadonlySignal; protected lockRows$ = signal(false); @@ -172,43 +98,56 @@ export abstract class SingleViewBase< return this.data$.value?.name ?? ''; }); - preRows: string[] = []; - - abstract propertyIds$: ReadonlySignal; - - properties$ = computed(() => { - return this.propertyIds$.value.map( - id => this.propertyGet(id) as ReturnType - ); + propertyIds$: ReadonlySignal = computed(() => { + return this.properties$.value.map(v => v.id); }); - abstract propertiesWithoutFilter$: ReadonlySignal; + propertyMap$: ReadonlySignal> = computed(() => { + return Object.fromEntries(this.properties$.value.map(v => [v.id, v])); + }); + + abstract properties$: ReadonlySignal; + + abstract propertiesRaw$: ReadonlySignal; abstract readonly$: ReadonlySignal; - 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(key: DataViewContextKey): 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 { - return this.dataSource.propertyDataGet(propertyId); - } - - propertyDataSet(propertyId: string, data: Record): 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); } diff --git a/blocksuite/affine/data-view/src/view-presets/kanban/define.ts b/blocksuite/affine/data-view/src/view-presets/kanban/define.ts index a713bb470e..d0cb16d624 100644 --- a/blocksuite/affine/data-view/src/view-presets/kanban/define.ts +++ b/blocksuite/affine/data-view/src/view-presets/kanban/define.ts @@ -71,7 +71,6 @@ export const kanbanViewModel = kanbanViewType.createModel({ return { columns: columns.map(id => ({ id: id, - hide: false, })), filter: { type: 'group', diff --git a/blocksuite/affine/data-view/src/view-presets/kanban/kanban-view-manager.ts b/blocksuite/affine/data-view/src/view-presets/kanban/kanban-view-manager.ts index f7aed80bcd..afd71de607 100644 --- a/blocksuite/affine/data-view/src/view-presets/kanban/kanban-view-manager.ts +++ b/blocksuite/affine/data-view/src/view-presets/kanban/kanban-view-manager.ts @@ -2,7 +2,7 @@ import { insertPositionToIndex, type InsertToPosition, } from '@blocksuite/affine-shared/utils'; -import { computed, type ReadonlySignal } from '@preact/signals-core'; +import { computed } from '@preact/signals-core'; import { evalFilter } from '../../core/filter/eval.js'; import { generateDefaultValues } from '../../core/filter/generate-default-values.js'; @@ -20,7 +20,7 @@ import { SingleViewBase } from '../../core/view-manager/single-view.js'; import type { KanbanViewData } from './define.js'; export class KanbanSingleView extends SingleViewBase { - propertiesWithoutFilter$ = computed(() => { + propertiesRaw$ = computed(() => { const needShow = new Set(this.dataSource.properties$.value); const result: string[] = []; this.data$.value?.columns.forEach(v => { @@ -30,12 +30,16 @@ export class KanbanSingleView extends SingleViewBase { } }); result.push(...needShow); - return result; + return result.map(id => this.propertyGetOrCreate(id)); + }); + + properties$ = computed(() => { + return this.propertiesRaw$.value.filter(property => !property.hide$.value); }); detailProperties$ = computed(() => { - return this.propertiesWithoutFilter$.value.filter( - id => this.propertyTypeGet(id) !== 'title' + return this.propertiesRaw$.value.filter( + property => property.type$.value !== 'title' ); }); @@ -76,9 +80,13 @@ export class KanbanSingleView extends SingleViewBase { v => v, this.view?.groupProperties.map(v => v.key) ?? [] ), - sortRow: (key, ids) => { + sortRow: (key, rows) => { const property = this.view?.groupProperties.find(v => v.key === key); - return sortByManually(ids, v => v, property?.manuallyCardSort ?? []); + return sortByManually( + rows, + v => v.rowId, + property?.manuallyCardSort ?? [] + ); }, changeGroupSort: keys => { const map = new Map(this.view?.groupProperties.map(v => [v.key, v])); @@ -134,28 +142,20 @@ export class KanbanSingleView extends SingleViewBase { mainProperties$ = computed(() => { return ( this.data$.value?.header ?? { - titleColumn: this.propertiesWithoutFilter$.value.find( - id => this.propertyTypeGet(id) === 'title' - ), + titleColumn: this.propertiesRaw$.value.find( + property => property.type$.value === 'title' + )?.id, iconColumn: 'type', } ); }); - propertyIds$: ReadonlySignal = computed(() => { - return this.propertiesWithoutFilter$.value.filter( - id => !this.propertyHideGet(id) - ); - }); - readonly$ = computed(() => { return this.manager.readonly$.value; }); - get columns(): string[] { - return this.propertiesWithoutFilter$.value.filter( - id => !this.propertyHideGet(id) - ); + get columns(): KanbanColumn[] { + return this.propertiesRaw$.value.filter(property => !property.hide$.value); } get filter(): FilterGroup { @@ -182,8 +182,8 @@ export class KanbanSingleView extends SingleViewBase { if (filter.conditions.length > 0) { const defaultValues = generateDefaultValues(filter, this.vars$.value); Object.entries(defaultValues).forEach(([propertyId, jsonValue]) => { - const property = this.propertyGet(propertyId); - const propertyMeta = this.propertyMetaGet(property.type$.value); + const property = this.propertyGetOrCreate(propertyId); + const propertyMeta = property.meta$.value; if (!propertyMeta) { return; } @@ -192,7 +192,7 @@ export class KanbanSingleView extends SingleViewBase { data: property.data$.value, dataSource: this.dataSource, }); - this.cellValueSet(id, propertyId, value); + this.cellGetOrCreate(id, propertyId).valueSet(value); }); } @@ -204,7 +204,7 @@ export class KanbanSingleView extends SingleViewBase { if (!columnId) { return; } - return this.propertyGet(columnId); + return this.propertyGetOrCreate(columnId); } getHeaderIcon(_rowId: string): KanbanColumn | undefined { @@ -212,7 +212,7 @@ export class KanbanSingleView extends SingleViewBase { if (!columnId) { return; } - return this.propertyGet(columnId); + return this.propertyGetOrCreate(columnId); } getHeaderTitle(_rowId: string): KanbanColumn | undefined { @@ -220,7 +220,7 @@ export class KanbanSingleView extends SingleViewBase { if (!columnId) { return; } - return this.propertyGet(columnId); + return this.propertyGetOrCreate(columnId); } hasHeader(_rowId: string): boolean { @@ -248,7 +248,7 @@ export class KanbanSingleView extends SingleViewBase { const rowMap = Object.fromEntries( this.properties$.value.map(column => [ column.id, - column.cellGet(rowId).jsonValue$.value, + column.cellGetOrCreate(rowId).jsonValue$.value, ]) ); return evalFilter(this.filter$.value, rowMap); @@ -256,32 +256,16 @@ export class KanbanSingleView extends SingleViewBase { return true; } - propertyGet(columnId: string): KanbanColumn { + propertyGetOrCreate(columnId: string): KanbanColumn { return new KanbanColumn(this, columnId); } +} - propertyHideGet(columnId: string): boolean { - return this.view?.columns.find(v => v.id === columnId)?.hide ?? false; - } - - propertyHideSet(columnId: string, hide: boolean): void { - this.dataUpdate(view => { - return { - columns: view.columns.map(v => - v.id === columnId - ? { - ...v, - hide, - } - : v - ), - }; - }); - } - - propertyMove(columnId: string, toAfterOfColumn: InsertToPosition): void { - this.dataUpdate(view => { - const columnIndex = view.columns.findIndex(v => v.id === columnId); +type KanbanColumnData = KanbanViewData['columns'][number]; +export class KanbanColumn extends PropertyBase { + override move(position: InsertToPosition): void { + this.kanbanView.dataUpdate(view => { + const columnIndex = view.columns.findIndex(v => v.id === this.id); if (columnIndex < 0) { return {}; } @@ -290,7 +274,7 @@ export class KanbanSingleView extends SingleViewBase { if (!column) { return {}; } - const index = insertPositionToIndex(toAfterOfColumn, columns); + const index = insertPositionToIndex(position, columns); columns.splice(index, 0, column); return { columns, @@ -298,23 +282,47 @@ export class KanbanSingleView extends SingleViewBase { }); } - override rowMove(rowId: string, position: InsertToPosition): void { - this.dataSource.rowMove(rowId, position); + override hideSet(hide: boolean): void { + this.viewDataUpdate(data => { + return { + ...data, + hide, + }; + }); + } + hide$ = computed(() => { + const hideFromViewData = this.viewData$.value?.hide; + if (hideFromViewData != null) { + return hideFromViewData; + } + const defaultShow = this.meta$.value?.config.fixed?.defaultShow; + if (defaultShow != null) { + return !defaultShow; + } + return false; + }); + + viewData$ = computed(() => { + return this.kanbanView.data$.value?.columns.find(v => v.id === this.id); + }); + + viewDataUpdate( + updater: (viewData: KanbanColumnData) => Partial + ): void { + this.kanbanView.dataUpdate(data => { + return { + ...data, + columns: data.columns.map(v => + v.id === this.id ? { ...v, ...updater(v) } : v + ), + }; + }); } - override rowNextGet(rowId: string): string | undefined { - const index = this.rows$.value.indexOf(rowId); - return this.rows$.value[index + 1]; - } - - override rowPrevGet(rowId: string): string | undefined { - const index = this.rows$.value.indexOf(rowId); - return this.rows$.value[index - 1]; - } -} - -export class KanbanColumn extends PropertyBase { - constructor(dataViewManager: KanbanSingleView, columnId: string) { - super(dataViewManager, columnId); + constructor( + private readonly kanbanView: KanbanSingleView, + columnId: string + ) { + super(kanbanView, columnId); } } diff --git a/blocksuite/affine/data-view/src/view-presets/kanban/mobile/card.ts b/blocksuite/affine/data-view/src/view-presets/kanban/mobile/card.ts index 6cd46c9e80..970fcb3e30 100644 --- a/blocksuite/affine/data-view/src/view-presets/kanban/mobile/card.ts +++ b/blocksuite/affine/data-view/src/view-presets/kanban/mobile/card.ts @@ -155,7 +155,7 @@ export class MobileKanbanCard extends SignalWatcher( return; } return html`
- ${icon.cellGet(this.cardId).value$.value} + ${icon.cellGetOrCreate(this.cardId).value$.value}
`; } diff --git a/blocksuite/affine/data-view/src/view-presets/kanban/mobile/cell.ts b/blocksuite/affine/data-view/src/view-presets/kanban/mobile/cell.ts index dc1ed4172d..d997a6af1d 100644 --- a/blocksuite/affine/data-view/src/view-presets/kanban/mobile/cell.ts +++ b/blocksuite/affine/data-view/src/view-presets/kanban/mobile/cell.ts @@ -130,7 +130,7 @@ export class MobileKanbanCell extends SignalWatcher( override render() { const props: CellRenderProps = { - cell: this.column.cellGet(this.cardId), + cell: this.column.cellGetOrCreate(this.cardId), isEditing$: this.isEditing$, selectCurrentCell: this.selectCurrentCell, }; diff --git a/blocksuite/affine/data-view/src/view-presets/kanban/mobile/group.ts b/blocksuite/affine/data-view/src/view-presets/kanban/mobile/group.ts index 5e8e51fd1b..b6b4f4f6c5 100644 --- a/blocksuite/affine/data-view/src/view-presets/kanban/mobile/group.ts +++ b/blocksuite/affine/data-view/src/view-presets/kanban/mobile/group.ts @@ -77,15 +77,15 @@ export class MobileKanbanGroup extends SignalWatcher( name: 'Ungroup', hide: () => this.group.value == null, select: () => { - this.group.rows.forEach(id => { - this.group.manager.removeFromGroup(id, this.group.key); + this.group.rows.forEach(row => { + this.group.manager.removeFromGroup(row.rowId, this.group.key); }); }, }), menu.action({ name: 'Delete Cards', select: () => { - this.view.rowDelete(this.group.rows); + this.view.rowsDelete(this.group.rows.map(row => row.rowId)); }, }), ], @@ -106,15 +106,15 @@ export class MobileKanbanGroup extends SignalWatcher(
${repeat( cards, - id => id, - id => { + row => row.rowId, + row => { return html` `; } diff --git a/blocksuite/affine/data-view/src/view-presets/kanban/mobile/menu.ts b/blocksuite/affine/data-view/src/view-presets/kanban/mobile/menu.ts index b69fbb7857..7b84d44e13 100644 --- a/blocksuite/affine/data-view/src/view-presets/kanban/mobile/menu.ts +++ b/blocksuite/affine/data-view/src/view-presets/kanban/mobile/menu.ts @@ -106,7 +106,7 @@ export const popCardMenu = ( }, prefix: DeleteIcon(), select: () => { - view.rowDelete([cardId]); + view.rowsDelete([cardId]); }, }), ], diff --git a/blocksuite/affine/data-view/src/view-presets/kanban/pc/card.ts b/blocksuite/affine/data-view/src/view-presets/kanban/pc/card.ts index 817e44d0f4..3b7310a12b 100644 --- a/blocksuite/affine/data-view/src/view-presets/kanban/pc/card.ts +++ b/blocksuite/affine/data-view/src/view-presets/kanban/pc/card.ts @@ -230,7 +230,7 @@ export class KanbanCard extends SignalWatcher( return; } return html`
- ${icon.cellGet(this.cardId).value$.value} + ${icon.cellGetOrCreate(this.cardId).value$.value}
`; } diff --git a/blocksuite/affine/data-view/src/view-presets/kanban/pc/cell.ts b/blocksuite/affine/data-view/src/view-presets/kanban/pc/cell.ts index 8fcd067ca3..d9e5147f9e 100644 --- a/blocksuite/affine/data-view/src/view-presets/kanban/pc/cell.ts +++ b/blocksuite/affine/data-view/src/view-presets/kanban/pc/cell.ts @@ -129,7 +129,7 @@ export class KanbanCell extends SignalWatcher( override render() { const props: CellRenderProps = { - cell: this.column.cellGet(this.cardId), + cell: this.column.cellGetOrCreate(this.cardId), isEditing$: this.isEditing$, selectCurrentCell: this.selectCurrentCell, }; diff --git a/blocksuite/affine/data-view/src/view-presets/kanban/pc/controller/selection.ts b/blocksuite/affine/data-view/src/view-presets/kanban/pc/controller/selection.ts index af1f096eb3..5b9ed1aadb 100644 --- a/blocksuite/affine/data-view/src/view-presets/kanban/pc/controller/selection.ts +++ b/blocksuite/affine/data-view/src/view-presets/kanban/pc/controller/selection.ts @@ -116,7 +116,7 @@ export class KanbanSelectionController implements ReactiveController { return; } if (selection.selectionType === 'card') { - this.host.props.view.rowDelete(selection.cards.map(v => v.cardId)); + this.host.props.view.rowsDelete(selection.cards.map(v => v.cardId)); this.selection = undefined; } } @@ -155,12 +155,13 @@ export class KanbanSelectionController implements ReactiveController { focusFirstCell() { const group = this.host.groupManager?.groupsDataList$.value?.[0]; const card = group?.rows[0]; - const columnId = card && this.host.props.view.getHeaderTitle(card)?.id; + const columnId = + card && this.host.props.view.getHeaderTitle(card.rowId)?.id; if (group && card && columnId) { this.selection = { selectionType: 'cell', groupKey: group.key, - cardId: card, + cardId: card.rowId, columnId, isEditing: false, }; diff --git a/blocksuite/affine/data-view/src/view-presets/kanban/pc/group.ts b/blocksuite/affine/data-view/src/view-presets/kanban/pc/group.ts index 0fd37e9a19..597dd4b9b8 100644 --- a/blocksuite/affine/data-view/src/view-presets/kanban/pc/group.ts +++ b/blocksuite/affine/data-view/src/view-presets/kanban/pc/group.ts @@ -143,15 +143,15 @@ export class KanbanGroup extends SignalWatcher( name: 'Ungroup', hide: () => this.group.value == null, select: () => { - this.group.rows.forEach(id => { - this.group.manager.removeFromGroup(id, this.group.key); + this.group.rows.forEach(row => { + this.group.manager.removeFromGroup(row.rowId, this.group.key); }); }, }), menu.action({ name: 'Delete Cards', select: () => { - this.view.rowDelete(this.group.rows); + this.view.rowsDelete(this.group.rows.map(row => row.rowId)); }, }), ]); @@ -170,15 +170,15 @@ export class KanbanGroup extends SignalWatcher(
${repeat( cards, - id => id, - id => { + row => row.rowId, + row => { return html` `; } diff --git a/blocksuite/affine/data-view/src/view-presets/table/mobile/cell.ts b/blocksuite/affine/data-view/src/view-presets/table/mobile/cell.ts index 40b74c26f5..94b177fa87 100644 --- a/blocksuite/affine/data-view/src/view-presets/table/mobile/cell.ts +++ b/blocksuite/affine/data-view/src/view-presets/table/mobile/cell.ts @@ -11,7 +11,7 @@ import { type SingleView, } from '../../../core/index.js'; import { TableViewAreaSelection } from '../selection'; -import type { TableColumn } from '../table-view-manager.js'; +import type { TableProperty } from '../table-view-manager.js'; export class MobileTableCell extends SignalWatcher( WithDisposable(ShadowlessElement) @@ -38,13 +38,13 @@ export class MobileTableCell extends SignalWatcher( private readonly _cell = signal(); @property({ attribute: false }) - accessor column!: TableColumn; + accessor column!: TableProperty; @property({ attribute: false }) accessor rowId!: string; cell$ = computed(() => { - return this.column.cellGet(this.rowId); + return this.column.cellGetOrCreate(this.rowId); }); isSelectionEditing$ = computed(() => { diff --git a/blocksuite/affine/data-view/src/view-presets/table/mobile/column-header.ts b/blocksuite/affine/data-view/src/view-presets/table/mobile/column-header.ts index 74cd09164f..048b2eb2af 100644 --- a/blocksuite/affine/data-view/src/view-presets/table/mobile/column-header.ts +++ b/blocksuite/affine/data-view/src/view-presets/table/mobile/column-header.ts @@ -25,7 +25,7 @@ import { inputConfig, typeConfig } from '../../../core/common/property-menu.js'; import type { Property } from '../../../core/view-manager/property.js'; import { numberFormats } from '../../../property-presets/number/utils/formats.js'; import { DEFAULT_COLUMN_TITLE_HEIGHT } from '../consts.js'; -import type { TableColumn, TableSingleView } from '../table-view-manager.js'; +import type { TableProperty, TableSingleView } from '../table-view-manager.js'; export class MobileTableColumnHeader extends SignalWatcher( WithDisposable(ShadowlessElement) @@ -174,16 +174,14 @@ export class MobileTableColumnHeader extends SignalWatcher( menu.action({ name: 'Move Left', prefix: MoveLeftIcon(), - hide: () => this.column.isFirst, + hide: () => this.column.isFirst$.value, select: () => { - const preId = this.tableViewManager.propertyPreGet( - this.column.id - )?.id; - if (!preId) { + const pre = this.column.prev$.value; + if (!pre) { return; } - this.tableViewManager.propertyMove(this.column.id, { - id: preId, + this.column.move({ + id: pre.id, before: true, }); }, @@ -191,16 +189,14 @@ export class MobileTableColumnHeader extends SignalWatcher( menu.action({ name: 'Move Right', prefix: MoveRightIcon(), - hide: () => this.column.isLast, + hide: () => this.column.isLast$.value, select: () => { - const nextId = this.tableViewManager.propertyNextGet( - this.column.id - )?.id; - if (!nextId) { + const next = this.column.next$.value; + if (!next) { return; } - this.tableViewManager.propertyMove(this.column.id, { - id: nextId, + this.column.move({ + id: next.id, before: false, }); }, @@ -256,7 +252,7 @@ export class MobileTableColumnHeader extends SignalWatcher( } @property({ attribute: false }) - accessor column!: TableColumn; + accessor column!: TableProperty; @property({ attribute: false }) accessor tableViewManager!: TableSingleView; diff --git a/blocksuite/affine/data-view/src/view-presets/table/mobile/group.ts b/blocksuite/affine/data-view/src/view-presets/table/mobile/group.ts index 531ddf68bb..fab4df0830 100644 --- a/blocksuite/affine/data-view/src/view-presets/table/mobile/group.ts +++ b/blocksuite/affine/data-view/src/view-presets/table/mobile/group.ts @@ -14,6 +14,7 @@ import { repeat } from 'lit/directives/repeat.js'; import type { DataViewRenderer } from '../../../core/data-view.js'; import { GroupTitle } from '../../../core/group-by/group-title.js'; import type { GroupData } from '../../../core/group-by/trait.js'; +import type { Row } from '../../../core/index.js'; import { LEFT_TOOL_BAR_WIDTH } from '../consts.js'; import type { DataViewTable } from '../pc/table-view.js'; import { TableViewAreaSelection } from '../selection'; @@ -100,15 +101,15 @@ export class MobileTableGroup extends SignalWatcher( name: 'Ungroup', hide: () => group.value == null, select: () => { - group.rows.forEach(id => { - group.manager.removeFromGroup(id, group.key); + group.rows.forEach(row => { + group.manager.removeFromGroup(row.rowId, group.key); }); }, }), menu.action({ name: 'Delete Cards', select: () => { - this.view.rowDelete(group.rows); + this.view.rowsDelete(group.rows.map(row => row.rowId)); }, }), ]); @@ -135,7 +136,7 @@ export class MobileTableGroup extends SignalWatcher( return this.group?.rows ?? this.view.rows$.value; } - private renderRows(ids: string[]) { + private renderRows(rows: Row[]) { return html`
${repeat( - ids, - id => id, - (id, idx) => { + rows, + row => row.rowId, + (row, idx) => { return html` `; } diff --git a/blocksuite/affine/data-view/src/view-presets/table/mobile/menu.ts b/blocksuite/affine/data-view/src/view-presets/table/mobile/menu.ts index 4ded6e42f1..2cce10f378 100644 --- a/blocksuite/affine/data-view/src/view-presets/table/mobile/menu.ts +++ b/blocksuite/affine/data-view/src/view-presets/table/mobile/menu.ts @@ -37,7 +37,7 @@ export const popMobileRowMenu = ( class: { 'delete-item': true }, prefix: DeleteIcon(), select: () => { - view.rowDelete([rowId]); + view.rowsDelete([rowId]); }, }), ], diff --git a/blocksuite/affine/data-view/src/view-presets/table/pc-virtual/controller/clipboard.ts b/blocksuite/affine/data-view/src/view-presets/table/pc-virtual/controller/clipboard.ts index 4c449223fd..970b07e30b 100644 --- a/blocksuite/affine/data-view/src/view-presets/table/pc-virtual/controller/clipboard.ts +++ b/blocksuite/affine/data-view/src/view-presets/table/pc-virtual/controller/clipboard.ts @@ -30,7 +30,7 @@ export class TableClipboardController implements ReactiveController { .map(row => row.cells.map(cell => cell.stringValue$.value).join('\t')) .join('\n'); const jsonResult: JsonAreaData = area.map(row => - row.cells.map(cell => cell.stringValue$.value) + row.cells.map(cell => cell.stringValue$.value ?? '') ); if (isCut) { const deleteRows: string[] = []; @@ -44,7 +44,7 @@ export class TableClipboardController implements ReactiveController { } } if (deleteRows.length) { - this.props.view.rowDelete(deleteRows); + this.props.view.rowsDelete(deleteRows); } } this.clipboard @@ -210,7 +210,7 @@ function getSelectedArea( .sort((a, b) => a.y - b.y) .map(v => v.row); return rows.map(r => { - const row = view.rowGet(r.id); + const row = view.rowGetOrCreate(r.id); return { row, cells: row.cells$.value, @@ -227,11 +227,11 @@ function getSelectedArea( return; } for (let i = rowsSelection.start; i <= rowsSelection.end; i++) { - const row: SelectedArea[number] = { + const rowArea: SelectedArea[number] = { cells: [], }; - const rowId = rows[i]; - if (rowId == null) { + const row = rows[i]; + if (row == null) { continue; } for (let j = columnsSelection.start; j <= columnsSelection.end; j++) { @@ -239,10 +239,10 @@ function getSelectedArea( if (columnId == null) { continue; } - const cell = view.cellGet(rowId, columnId); - row.cells.push(cell); + const cell = view.cellGetOrCreate(row.rowId, columnId); + rowArea.cells.push(cell); } - data.push(row); + data.push(rowArea); } return data; diff --git a/blocksuite/affine/data-view/src/view-presets/table/pc-virtual/controller/drag-to-fill.ts b/blocksuite/affine/data-view/src/view-presets/table/pc-virtual/controller/drag-to-fill.ts index da11b735ba..797ebab271 100644 --- a/blocksuite/affine/data-view/src/view-presets/table/pc-virtual/controller/drag-to-fill.ts +++ b/blocksuite/affine/data-view/src/view-presets/table/pc-virtual/controller/drag-to-fill.ts @@ -95,7 +95,7 @@ export function fillSelectionWithFocusCellData( const curCell = cellContainer.cell$.value; - if (t.richText.is(curCol.dataType$.value)) { + if (curCol.dataType$.value && t.richText.is(curCol.dataType$.value)) { const focusCellText = focusData as Text | undefined; const delta = focusCellText?.toDelta() ?? [{ insert: '' }]; diff --git a/blocksuite/affine/data-view/src/view-presets/table/pc-virtual/controller/hotkeys.ts b/blocksuite/affine/data-view/src/view-presets/table/pc-virtual/controller/hotkeys.ts index 002d364a34..e2f4151ae4 100644 --- a/blocksuite/affine/data-view/src/view-presets/table/pc-virtual/controller/hotkeys.ts +++ b/blocksuite/affine/data-view/src/view-presets/table/pc-virtual/controller/hotkeys.ts @@ -25,7 +25,7 @@ export class TableHotkeysController implements ReactiveController { if (TableViewRowSelection.is(selection)) { const rows = TableViewRowSelection.rowsIds(selection); this.selectionController.selection = undefined; - this.host.props.view.rowDelete(rows); + this.host.props.view.rowsDelete(rows); return; } const { @@ -336,11 +336,14 @@ export class TableHotkeysController implements ReactiveController { rows: this.host.props.view.groupTrait.groupsDataList$.value?.flatMap( group => - group?.rows.map(id => ({ groupKey: group.key, id })) ?? [] + group?.rows.map(row => ({ + groupKey: group.key, + id: row.rowId, + })) ?? [] ) ?? - this.host.props.view.rows$.value.map(id => ({ + this.host.props.view.rows$.value.map(row => ({ groupKey: undefined, - id, + id: row.rowId, })), }); return true; diff --git a/blocksuite/affine/data-view/src/view-presets/table/pc-virtual/controller/selection.ts b/blocksuite/affine/data-view/src/view-presets/table/pc-virtual/controller/selection.ts index 73693c1c85..a56e293d9c 100644 --- a/blocksuite/affine/data-view/src/view-presets/table/pc-virtual/controller/selection.ts +++ b/blocksuite/affine/data-view/src/view-presets/table/pc-virtual/controller/selection.ts @@ -242,7 +242,7 @@ export class TableSelectionController implements ReactiveController { this.selection = TableViewAreaSelection.create({ groupKey: groupKey, focus: { - rowIndex: rows?.findIndex(v => v === id) ?? 0, + rowIndex: rows?.findIndex(v => v.rowId === id) ?? 0, columnIndex: index, }, isEditing: true, @@ -373,7 +373,7 @@ export class TableSelectionController implements ReactiveController { } deleteRow(rowId: string) { - this.view.rowDelete([rowId]); + this.view.rowsDelete([rowId]); this.focusToCell('up'); } diff --git a/blocksuite/affine/data-view/src/view-presets/table/pc-virtual/group/bottom/stats/column-stats-column.ts b/blocksuite/affine/data-view/src/view-presets/table/pc-virtual/group/bottom/stats/column-stats-column.ts index 7c20456d8c..f74faf9de2 100644 --- a/blocksuite/affine/data-view/src/view-presets/table/pc-virtual/group/bottom/stats/column-stats-column.ts +++ b/blocksuite/affine/data-view/src/view-presets/table/pc-virtual/group/bottom/stats/column-stats-column.ts @@ -18,7 +18,7 @@ import { typeSystem } from '../../../../../../core'; import type { GroupData } from '../../../../../../core/group-by/trait'; import { statsFunctions } from '../../../../../../core/statistics'; import type { StatisticsConfig } from '../../../../../../core/statistics/types'; -import type { TableColumn } from '../../../../table-view-manager'; +import type { TableProperty } from '../../../../table-view-manager'; const styles = css` .stats-cell { @@ -73,12 +73,12 @@ export class VirtualDatabaseColumnStatsCell extends SignalWatcher( static override styles = styles; @property({ attribute: false }) - accessor column!: TableColumn; + accessor column!: TableProperty; cellValues$ = computed(() => { if (this.group) { - return this.group.rows.map(id => { - return this.column.valueGet(id); + return this.group.rows.map(row => { + return this.column.valueGet(row.rowId); }); } return this.column.cells$.value.map(cell => cell.jsonValue$.value); @@ -157,7 +157,7 @@ export class VirtualDatabaseColumnStatsCell extends SignalWatcher( values$ = signal([]); statsResult$ = computed(() => { - const meta = this.column.view.propertyMetaGet(this.column.type$.value); + const meta = this.column.meta$.value; if (!meta) { return null; } diff --git a/blocksuite/affine/data-view/src/view-presets/table/pc-virtual/group/top/group-header.ts b/blocksuite/affine/data-view/src/view-presets/table/pc-virtual/group/top/group-header.ts index cd87d89edc..2408619507 100644 --- a/blocksuite/affine/data-view/src/view-presets/table/pc-virtual/group/top/group-header.ts +++ b/blocksuite/affine/data-view/src/view-presets/table/pc-virtual/group/top/group-header.ts @@ -86,15 +86,15 @@ export class TableGroupHeader extends SignalWatcher( name: 'Ungroup', hide: () => group.value == null, select: () => { - group.rows.forEach(id => { - group.manager.removeFromGroup(id, group.key); + group.rows.forEach(row => { + group.manager.removeFromGroup(row.rowId, group.key); }); }, }), menu.action({ name: 'Delete Cards', select: () => { - this.tableViewManager.rowDelete(group.rows); + this.tableViewManager.rowsDelete(group.rows.map(row => row.rowId)); }, }), ]); diff --git a/blocksuite/affine/data-view/src/view-presets/table/pc-virtual/group/top/group-title.ts b/blocksuite/affine/data-view/src/view-presets/table/pc-virtual/group/top/group-title.ts index e86cb4188f..b5a3c8fb92 100644 --- a/blocksuite/affine/data-view/src/view-presets/table/pc-virtual/group/top/group-title.ts +++ b/blocksuite/affine/data-view/src/view-presets/table/pc-virtual/group/top/group-title.ts @@ -48,7 +48,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, }; diff --git a/blocksuite/affine/data-view/src/view-presets/table/pc-virtual/group/top/header/column-header.css.ts b/blocksuite/affine/data-view/src/view-presets/table/pc-virtual/group/top/header/column-header.css.ts index c8b452afcc..4e32564196 100644 --- a/blocksuite/affine/data-view/src/view-presets/table/pc-virtual/group/top/header/column-header.css.ts +++ b/blocksuite/affine/data-view/src/view-presets/table/pc-virtual/group/top/header/column-header.css.ts @@ -1,11 +1,7 @@ -import { baseTheme } from '@toeverything/theme'; import { cssVarV2 } from '@toeverything/theme/v2'; import { globalStyle, style } from '@vanilla-extract/css'; -import { - DEFAULT_ADD_BUTTON_WIDTH, - DEFAULT_COLUMN_TITLE_HEIGHT, -} from '../../../../consts'; +import { DEFAULT_COLUMN_TITLE_HEIGHT } from '../../../../consts'; export const columnHeaderContainer = style({ display: 'block', @@ -33,104 +29,6 @@ export const cell = style({ userSelect: 'none', }); -export const addColumnButton = style({ - flex: 1, - minWidth: `${DEFAULT_ADD_BUTTON_WIDTH}px`, - minHeight: '100%', - display: 'flex', - alignItems: 'center', -}); - -export const columnContent = style({ - display: 'flex', - alignItems: 'center', - gap: '6px', - width: '100%', - height: '100%', - padding: '6px', - boxSizing: 'border-box', - position: 'relative', -}); - -export const columnText = style({ - flex: 1, - display: 'flex', - alignItems: 'center', - gap: '6px', - overflow: 'hidden', - color: 'var(--affine-text-secondary-color)', - fontSize: '14px', - position: 'relative', -}); - -export const columnTypeIcon = style({ - display: 'flex', - alignItems: 'center', - borderRadius: '4px', - padding: '2px', - fontSize: '18px', - color: cssVarV2.icon.primary, -}); - -export const columnTextContent = style({ - flex: 1, - display: 'flex', - alignItems: 'center', - overflow: 'hidden', -}); - -export const columnTextInput = style({ - flex: 1, - whiteSpace: 'nowrap', - overflow: 'hidden', - textOverflow: 'ellipsis', - fontWeight: 500, -}); - -export const columnTextIcon = style({ - display: 'flex', - alignItems: 'center', - width: '16px', - height: '16px', - background: 'var(--affine-white)', - border: `1px solid ${cssVarV2.layer.insideBorder.border}`, - borderRadius: '4px', - opacity: 0, -}); - -export const columnTextSaveIcon = style({ - display: 'flex', - alignItems: 'center', - width: '16px', - height: '16px', - border: '1px solid transparent', - borderRadius: '4px', - fill: 'var(--affine-icon-color)', - selectors: { - '&:hover': { - background: 'var(--affine-white)', - borderColor: cssVarV2.layer.insideBorder.border, - }, - }, -}); - -export const columnInput = style({ - width: '100%', - height: '24px', - padding: 0, - border: 'none', - color: 'inherit', - fontWeight: 600, - fontSize: '14px', - fontFamily: baseTheme.fontSansFamily, - background: 'transparent', - selectors: { - '&:focus': { - outline: 'none', - }, - }, -}); - export const columnMove = style({ display: 'flex', alignItems: 'center', @@ -156,17 +54,6 @@ globalStyle(`${columnMove} svg`, { opacity: 0, }); -export const databaseAddColumnButton = style({ - position: 'sticky', - right: 0, - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - width: '40px', - height: '38px', - cursor: 'pointer', -}); - export const headerAddColumnButton = style({ height: `${DEFAULT_COLUMN_TITLE_HEIGHT}px`, backgroundColor: 'var(--affine-background-primary-color)', @@ -178,18 +65,3 @@ export const headerAddColumnButton = style({ fontSize: '18px', color: cssVarV2.icon.primary, }); - -export const columnTypeMenuIcon = style({ - border: `1px solid ${cssVarV2.layer.insideBorder.border}`, - borderRadius: '4px', - padding: '5px', - backgroundColor: 'var(--affine-background-secondary-color)', -}); - -export const columnMovePreview = style({ - position: 'fixed', - zIndex: 100, - width: '100px', - height: '100px', - background: 'var(--affine-text-emphasis-color)', -}); diff --git a/blocksuite/affine/data-view/src/view-presets/table/pc-virtual/group/top/header/column-header.ts b/blocksuite/affine/data-view/src/view-presets/table/pc-virtual/group/top/header/column-header.ts index 2bbc1ae352..1f45c1d0f0 100644 --- a/blocksuite/affine/data-view/src/view-presets/table/pc-virtual/group/top/header/column-header.ts +++ b/blocksuite/affine/data-view/src/view-presets/table/pc-virtual/group/top/header/column-header.ts @@ -30,8 +30,6 @@ export class VirtualTableHeader extends SignalWatcher( column.editTitle(); }; - preMove = 0; - private get readonly() { return this.tableViewManager.readonly$.value; } @@ -41,10 +39,6 @@ export class VirtualTableHeader extends SignalWatcher( this.classList.add(styles.columnHeaderContainer); } - getScale() { - return this.scaleDiv?.getBoundingClientRect().width ?? 1; - } - override render() { return html`
diff --git a/blocksuite/affine/data-view/src/view-presets/table/pc-virtual/group/top/header/column-move-preview.ts b/blocksuite/affine/data-view/src/view-presets/table/pc-virtual/group/top/header/column-move-preview.ts index 918fbca038..119f2cd9a0 100644 --- a/blocksuite/affine/data-view/src/view-presets/table/pc-virtual/group/top/header/column-move-preview.ts +++ b/blocksuite/affine/data-view/src/view-presets/table/pc-virtual/group/top/header/column-move-preview.ts @@ -8,8 +8,9 @@ import { styleMap } from 'lit/directives/style-map.js'; import { html } from 'lit/static-html.js'; import type { GroupData } from '../../../../../../core/group-by/trait'; +import type { Row } from '../../../../../../core/view-manager/row'; import type { - TableColumn, + TableProperty, TableSingleView, } from '../../../../table-view-manager'; @@ -29,8 +30,8 @@ export class DataViewColumnPreview extends SignalWatcher( return this.column.view as TableSingleView; } - private renderGroup(rows: string[]) { - const columnIndex = this.tableViewManager.propertyIndexGet(this.column.id); + private renderGroup(rows: Row[]) { + const columnIndex = this.column.index$.value; return html`
${repeat( - ids, - id => id, - (id, idx) => { + rows, + row => row.rowId, + (row, idx) => { return html` `; } diff --git a/blocksuite/affine/data-view/src/view-presets/table/pc/header/column-renderer.ts b/blocksuite/affine/data-view/src/view-presets/table/pc/header/column-renderer.ts index 6cf0e9cf1a..72c05d9544 100644 --- a/blocksuite/affine/data-view/src/view-presets/table/pc/header/column-renderer.ts +++ b/blocksuite/affine/data-view/src/view-presets/table/pc/header/column-renderer.ts @@ -8,7 +8,11 @@ import { styleMap } from 'lit/directives/style-map.js'; import { html } from 'lit/static-html.js'; import type { GroupData } from '../../../../core/group-by/trait.js'; -import type { TableColumn, TableSingleView } from '../../table-view-manager.js'; +import type { Row } from '../../../../core/index.js'; +import type { + TableProperty, + TableSingleView, +} from '../../table-view-manager.js'; export class DataViewColumnPreview extends SignalWatcher( WithDisposable(ShadowlessElement) @@ -26,8 +30,8 @@ export class DataViewColumnPreview extends SignalWatcher( return this.column.view as TableSingleView; } - private renderGroup(rows: string[]) { - const columnIndex = this.tableViewManager.propertyIndexGet(this.column.id); + private renderGroup(rows: Row[]) { + const columnIndex = this.column.index$.value; return html`
({}), + }, + fixed: { + defaultData: {}, + defaultOrder: 'end', + defaultShow: false, + }, + rawValue: { + schema: zod.string().nullable(), + default: () => null, + toString: ({ value }) => value ?? '', + fromString: () => { + return { value: null }; + }, + toJson: ({ value }) => value, + fromJson: ({ value }) => value, + }, + jsonValue: { + schema: zod.string().nullable(), + isEmpty: () => false, + type: () => t.string.instance(), + }, +}); diff --git a/packages/frontend/core/src/blocksuite/database-block/properties/created-by/view.tsx b/packages/frontend/core/src/blocksuite/database-block/properties/created-by/view.tsx new file mode 100644 index 0000000000..01f581d312 --- /dev/null +++ b/packages/frontend/core/src/blocksuite/database-block/properties/created-by/view.tsx @@ -0,0 +1,130 @@ +import { Avatar, uniReactRoot } from '@affine/component'; +import { + type CellRenderProps, + createIcon, + type DataViewCellLifeCycle, + HostContextKey, +} from '@blocksuite/affine/blocks/database'; +import { + UserProvider, + type UserService, +} from '@blocksuite/affine/shared/services'; +import { css } from '@emotion/css'; +import { + forwardRef, + type ForwardRefRenderFunction, + type ReactNode, + useEffect, + useImperativeHandle, +} from 'react'; + +import { useSignalValue } from '../../../../modules/doc-info/utils'; +import { createdByPropertyModelConfig } from './define'; + +const cellContainer = css({ + width: '100%', + position: 'relative', + gap: '6px', + display: 'flex', + flexWrap: 'wrap', + overflow: 'hidden', +}); +const memberPreviewContainer = css({ + display: 'flex', + alignItems: 'center', + gap: '4px', + overflow: 'hidden', +}); +const memberName = css({ + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + fontSize: '14px', + lineHeight: '22px', +}); +const avatar = css({ + flexShrink: 0, +}); + +const CreatedByCellComponent: ForwardRefRenderFunction< + DataViewCellLifeCycle, + CellRenderProps<{}, string | null, string | null> +> = (props, ref): ReactNode => { + useImperativeHandle( + ref, + () => ({ + beforeEnterEditMode: () => { + return false; + }, + beforeExitEditingMode: () => {}, + afterEnterEditingMode: () => {}, + focusCell: () => true, + blurCell: () => true, + forceUpdate: () => {}, + }), + [] + ); + const host = props.cell.view.contextGet(HostContextKey); + const userService = host?.std.getOptional(UserProvider); + const memberId = useSignalValue(props.cell.value$); + if (!memberId) { + return null; + } + return ( +
+
+ +
+
+ ); +}; + +const useMemberInfo = ( + id: string, + userService: UserService | null | undefined +) => { + useEffect(() => { + userService?.revalidateUserInfo(id); + }, [id, userService]); + return useSignalValue(userService?.userInfo$(id)); +}; + +const MemberPreview = ({ + memberId, + userService, +}: { + memberId: string; + userService: UserService | null | undefined; +}) => { + const userInfo = useMemberInfo(memberId, userService); + if (!userInfo) { + return null; + } + return ( +
+ +
+ {userInfo.removed ? 'Deleted user' : userInfo.name || 'Unnamed'} +
+
+ ); +}; + +const CreatedByCell = forwardRef(CreatedByCellComponent); + +export const createdByPropertyConfig = + createdByPropertyModelConfig.createPropertyMeta({ + icon: createIcon('UserIcon'), + cellRenderer: { + view: uniReactRoot.createUniComponent(CreatedByCell), + }, + }); diff --git a/packages/frontend/core/src/blocksuite/database-block/properties/index.ts b/packages/frontend/core/src/blocksuite/database-block/properties/index.ts index d77e6ab42a..70ce48f565 100644 --- a/packages/frontend/core/src/blocksuite/database-block/properties/index.ts +++ b/packages/frontend/core/src/blocksuite/database-block/properties/index.ts @@ -1,9 +1,11 @@ import type { PropertyMetaConfig } from '@blocksuite/affine/blocks/database'; +import { createdByPropertyConfig } from './created-by/view'; import { filePropertyConfig } from './file/view'; import { memberPropertyConfig } from './member/view'; export const propertiesPresets: PropertyMetaConfig[] = [ filePropertyConfig, memberPropertyConfig, + createdByPropertyConfig, ]; diff --git a/packages/frontend/core/src/blocksuite/database-block/properties/member/multi-member-select/index.tsx b/packages/frontend/core/src/blocksuite/database-block/properties/member/multi-member-select/index.tsx index d7a424bcd4..209c9300ca 100644 --- a/packages/frontend/core/src/blocksuite/database-block/properties/member/multi-member-select/index.tsx +++ b/packages/frontend/core/src/blocksuite/database-block/properties/member/multi-member-select/index.tsx @@ -205,7 +205,11 @@ export const MemberListItem = (props: { data-selected={isSelected ? 'true' : 'false'} >
- +
{member.name}
@@ -228,6 +232,7 @@ export const MemberPreview = ({ return (