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

close: BS-3431

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

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

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

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

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

- **Chores**
  - Added `@emotion/css` dependency for styling support.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
zzj3720
2025-05-08 11:35:36 +00:00
parent 7c8b977bf9
commit 6689bd1914
76 changed files with 1168 additions and 1042 deletions
@@ -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<string, SpacialProperty> = {
'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<PropertyMetaConfig[]>([]);
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) {
@@ -140,7 +140,7 @@ export class BlockRenderer
return;
}
return html` <div class="database-block-detail-header-icon">
${this.view.cellValueGet(this.rowId, iconColumn)}
${this.view.cellGetOrCreate(this.rowId, iconColumn).value$.value}
</div>`;
}
@@ -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);
@@ -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<number, number> {
renderContent() {
const formattedDate = this.value
? format(this.value, 'yyyy-MM-dd HH:mm:ss')
: '';
return html`<div class="${textStyle}">${formattedDate}</div>`;
}
override connectedCallback(): void {
super.connectedCallback();
this.classList.add(createdTimeCellStyle);
}
override beforeEnterEditMode() {
return false;
}
override render() {
return html`<div class="date-container">${this.renderContent()}</div>`;
}
}
export const createdTimeColumnConfig =
createdTimePropertyModelConfig.createPropertyMeta({
icon: createIcon('DateTimeIcon'),
cellRenderer: {
view: createFromBaseCellRenderer(CreatedTimeCell),
},
});
@@ -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',
},
}
);
@@ -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,
};
@@ -310,7 +310,6 @@ export class RichTextCell extends BaseCellRenderer<Text, string> {
});
}
} else {
console.log(text);
inlineEditor.insertText(inlineRange, text);
inlineEditor.setInlineRange({
index: inlineRange.index + text.length,
@@ -271,7 +271,8 @@ export class HeaderAreaTextCell extends BaseCellRenderer<Text, string> {
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;
});