From a2a90df27674c7983cf41089cf40fbdd8e40d7d6 Mon Sep 17 00:00:00 2001 From: zzj3720 <17165520+zzj3720@users.noreply.github.com> Date: Tue, 13 May 2025 13:53:37 +0000 Subject: [PATCH] feat(editor): add grouping support for member property of the database block (#12243) close: BS-3433 ## Summary by CodeRabbit - **New Features** - Introduced advanced group-by configurations for database blocks with user membership support. - Added a React hook for fetching and displaying user information in member-related components. - Enabled dynamic user and membership data types in database properties. - **Improvements** - Replaced context-based service access with a dependency injection system for shared services and state. - Enhanced type safety and consistency across group-by UI components and data handling. - Centralized group data management with a new Group class and refined group trait logic. - **Bug Fixes** - Improved reliability and consistency in retrieving and rendering user and group information. - **Style** - Removed obsolete member selection styles for cleaner UI code. - **Chores** - Registered external group-by configurations via dependency injection. - Refactored internal APIs for data sources, views, and group-by matchers to use service-based patterns. --- .../database/src/context/host-context.ts | 7 +- .../affine/blocks/database/src/data-source.ts | 11 +- .../blocks/database/src/database-block.ts | 16 +- .../blocks/database/src/database-spec.ts | 14 -- .../affine/blocks/database/src/index.ts | 1 - .../src/properties/link/cell-renderer.ts | 4 +- .../src/properties/rich-text/cell-renderer.ts | 8 +- .../src/properties/rich-text/define.ts | 4 +- .../database/src/properties/title/define.ts | 4 +- .../database/src/properties/title/text.ts | 6 +- .../data-view/src/core/data-source/base.ts | 39 +++- .../data-view/src/core/data-source/context.ts | 12 -- .../data-view/src/core/data-source/index.ts | 1 - .../data-view/src/core/group-by/default.ts | 5 +- .../data-view/src/core/group-by/define.ts | 40 ++-- .../src/core/group-by/group-title.ts | 42 ++-- .../data-view/src/core/group-by/index.ts | 2 + .../data-view/src/core/group-by/matcher.ts | 40 +++- .../src/core/group-by/renderer/base.ts | 33 ++- .../core/group-by/renderer/boolean-group.ts | 2 +- .../core/group-by/renderer/number-group.ts | 2 +- .../core/group-by/renderer/select-group.ts | 4 +- .../core/group-by/renderer/string-group.ts | 2 +- .../data-view/src/core/group-by/setting.ts | 18 +- .../data-view/src/core/group-by/trait.ts | 194 ++++++++++-------- .../data-view/src/core/group-by/types.ts | 38 ++-- .../data-view/src/core/logical/matcher.ts | 43 ++++ .../src/core/logical/type-presets.ts | 11 + .../src/core/view-manager/single-view.ts | 13 +- .../src/view-presets/kanban/define.ts | 5 +- .../src/view-presets/kanban/mobile/group.ts | 4 +- .../src/view-presets/kanban/mobile/menu.ts | 2 +- .../src/view-presets/kanban/pc/group.ts | 4 +- .../src/view-presets/kanban/pc/menu.ts | 2 +- .../src/view-presets/table/mobile/group.ts | 4 +- .../group/bottom/stats/column-stats-bar.ts | 4 +- .../group/bottom/stats/column-stats-column.ts | 4 +- .../table/pc-virtual/group/top/group-title.ts | 26 +-- .../group/top/header/column-move-preview.ts | 4 +- .../group/top/header/single-column-header.ts | 10 +- .../src/view-presets/table/pc/group.ts | 4 +- .../table/pc/header/column-renderer.ts | 4 +- .../table/pc/header/database-header-column.ts | 10 +- .../table/stats/column-stats-bar.ts | 4 +- .../table/stats/column-stats-column.ts | 4 +- .../quick-setting-bar/context.ts | 11 +- .../widget-presets/quick-setting-bar/index.ts | 12 +- .../tools/presets/filter/filter.ts | 10 +- .../widget-presets/tools/presets/sort/sort.ts | 10 +- .../database-block/group-by/index.tsx | 147 +++++++++++++ .../database-block/hooks/use-member-info.ts | 14 ++ .../properties/created-by/define.ts | 25 ++- .../properties/created-by/view.tsx | 16 +- .../database-block/properties/file/view.tsx | 6 +- .../properties/member/define.ts | 26 ++- .../member/multi-member-select/index.tsx | 10 +- .../properties/member/style.css.ts | 51 ----- .../database-block/properties/member/view.tsx | 15 +- .../database-block-config-service.ts | 12 +- 59 files changed, 702 insertions(+), 374 deletions(-) delete mode 100644 blocksuite/affine/blocks/database/src/database-spec.ts delete mode 100644 blocksuite/affine/data-view/src/core/data-source/context.ts create mode 100644 packages/frontend/core/src/blocksuite/database-block/group-by/index.tsx create mode 100644 packages/frontend/core/src/blocksuite/database-block/hooks/use-member-info.ts diff --git a/blocksuite/affine/blocks/database/src/context/host-context.ts b/blocksuite/affine/blocks/database/src/context/host-context.ts index 727a147c3..8979ede01 100644 --- a/blocksuite/affine/blocks/database/src/context/host-context.ts +++ b/blocksuite/affine/blocks/database/src/context/host-context.ts @@ -1,7 +1,4 @@ -import { createContextKey } from '@blocksuite/data-view'; +import { createIdentifier } from '@blocksuite/global/di'; import type { EditorHost } from '@blocksuite/std'; -export const HostContextKey = createContextKey( - 'editor-host', - undefined -); +export const EditorHostKey = createIdentifier('editor-host'); diff --git a/blocksuite/affine/blocks/database/src/data-source.ts b/blocksuite/affine/blocks/database/src/data-source.ts index 4d8830f81..e97fbf0cd 100644 --- a/blocksuite/affine/blocks/database/src/data-source.ts +++ b/blocksuite/affine/blocks/database/src/data-source.ts @@ -57,6 +57,9 @@ type SpacialProperty = { valueGet: (rowId: string, propertyId: string) => unknown; }; export class DatabaseBlockDataSource extends DataSourceBase { + override get parentProvider() { + return this._model.store.provider; + } spacialProperties: Record = { 'created-time': { valueSet: () => {}, @@ -186,9 +189,13 @@ export class DatabaseBlockDataSource extends DataSourceBase { ); }); - constructor(model: DatabaseBlockModel) { + constructor( + model: DatabaseBlockModel, + init?: (dataSource: DatabaseBlockDataSource) => void + ) { super(); - this._model = model; + this._model = model; // ensure invariants first + init?.(this); // then allow external initialisation } private _runCapture() { diff --git a/blocksuite/affine/blocks/database/src/database-block.ts b/blocksuite/affine/blocks/database/src/database-block.ts index 83c565fbb..92c284a9c 100644 --- a/blocksuite/affine/blocks/database/src/database-block.ts +++ b/blocksuite/affine/blocks/database/src/database-block.ts @@ -27,6 +27,7 @@ import { type DataViewWidget, type DataViewWidgetProps, defineUniComponent, + ExternalGroupByConfigProvider, renderUniLit, type SingleView, uniMap, @@ -47,7 +48,7 @@ import { css, html, nothing, unsafeCSS } from 'lit'; import { popSideDetail } from './components/layout.js'; import { DatabaseConfigExtension } from './config.js'; -import { HostContextKey } from './context/host-context.js'; +import { EditorHostKey } from './context/host-context.js'; import { DatabaseBlockDataSource } from './data-source.js'; import { BlockRenderer } from './detail-panel/block-renderer.js'; import { NoteRenderer } from './detail-panel/note-renderer.js'; @@ -333,8 +334,17 @@ export class DatabaseBlockComponent extends CaptionedBlockComponent { + dataSource.serviceSet(EditorHostKey, this.host); + this.std.provider + .getAll(ExternalGroupByConfigProvider) + .forEach(config => { + dataSource.serviceSet( + ExternalGroupByConfigProvider(config.name), + config + ); + }); + }); const id = currentViewStorage.getCurrentView(this.model.id); if (id && this.dataSource.viewManager.viewGet(id)) { this.dataSource.viewManager.setCurrentView(id); diff --git a/blocksuite/affine/blocks/database/src/database-spec.ts b/blocksuite/affine/blocks/database/src/database-spec.ts deleted file mode 100644 index 863db62e2..000000000 --- a/blocksuite/affine/blocks/database/src/database-spec.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { SlashMenuConfigExtension } from '@blocksuite/affine-widget-slash-menu'; -import { BlockViewExtension, FlavourExtension } from '@blocksuite/std'; -import type { ExtensionType } from '@blocksuite/store'; -import { literal } from 'lit/static-html.js'; - -import { DatabaseBlockAdapterExtensions } from './adapters/extension.js'; -import { databaseSlashMenuConfig } from './configs/slash-menu.js'; - -export const DatabaseBlockSpec: ExtensionType[] = [ - FlavourExtension('affine:database'), - BlockViewExtension('affine:database', literal`affine-database`), - DatabaseBlockAdapterExtensions, - SlashMenuConfigExtension('affine:database', databaseSlashMenuConfig), -].flat(); diff --git a/blocksuite/affine/blocks/database/src/index.ts b/blocksuite/affine/blocks/database/src/index.ts index e3f4689f5..90813a8cd 100644 --- a/blocksuite/affine/blocks/database/src/index.ts +++ b/blocksuite/affine/blocks/database/src/index.ts @@ -4,7 +4,6 @@ export * from './config'; export * from './context'; export * from './data-source'; export * from './database-block'; -export * from './database-spec'; export * from './detail-panel/block-renderer'; export * from './detail-panel/note-renderer'; export * from './properties'; diff --git a/blocksuite/affine/blocks/database/src/properties/link/cell-renderer.ts b/blocksuite/affine/blocks/database/src/properties/link/cell-renderer.ts index 8ff76cdf5..67e4b5fe4 100644 --- a/blocksuite/affine/blocks/database/src/properties/link/cell-renderer.ts +++ b/blocksuite/affine/blocks/database/src/properties/link/cell-renderer.ts @@ -15,7 +15,7 @@ import { computed } from '@preact/signals-core'; import { html, nothing, type PropertyValues } from 'lit'; import { createRef, ref } from 'lit/directives/ref.js'; -import { HostContextKey } from '../../context/host-context.js'; +import { EditorHostKey } from '../../context/host-context.js'; import { inlineLinkNodeStyle, linkCellStyle, @@ -88,7 +88,7 @@ export class LinkCell extends BaseCellRenderer { }; get std() { - const host = this.view.contextGet(HostContextKey); + const host = this.view.serviceGet(EditorHostKey); return host?.std; } 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 7b7e01c51..9ee787e47 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 @@ -24,7 +24,7 @@ import { computed, effect, signal } from '@preact/signals-core'; import { ref } from 'lit/directives/ref.js'; import { html } from 'lit/static-html.js'; -import { HostContextKey } from '../../context/host-context.js'; +import { EditorHostKey } from '../../context/host-context.js'; import type { DatabaseBlockComponent } from '../../database-block.js'; import { richTextCellStyle, @@ -87,7 +87,7 @@ export class RichTextCell extends BaseCellRenderer { get inlineManager() { return this.view - .contextGet(HostContextKey) + .serviceGet(EditorHostKey) ?.std.get(DefaultInlineManagerExtension.identifier); } @@ -98,7 +98,7 @@ export class RichTextCell extends BaseCellRenderer { } get host() { - return this.view.contextGet(HostContextKey); + return this.view.serviceGet(EditorHostKey); } private readonly richText$ = signal(); @@ -398,7 +398,7 @@ export class RichTextCell extends BaseCellRenderer { } private get std() { - return this.view.contextGet(HostContextKey)?.std; + return this.view.serviceGet(EditorHostKey)?.std; } insertDelta = (delta: DeltaInsert) => { diff --git a/blocksuite/affine/blocks/database/src/properties/rich-text/define.ts b/blocksuite/affine/blocks/database/src/properties/rich-text/define.ts index 96c1297c2..7df11a61d 100644 --- a/blocksuite/affine/blocks/database/src/properties/rich-text/define.ts +++ b/blocksuite/affine/blocks/database/src/properties/rich-text/define.ts @@ -5,7 +5,7 @@ import { Text } from '@blocksuite/store'; import * as Y from 'yjs'; import zod from 'zod'; -import { HostContextKey } from '../../context/host-context.js'; +import { EditorHostKey } from '../../context/host-context.js'; import { isLinkedDoc } from '../../utils/title-doc.js'; export const richTextColumnType = propertyType('rich-text'); @@ -43,7 +43,7 @@ export const richTextPropertyModelConfig = richTextColumnType.modelConfig({ }, toJson: ({ value, dataSource }) => { if (!value) return null; - const host = dataSource.contextGet(HostContextKey); + const host = dataSource.serviceGet(EditorHostKey); if (host) { const collection = host.std.workspace; const yText = toYText(value); diff --git a/blocksuite/affine/blocks/database/src/properties/title/define.ts b/blocksuite/affine/blocks/database/src/properties/title/define.ts index 6450fae4f..400b5d4a1 100644 --- a/blocksuite/affine/blocks/database/src/properties/title/define.ts +++ b/blocksuite/affine/blocks/database/src/properties/title/define.ts @@ -3,7 +3,7 @@ import { Text } from '@blocksuite/store'; import { Doc } from 'yjs'; import zod from 'zod'; -import { HostContextKey } from '../../context/host-context.js'; +import { EditorHostKey } from '../../context/host-context.js'; import { isLinkedDoc } from '../../utils/title-doc.js'; export const titleColumnType = propertyType('title'); @@ -28,7 +28,7 @@ export const titlePropertyModelConfig = titleColumnType.modelConfig({ }, toJson: ({ value, dataSource }) => { if (!value) return ''; - const host = dataSource.contextGet(HostContextKey); + const host = dataSource.serviceGet(EditorHostKey); if (host) { const collection = host.std.workspace; const deltas = value.deltas$.value; diff --git a/blocksuite/affine/blocks/database/src/properties/title/text.ts b/blocksuite/affine/blocks/database/src/properties/title/text.ts index fbad7f071..67978672f 100644 --- a/blocksuite/affine/blocks/database/src/properties/title/text.ts +++ b/blocksuite/affine/blocks/database/src/properties/title/text.ts @@ -17,7 +17,7 @@ import { property } from 'lit/decorators.js'; import { createRef, ref } from 'lit/directives/ref.js'; import { html } from 'lit/static-html.js'; -import { HostContextKey } from '../../context/host-context.js'; +import { EditorHostKey } from '../../context/host-context.js'; import type { DatabaseBlockComponent } from '../../database-block.js'; import { getSingleDocIdFromText } from '../../utils/title-doc.js'; import { @@ -32,7 +32,7 @@ export class HeaderAreaTextCell extends BaseCellRenderer { docId$ = signal(); get host() { - return this.view.contextGet(HostContextKey); + return this.view.serviceGet(EditorHostKey); } get inlineEditor() { @@ -50,7 +50,7 @@ export class HeaderAreaTextCell extends BaseCellRenderer { } get std() { - return this.view.contextGet(HostContextKey)?.std; + return this.view.serviceGet(EditorHostKey)?.std; } private readonly _onCopy = (e: ClipboardEvent) => { diff --git a/blocksuite/affine/data-view/src/core/data-source/base.ts b/blocksuite/affine/data-view/src/core/data-source/base.ts index 78f1911c3..ac5cd8b08 100644 --- a/blocksuite/affine/data-view/src/core/data-source/base.ts +++ b/blocksuite/affine/data-view/src/core/data-source/base.ts @@ -1,5 +1,11 @@ import type { ColumnDataType } from '@blocksuite/affine-model'; import type { InsertToPosition } from '@blocksuite/affine-shared/utils'; +import { + Container, + createScope, + type GeneralServiceIdentifier, + type ServiceProvider, +} from '@blocksuite/global/di'; import { computed, type ReadonlySignal } from '@preact/signals-core'; import type { TypeInstance } from '../logical/type.js'; @@ -8,7 +14,6 @@ import type { DatabaseFlags } from '../types.js'; import type { ViewConvertConfig } from '../view/convert.js'; import type { DataViewDataType, ViewMeta } from '../view/data-view.js'; import type { ViewManager } from '../view-manager/view-manager.js'; -import type { DataViewContextKey } from './context.js'; export interface DataSource { readonly$: ReadonlySignal; @@ -65,7 +70,9 @@ export interface DataSource { propertyDelete(id: string): void; propertyCanDelete(propertyId: string): boolean; - contextGet(key: DataViewContextKey): T; + provider: ServiceProvider; + serviceGet(key: GeneralServiceIdentifier): T | null; + serviceGetOrCreate(key: GeneralServiceIdentifier, create: () => T): T; viewConverts: ViewConvertConfig[]; viewManager: ViewManager; @@ -91,6 +98,8 @@ export interface DataSource { viewMetaGetById$(viewId: string): ReadonlySignal; } +export const DataSourceScope = createScope('data-source'); + export abstract class DataSourceBase implements DataSource { propertyTypeCanSet(propertyId: string): boolean { return !this.isFixedProperty(propertyId); @@ -101,7 +110,9 @@ export abstract class DataSourceBase implements DataSource { propertyCanDelete(propertyId: string): boolean { return !this.isFixedProperty(propertyId); } - context = new Map(); + protected container = new Container(); + + abstract get parentProvider(): ServiceProvider; abstract featureFlags$: ReadonlySignal; @@ -144,12 +155,26 @@ export abstract class DataSourceBase implements DataSource { return computed(() => this.cellValueGet(rowId, propertyId)); } - contextGet(key: DataViewContextKey): T { - return (this.context.get(key.key) as T) ?? key.defaultValue; + get provider() { + return this.container.provider(DataSourceScope, this.parentProvider); } - contextSet(key: DataViewContextKey, value: T): void { - this.context.set(key.key, value); + serviceGet(key: GeneralServiceIdentifier): T | null { + return this.provider.getOptional(key); + } + + serviceSet(key: GeneralServiceIdentifier, value: T): void { + this.container.addValue(key, value, { scope: DataSourceScope }); + } + + serviceGetOrCreate(key: GeneralServiceIdentifier, create: () => T): T { + const result = this.serviceGet(key); + if (result != null) { + return result; + } + const value = create(); + this.serviceSet(key, value); + return value; } abstract propertyAdd( diff --git a/blocksuite/affine/data-view/src/core/data-source/context.ts b/blocksuite/affine/data-view/src/core/data-source/context.ts deleted file mode 100644 index f81b56973..000000000 --- a/blocksuite/affine/data-view/src/core/data-source/context.ts +++ /dev/null @@ -1,12 +0,0 @@ -export interface DataViewContextKey { - key: symbol; - defaultValue: T; -} - -export const createContextKey = ( - name: string, - defaultValue: T -): DataViewContextKey => ({ - key: Symbol(name), - defaultValue, -}); diff --git a/blocksuite/affine/data-view/src/core/data-source/index.ts b/blocksuite/affine/data-view/src/core/data-source/index.ts index ffe27a828..ab44c4589 100644 --- a/blocksuite/affine/data-view/src/core/data-source/index.ts +++ b/blocksuite/affine/data-view/src/core/data-source/index.ts @@ -1,2 +1 @@ export * from './base.js'; -export * from './context.js'; diff --git a/blocksuite/affine/data-view/src/core/group-by/default.ts b/blocksuite/affine/data-view/src/core/group-by/default.ts index 97a74f7e0..6e5268531 100644 --- a/blocksuite/affine/data-view/src/core/group-by/default.ts +++ b/blocksuite/affine/data-view/src/core/group-by/default.ts @@ -1,7 +1,7 @@ import type { GroupBy } from '../common/types.js'; import type { DataSource } from '../data-source/index.js'; import type { PropertyMetaConfig } from '../property/property-config.js'; -import { groupByMatcher } from './matcher.js'; +import { getGroupByService } from './matcher.js'; export const defaultGroupBy = ( dataSource: DataSource, @@ -9,7 +9,8 @@ export const defaultGroupBy = ( propertyId: string, data: NonNullable ): GroupBy | undefined => { - const name = groupByMatcher.match( + const groupByService = getGroupByService(dataSource); + const name = groupByService?.matcher.match( propertyMeta.config.jsonValue.type({ data, dataSource }) )?.name; return name != null diff --git a/blocksuite/affine/data-view/src/core/group-by/define.ts b/blocksuite/affine/data-view/src/core/group-by/define.ts index 60a8d0138..a72c54113 100644 --- a/blocksuite/affine/data-view/src/core/group-by/define.ts +++ b/blocksuite/affine/data-view/src/core/group-by/define.ts @@ -1,6 +1,6 @@ import hash from '@emotion/hash'; -import { MatcherCreator } from '../logical/matcher.js'; +import type { TypeInstance } from '../logical/type.js'; import { t } from '../logical/type-presets.js'; import { createUniComponentFromWebComponent } from '../utils/uni-component/uni-component.js'; import { BooleanGroupView } from './renderer/boolean-group.js'; @@ -8,15 +8,23 @@ import { NumberGroupView } from './renderer/number-group.js'; import { SelectGroupView } from './renderer/select-group.js'; import { StringGroupView } from './renderer/string-group.js'; import type { GroupByConfig } from './types.js'; - -const groupByMatcherCreator = new MatcherCreator(); -const ungroups = { +export const createGroupByConfig = < + Data extends Record, + MatchType extends TypeInstance, + GroupValue = unknown, +>( + config: GroupByConfig +): GroupByConfig => { + return config as never as GroupByConfig; +}; +export const ungroups = { key: 'Ungroups', value: null, }; export const groupByMatchers = [ - groupByMatcherCreator.createMatcher(t.tag.instance(), { + createGroupByConfig({ name: 'select', + matchType: t.tag.instance(), groupName: (type, value) => { if (t.tag.is(type) && type.data) { return type.data.find(v => v.id === value)?.value ?? ''; @@ -48,11 +56,12 @@ export const groupByMatchers = [ }, view: createUniComponentFromWebComponent(SelectGroupView), }), - groupByMatcherCreator.createMatcher(t.array.instance(t.tag.instance()), { + createGroupByConfig({ name: 'multi-select', - groupName: (type, value) => { - if (t.tag.is(type) && type.data) { - return type.data.find(v => v.id === value)?.value ?? ''; + matchType: t.array.instance(t.tag.instance()), + groupName: (type, value: string | null) => { + if (t.array.is(type) && t.tag.is(type.element) && type.element.data) { + return type.element.data.find(v => v.id === value)?.value ?? ''; } return ''; }, @@ -94,8 +103,9 @@ export const groupByMatchers = [ }, view: createUniComponentFromWebComponent(SelectGroupView), }), - groupByMatcherCreator.createMatcher(t.string.instance(), { + createGroupByConfig({ name: 'text', + matchType: t.string.instance(), groupName: (_type, value) => { return `${value ?? ''}`; }, @@ -115,15 +125,16 @@ export const groupByMatchers = [ }, view: createUniComponentFromWebComponent(StringGroupView), }), - groupByMatcherCreator.createMatcher(t.number.instance(), { + createGroupByConfig({ name: 'number', - groupName: (_type, value) => { + matchType: t.number.instance(), + groupName: (_type, value: number | null) => { return `${value ?? ''}`; }, defaultKeys: _type => { return [ungroups]; }, - valuesGroup: (value, _type) => { + valuesGroup: (value: number | null, _type) => { if (typeof value !== 'number') { return [ungroups]; } @@ -137,8 +148,9 @@ export const groupByMatchers = [ addToGroup: value => (typeof value === 'number' ? value * 10 : null), view: createUniComponentFromWebComponent(NumberGroupView), }), - groupByMatcherCreator.createMatcher(t.boolean.instance(), { + createGroupByConfig({ name: 'boolean', + matchType: t.boolean.instance(), groupName: (_type, value) => { return `${value?.toString() ?? ''}`; }, 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 b4da888d2..9e58bed86 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 @@ -5,10 +5,10 @@ import { nothing } from 'lit'; import { html } from 'lit/static-html.js'; import { renderUniLit } from '../utils/uni-component/uni-component.js'; -import type { GroupData } from './trait.js'; +import type { Group } from './trait.js'; import type { GroupRenderProps } from './types.js'; -function GroupHeaderCount(group: GroupData) { +function GroupHeaderCount(group: Group) { const cards = group.rows; if (!cards.length) { return; @@ -16,32 +16,25 @@ function GroupHeaderCount(group: GroupData) { return html`
${cards.length}
`; } const GroupTitleMobile = ( - groupData: GroupData, + groupData: Group, ops: { readonly: boolean; clickAdd: (evt: MouseEvent) => void; clickOps: (evt: MouseEvent) => void; } ) => { - const data = groupData.manager.config$.value; - if (!data) return nothing; + const type = groupData.tType; + if (!type) return nothing; const icon = groupData.value == null ? '' : html` `; const props: GroupRenderProps = { - value: groupData.value, - data: groupData.property.data$.value, - updateData: groupData.manager.updateData, - updateValue: value => - groupData.manager.updateValue( - groupData.rows.map(row => row.rowId), - value - ), + group: groupData, readonly: ops.readonly, }; @@ -103,7 +96,7 @@ const GroupTitleMobile = (
- ${icon} ${renderUniLit(data.view, props)} ${columnName} + ${icon} ${renderUniLit(groupData.view, props)} ${columnName} ${GroupHeaderCount(groupData)}
${ops.readonly @@ -120,7 +113,7 @@ const GroupTitleMobile = ( }; export const GroupTitle = ( - groupData: GroupData, + groupData: Group, ops: { readonly: boolean; clickAdd: (evt: MouseEvent) => void; @@ -130,25 +123,18 @@ export const GroupTitle = ( if (IS_MOBILE) { return GroupTitleMobile(groupData, ops); } - const data = groupData.manager.config$.value; - if (!data) return nothing; + const type = groupData.tType; + if (!type) return nothing; const icon = groupData.value == null ? '' : html` `; const props: GroupRenderProps = { - value: groupData.value, - data: groupData.property.data$.value, - updateData: groupData.manager.updateData, - updateValue: value => - groupData.manager.updateValue( - groupData.rows.map(row => row.rowId), - value - ), + group: groupData, readonly: ops.readonly, }; @@ -228,7 +214,7 @@ export const GroupTitle = (
- ${icon} ${renderUniLit(data.view, props)} ${columnName} + ${icon} ${renderUniLit(groupData.view, props)} ${columnName} ${GroupHeaderCount(groupData)}
${ops.readonly diff --git a/blocksuite/affine/data-view/src/core/group-by/index.ts b/blocksuite/affine/data-view/src/core/group-by/index.ts index 5c36099e4..15042294f 100644 --- a/blocksuite/affine/data-view/src/core/group-by/index.ts +++ b/blocksuite/affine/data-view/src/core/group-by/index.ts @@ -1 +1,3 @@ +export * from './define.js'; +export * from './matcher.js'; export * from './trait.js'; diff --git a/blocksuite/affine/data-view/src/core/group-by/matcher.ts b/blocksuite/affine/data-view/src/core/group-by/matcher.ts index 7b3cd536e..205e242fb 100644 --- a/blocksuite/affine/data-view/src/core/group-by/matcher.ts +++ b/blocksuite/affine/data-view/src/core/group-by/matcher.ts @@ -1,5 +1,41 @@ -import { Matcher } from '../logical/matcher.js'; +import { createIdentifier } from '@blocksuite/global/di'; + +import type { DataSource } from '../data-source/base.js'; +import { Matcher_ } from '../logical/matcher.js'; import { groupByMatchers } from './define.js'; import type { GroupByConfig } from './types.js'; -export const groupByMatcher = new Matcher(groupByMatchers); +export const createGroupByMatcher = (list: GroupByConfig[]) => { + return new Matcher_(list, v => v.matchType); +}; + +export class GroupByService { + constructor(private readonly dataSource: DataSource) {} + + allExternalGroupByConfig(): GroupByConfig[] { + return Array.from( + this.dataSource.provider.getAll(ExternalGroupByConfigProvider).values() + ); + } + + get matcher() { + return createGroupByMatcher([ + ...this.allExternalGroupByConfig(), + ...groupByMatchers, + ]); + } +} + +export const GroupByProvider = + createIdentifier('group-by-service'); + +export const getGroupByService = (dataSource: DataSource) => { + return dataSource.serviceGetOrCreate( + GroupByProvider, + () => new GroupByService(dataSource) + ); +}; + +export const ExternalGroupByConfigProvider = createIdentifier( + 'external-group-by-config' +); diff --git a/blocksuite/affine/data-view/src/core/group-by/renderer/base.ts b/blocksuite/affine/data-view/src/core/group-by/renderer/base.ts index 80bd84ebe..d73ad16cb 100644 --- a/blocksuite/affine/data-view/src/core/group-by/renderer/base.ts +++ b/blocksuite/affine/data-view/src/core/group-by/renderer/base.ts @@ -2,24 +2,39 @@ import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit'; import { ShadowlessElement } from '@blocksuite/std'; import { property } from 'lit/decorators.js'; +import type { Group } from '../trait.js'; import type { GroupRenderProps } from '../types.js'; -export class BaseGroup, Value> +export class BaseGroup> extends SignalWatcher(WithDisposable(ShadowlessElement)) - implements GroupRenderProps + implements GroupRenderProps { @property({ attribute: false }) - accessor data!: Data; + accessor group!: Group; @property({ attribute: false }) accessor readonly!: boolean; - @property({ attribute: false }) - accessor updateData: ((data: Data) => void) | undefined = undefined; + updateData(data: Data) { + this.group.manager.updateData(data); + } - @property({ attribute: false }) - accessor updateValue: ((value: Value) => void) | undefined = undefined; + updateValue(value: JsonValue) { + this.group.manager.updateValue( + this.group.rows.map(row => row.rowId), + value + ); + } - @property({ attribute: false }) - accessor value!: Value; + get value(): JsonValue { + return this.group.value as JsonValue; + } + + get type() { + return this.group.tType; + } + + get data() { + return this.group.property.data$.value; + } } diff --git a/blocksuite/affine/data-view/src/core/group-by/renderer/boolean-group.ts b/blocksuite/affine/data-view/src/core/group-by/renderer/boolean-group.ts index 4090bbb53..201cc7a9b 100644 --- a/blocksuite/affine/data-view/src/core/group-by/renderer/boolean-group.ts +++ b/blocksuite/affine/data-view/src/core/group-by/renderer/boolean-group.ts @@ -3,7 +3,7 @@ import { css, html } from 'lit'; import { BaseGroup } from './base.js'; -export class BooleanGroupView extends BaseGroup, boolean> { +export class BooleanGroupView extends BaseGroup> { static override styles = css` .data-view-group-title-boolean-view { display: flex; diff --git a/blocksuite/affine/data-view/src/core/group-by/renderer/number-group.ts b/blocksuite/affine/data-view/src/core/group-by/renderer/number-group.ts index be9f840eb..ae946fa3c 100644 --- a/blocksuite/affine/data-view/src/core/group-by/renderer/number-group.ts +++ b/blocksuite/affine/data-view/src/core/group-by/renderer/number-group.ts @@ -7,7 +7,7 @@ import { css, html } from 'lit'; import { BaseGroup } from './base.js'; -export class NumberGroupView extends BaseGroup, number> { +export class NumberGroupView extends BaseGroup> { static override styles = css` .data-view-group-title-number-view { border-radius: 8px; diff --git a/blocksuite/affine/data-view/src/core/group-by/renderer/select-group.ts b/blocksuite/affine/data-view/src/core/group-by/renderer/select-group.ts index b4200045d..d5a7979af 100644 --- a/blocksuite/affine/data-view/src/core/group-by/renderer/select-group.ts +++ b/blocksuite/affine/data-view/src/core/group-by/renderer/select-group.ts @@ -12,10 +12,10 @@ import type { SelectTag } from '../../logical/index.js'; import { BaseGroup } from './base.js'; export class SelectGroupView extends BaseGroup< + string, { options: SelectTag[]; - }, - string + } > { static override styles = css` data-view-group-title-select-view { diff --git a/blocksuite/affine/data-view/src/core/group-by/renderer/string-group.ts b/blocksuite/affine/data-view/src/core/group-by/renderer/string-group.ts index cb39355c3..1114d87c9 100644 --- a/blocksuite/affine/data-view/src/core/group-by/renderer/string-group.ts +++ b/blocksuite/affine/data-view/src/core/group-by/renderer/string-group.ts @@ -7,7 +7,7 @@ import { css, html } from 'lit'; import { BaseGroup } from './base.js'; -export class StringGroupView extends BaseGroup, string> { +export class StringGroupView extends BaseGroup> { static override styles = css` .data-view-group-title-string-view { border-radius: 8px; 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 951e313a7..d8a47288e 100644 --- a/blocksuite/affine/data-view/src/core/group-by/setting.ts +++ b/blocksuite/affine/data-view/src/core/group-by/setting.ts @@ -24,7 +24,7 @@ import { sortable, } from '../utils/wc-dnd/sort/sort-context.js'; import { verticalListSortingStrategy } from '../utils/wc-dnd/sort/strategies/index.js'; -import { groupByMatcher } from './matcher.js'; +import { getGroupByService } from './matcher.js'; import type { GroupTrait } from './trait.js'; import type { GroupRenderProps } from './types.js'; @@ -142,21 +142,22 @@ export class GroupSetting extends SignalWatcher( groups, group => group?.key ?? 'default key', group => { - if (!group) return; + const type = group.property.dataType$.value; + if (!type) return; const props: GroupRenderProps = { - value: group.value, - data: group.property.data$.value, + group, readonly: true, }; - const config = group.manager.config$.value; return html`
-
- ${renderUniLit(config?.view, props)} +
+ ${renderUniLit(group.view, props)}
@@ -198,7 +199,8 @@ export const selectGroupByProperty = ( if (!dataType) { return false; } - return !!groupByMatcher.match(dataType); + const groupByService = getGroupByService(view.manager.dataSource); + return !!groupByService?.matcher.match(dataType); }) .map(property => { return menu.action({ 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 43acac007..2758cd09c 100644 --- a/blocksuite/affine/data-view/src/core/group-by/trait.ts +++ b/blocksuite/affine/data-view/src/core/group-by/trait.ts @@ -12,89 +12,112 @@ 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; - key: string; - name: string; - type: TypeInstance; - value: unknown; - rows: Row[]; +import { getGroupByService } from './matcher.js'; +import type { GroupByConfig } from './types.js'; + +export type GroupInfo< + RawValue = unknown, + JsonValue = unknown, + Data extends Record = Record, +> = { + config: GroupByConfig; + property: Property; + tType: TypeInstance; }; +export class Group< + RawValue = unknown, + JsonValue = unknown, + Data extends Record = Record, +> { + rows: Row[] = []; + constructor( + public readonly key: string, + public readonly value: JsonValue, + private readonly groupInfo: GroupInfo, + public readonly manager: GroupTrait + ) {} + + get property() { + return this.groupInfo.property; + } + + name$ = computed(() => { + const type = this.property.dataType$.value; + if (!type) { + return ''; + } + return this.groupInfo.config.groupName(type, this.value); + }); + + private get config() { + return this.groupInfo.config; + } + + get tType() { + return this.groupInfo.tType; + } + get view() { + return this.config.view; + } +} + export class GroupTrait { - config$ = computed(() => { + groupInfo$ = computed(() => { const groupBy = this.groupBy$.value; if (!groupBy) { return; } - const result = groupByMatcher.find(v => v.data.name === groupBy.name); + const property = this.view.propertyGetOrCreate(groupBy.columnId); + if (!property) { + return; + } + const tType = property.dataType$.value; + if (!tType) { + return; + } + const groupByService = getGroupByService(this.view.manager.dataSource); + const result = groupByService?.matcher.match(tType); if (!result) { return; } - return result.data; + return { + config: result, + property, + tType: tType, + }; }); - property$ = computed(() => { - const groupBy = this.groupBy$.value; - if (!groupBy) { + staticInfo$ = computed(() => { + const groupInfo = this.groupInfo$.value; + if (!groupInfo) { return; } - return this.view.propertyGetOrCreate(groupBy.columnId); - }); - - staticGroupDataMap$ = computed< - Record> | undefined - >(() => { - const config = this.config$.value; - const property = this.property$.value; - const tType = property?.dataType$.value; - if (!config || !tType || !property) { - return; - } - return Object.fromEntries( - config.defaultKeys(tType).map(({ key, value }) => [ - key, - { - key, - property, - name: config.groupName(tType, value), - manager: this, - type: tType, - value, - }, - ]) + const staticMap = Object.fromEntries( + groupInfo.config + .defaultKeys(groupInfo.tType) + .map(({ key, value }) => [key, new Group(key, value, groupInfo, this)]) ); + return { + staticMap, + groupInfo, + }; }); - groupDataMap$ = computed | undefined>(() => { - const staticGroupMap = this.staticGroupDataMap$.value; - const config = this.config$.value; - const groupBy = this.groupBy$.value; - const property = this.property$.value; - const tType = property?.dataType$.value; - if (!staticGroupMap || !config || !groupBy || !tType || !property) { + groupDataMap$ = computed(() => { + const staticInfo = this.staticInfo$.value; + if (!staticInfo) { return; } - const groupMap: Record = Object.fromEntries( - Object.entries(staticGroupMap).map(([k, v]) => [k, { ...v, rows: [] }]) - ); + const { staticMap, groupInfo } = staticInfo; + const groupMap: Record = { ...staticMap }; this.view.rows$.value.forEach(row => { - const value = this.view.cellGetOrCreate(row.rowId, groupBy.columnId) + const value = this.view.cellGetOrCreate(row.rowId, groupInfo.property.id) .jsonValue$.value; - const keys = config.valuesGroup(value, tType); + const keys = groupInfo.config.valuesGroup(value, groupInfo.tType); keys.forEach(({ key, value }) => { if (!groupMap[key]) { - groupMap[key] = { - key, - property: property, - name: config.groupName(tType, value), - manager: this, - value, - rows: [], - type: tType, - }; + groupMap[key] = new Group(key, value, groupInfo, this); } groupMap[key].rows.push(row); }); @@ -115,30 +138,30 @@ export class GroupTrait { }); return sortedGroup .map(key => groupMap[key]) - .filter((v): v is GroupData => v != null); + .filter((v): v is Group => v != null); }), this.view.isLocked$ ); updateData = (data: NonNullable) => { - const propertyId = this.propertyId; - if (!propertyId) { + const property = this.property$.value; + if (!property) { return; } - this.view.propertyGetOrCreate(propertyId).dataUpdate(() => data); + this.view.propertyGetOrCreate(property.id).dataUpdate(() => data); }; get addGroup() { - const type = this.property$.value?.type$.value; - if (!type) { - return; - } - return this.view.manager.dataSource.propertyMetaGet(type)?.config.addGroup; + return this.property$.value?.meta$.value?.config.addGroup; } - get propertyId() { - return this.groupBy$.value?.columnId; - } + property$ = computed(() => { + const groupInfo = this.groupInfo$.value; + if (!groupInfo) { + return; + } + return groupInfo.property; + }); constructor( private readonly groupBy$: ReadonlySignal, @@ -158,18 +181,20 @@ export class GroupTrait { addToGroup(rowId: string, key: string) { const groupMap = this.groupDataMap$.value; - const propertyId = this.propertyId; - if (!groupMap || !propertyId) { + const groupInfo = this.groupInfo$.value; + if (!groupMap || !groupInfo) { return; } - const addTo = this.config$.value?.addToGroup ?? (value => value); + const addTo = groupInfo.config.addToGroup ?? (value => value); const v = groupMap[key]?.value; if (v != null) { const newValue = addTo( v, - this.view.cellGetOrCreate(rowId, propertyId).jsonValue$.value + this.view.cellGetOrCreate(rowId, groupInfo.property.id).jsonValue$.value ); - this.view.cellGetOrCreate(rowId, propertyId).valueSet(newValue); + this.view + .cellGetOrCreate(rowId, groupInfo.property.id) + .valueSet(newValue); } } @@ -229,11 +254,12 @@ export class GroupTrait { return; } if (fromGroupKey !== toGroupKey) { - const propertyId = this.propertyId; + const propertyId = this.property$.value?.id; if (!propertyId) { return; } - const remove = this.config$.value?.removeFromGroup ?? (() => null); + const remove = + this.groupInfo$.value?.config.removeFromGroup ?? (() => null); const group = fromGroupKey != null ? groupMap[fromGroupKey] : undefined; let newValue: unknown = null; if (group) { @@ -242,7 +268,8 @@ export class GroupTrait { this.view.cellGetOrCreate(rowId, propertyId).jsonValue$.value ); } - const addTo = this.config$.value?.addToGroup ?? (value => value); + const addTo = + this.groupInfo$.value?.config.addToGroup ?? (value => value); newValue = addTo(groupMap[toGroupKey]?.value ?? null, newValue); this.view.cellGetOrCreate(rowId, propertyId).jsonValueSet(newValue); } @@ -275,11 +302,12 @@ export class GroupTrait { if (!groupMap) { return; } - const propertyId = this.propertyId; + const propertyId = this.property$.value?.id; if (!propertyId) { return; } - const remove = this.config$.value?.removeFromGroup ?? (() => undefined); + const remove = + this.groupInfo$.value?.config.removeFromGroup ?? (() => undefined); const newValue = remove( groupMap[key]?.value ?? null, this.view.cellGetOrCreate(rowId, propertyId).jsonValue$.value @@ -288,7 +316,7 @@ export class GroupTrait { } updateValue(rows: string[], value: unknown) { - const propertyId = this.propertyId; + const propertyId = this.property$.value?.id; if (!propertyId) { return; } diff --git a/blocksuite/affine/data-view/src/core/group-by/types.ts b/blocksuite/affine/data-view/src/core/group-by/types.ts index 3af11396f..906736793 100644 --- a/blocksuite/affine/data-view/src/core/group-by/types.ts +++ b/blocksuite/affine/data-view/src/core/group-by/types.ts @@ -1,35 +1,41 @@ import type { UniComponent } from '@blocksuite/affine-shared/types'; -import type { TypeInstance } from '../logical/type.js'; +import type { TypeInstance, ValueTypeOf } from '../logical/type.js'; +import type { Group } from './trait.js'; export interface GroupRenderProps< - Data extends NonNullable = NonNullable, JsonValue = unknown, + Data extends Record = Record, > { - data: Data; - updateData?: (data: Data) => void; - value: JsonValue; - updateValue?: (value: JsonValue) => void; + group: Group; readonly: boolean; } export type GroupByConfig< - JsonValue = unknown, Data extends NonNullable = NonNullable, + MatchType extends TypeInstance = TypeInstance, + GroupValue = unknown, > = { name: string; - groupName: (type: TypeInstance, value: unknown) => string; - defaultKeys: (type: TypeInstance) => { + matchType: MatchType; + groupName: (type: MatchType, value: GroupValue | null) => string; + defaultKeys: (type: MatchType) => { key: string; - value: JsonValue; + value: GroupValue | null; }[]; valuesGroup: ( - value: unknown, - type: TypeInstance + value: ValueTypeOf | null, + type: MatchType ) => { key: string; - value: JsonValue; + value: GroupValue | null; }[]; - addToGroup?: (value: JsonValue, oldValue: JsonValue) => JsonValue; - removeFromGroup?: (value: JsonValue, oldValue: JsonValue) => JsonValue; - view: UniComponent>; + addToGroup?: ( + value: GroupValue | null, + oldValue: ValueTypeOf | null + ) => ValueTypeOf | null; + removeFromGroup?: ( + value: GroupValue | null, + oldValue: ValueTypeOf | null + ) => ValueTypeOf | null; + view: UniComponent>; }; diff --git a/blocksuite/affine/data-view/src/core/logical/matcher.ts b/blocksuite/affine/data-view/src/core/logical/matcher.ts index 5c1db06ba..e24c7d3f1 100644 --- a/blocksuite/affine/data-view/src/core/logical/matcher.ts +++ b/blocksuite/affine/data-view/src/core/logical/matcher.ts @@ -68,3 +68,46 @@ export class Matcher { return; } } + +export class Matcher_ { + constructor( + private readonly list: Value[], + private readonly getType: (value: Value) => Type, + private readonly matchFunc: ( + type: Type, + target: TypeInstance + ) => boolean = (type, target) => typeSystem.unify(target, type) + ) {} + all(): Value[] { + return this.list; + } + + allMatched(type: TypeInstance): Value[] { + const result: Value[] = []; + for (const t of this.list) { + const tType = this.getType(t); + if (this.matchFunc(tType, type)) { + result.push(t); + } + } + return result; + } + + find(f: (data: Value) => boolean): Value | undefined { + return this.list.find(f); + } + + isMatched(type: Type, target: TypeInstance) { + return this.matchFunc(type, target); + } + + match(type: TypeInstance) { + for (const t of this.list) { + const tType = this.getType(t); + if (this.matchFunc(tType, type)) { + return t; + } + } + return; + } +} diff --git a/blocksuite/affine/data-view/src/core/logical/type-presets.ts b/blocksuite/affine/data-view/src/core/logical/type-presets.ts index 3e3188f21..6b80408d4 100644 --- a/blocksuite/affine/data-view/src/core/logical/type-presets.ts +++ b/blocksuite/affine/data-view/src/core/logical/type-presets.ts @@ -1,3 +1,7 @@ +import type { + UserListService, + UserService, +} from '@blocksuite/affine-shared/services'; import * as zod from 'zod'; import Zod from 'zod'; @@ -12,6 +16,11 @@ export const SelectTagSchema = Zod.object({ color: Zod.string(), value: Zod.string(), }); +export const UserInfoSchema = Zod.object({ + userService: Zod.custom(() => true), + userListService: Zod.custom(() => true), +}); +export type UserInfo = Zod.TypeOf; export const unknown = defineDataType('Unknown', zod.never(), zod.unknown()); export const dt = { number: defineDataType('Number', zod.number(), zod.number()), @@ -22,6 +31,7 @@ export const dt = { url: defineDataType('URL', zod.string(), zod.string()), image: defineDataType('Image', zod.string(), zod.string()), tag: defineDataType('Tag', zod.array(SelectTagSchema), zod.string()), + user: defineDataType('User', UserInfoSchema, zod.string()), }; export const t = { unknown, @@ -53,4 +63,5 @@ export const converts: TypeConvertConfig[] = [ ), createTypeConvert(t.richText.instance(), t.string.instance(), value => value), createTypeConvert(t.url.instance(), t.string.instance(), value => value), + createTypeConvert(t.user.instance(), t.string.instance(), value => value), ]; 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 aa464dd8a..593f1fd5b 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,7 +1,7 @@ import type { InsertToPosition } from '@blocksuite/affine-shared/utils'; +import type { GeneralServiceIdentifier } from '@blocksuite/global/di'; 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 { PropertyMetaConfig } from '../property/property-config.js'; import type { TraitKey } from '../traits/key.js'; @@ -61,7 +61,8 @@ export interface SingleView { type?: string ): string | undefined; - contextGet(key: DataViewContextKey): T; + serviceGet(key: GeneralServiceIdentifier): T | null; + serviceGetOrCreate(key: GeneralServiceIdentifier, create: () => T): T; traitGet(key: TraitKey): T | undefined; @@ -201,8 +202,12 @@ export abstract class SingleViewBase< return new CellBase(this, propertyId, rowId); } - contextGet(key: DataViewContextKey): T { - return this.dataSource.contextGet(key); + serviceGet(key: GeneralServiceIdentifier): T | null { + return this.dataSource.serviceGet(key); + } + + serviceGetOrCreate(key: GeneralServiceIdentifier, create: () => T): T { + return this.dataSource.serviceGetOrCreate(key, create); } dataUpdate(updater: (viewData: ViewData) => Partial): void { 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 d0cb16d62..7a35e9333 100644 --- a/blocksuite/affine/data-view/src/view-presets/kanban/define.ts +++ b/blocksuite/affine/data-view/src/view-presets/kanban/define.ts @@ -2,7 +2,7 @@ import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions'; import type { GroupBy, GroupProperty } from '../../core/common/types.js'; import type { FilterGroup } from '../../core/filter/types.js'; -import { defaultGroupBy, groupByMatcher, t } from '../../core/index.js'; +import { defaultGroupBy, getGroupByService, t } from '../../core/index.js'; import type { Sort } from '../../core/sort/types.js'; import { type BasicViewDataType, viewType } from '../../core/view/data-view.js'; import { KanbanSingleView } from './kanban-view-manager.js'; @@ -34,10 +34,11 @@ export const kanbanViewModel = kanbanViewType.createModel({ defaultName: 'Kanban View', dataViewManager: KanbanSingleView, defaultData: viewManager => { + const groupByService = getGroupByService(viewManager.dataSource); const columns = viewManager.dataSource.properties$.value; const allowList = columns.filter(columnId => { const dataType = viewManager.dataSource.propertyDataTypeGet(columnId); - return dataType && !!groupByMatcher.match(dataType); + return dataType && !!groupByService?.matcher.match(dataType); }); const getWeight = (columnId: string) => { const dataType = viewManager.dataSource.propertyDataTypeGet(columnId); 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 b6b4f4f6c..72ce91be9 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 @@ -13,7 +13,7 @@ import { html } from 'lit/static-html.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 { Group } from '../../../core/group-by/trait.js'; import { dragHandler } from '../../../core/utils/wc-dnd/dnd-context.js'; import type { KanbanSingleView } from '../kanban-view-manager.js'; @@ -137,7 +137,7 @@ export class MobileKanbanGroup extends SignalWatcher( accessor dataViewEle!: DataViewRenderer; @property({ attribute: false }) - accessor group!: GroupData; + accessor group!: Group; @property({ attribute: false }) accessor view!: KanbanSingleView; 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 7b84d44e1..8bee34005 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 @@ -55,7 +55,7 @@ export const popCardMenu = ( }) .map(group => { return menu.action({ - name: group.value != null ? group.name : 'Ungroup', + name: group.value != null ? group.name$.value : 'Ungroup', select: () => { groupTrait.moveCardTo( cardId, 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 597dd4b9b..6024101ec 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 @@ -13,7 +13,7 @@ import { html } from 'lit/static-html.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 { Group } from '../../../core/group-by/trait.js'; import { dragHandler } from '../../../core/utils/wc-dnd/dnd-context.js'; import type { KanbanSingleView } from '../kanban-view-manager.js'; @@ -201,7 +201,7 @@ export class KanbanGroup extends SignalWatcher( accessor dataViewEle!: DataViewRenderer; @property({ attribute: false }) - accessor group!: GroupData; + accessor group!: Group; @property({ attribute: false }) accessor view!: KanbanSingleView; diff --git a/blocksuite/affine/data-view/src/view-presets/kanban/pc/menu.ts b/blocksuite/affine/data-view/src/view-presets/kanban/pc/menu.ts index 743b8e4cb..dac06f91f 100644 --- a/blocksuite/affine/data-view/src/view-presets/kanban/pc/menu.ts +++ b/blocksuite/affine/data-view/src/view-presets/kanban/pc/menu.ts @@ -60,7 +60,7 @@ export const popCardMenu = ( }) .map(group => { return menu.action({ - name: group.value != null ? group.name : 'Ungroup', + name: group.value != null ? group.name$.value : 'Ungroup', select: () => { selection.moveCard(rowId, group.key); }, 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 fab4df083..4b4f9f3a0 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 @@ -13,7 +13,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 { Group } 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'; @@ -185,7 +185,7 @@ export class MobileTableGroup extends SignalWatcher( accessor dataViewEle!: DataViewRenderer; @property({ attribute: false }) - accessor group: GroupData | undefined = undefined; + accessor group: Group | undefined = undefined; @query('.affine-database-block-rows') accessor rowsContainer: HTMLElement | null = null; diff --git a/blocksuite/affine/data-view/src/view-presets/table/pc-virtual/group/bottom/stats/column-stats-bar.ts b/blocksuite/affine/data-view/src/view-presets/table/pc-virtual/group/bottom/stats/column-stats-bar.ts index bfe156218..eb361cb4f 100644 --- a/blocksuite/affine/data-view/src/view-presets/table/pc-virtual/group/bottom/stats/column-stats-bar.ts +++ b/blocksuite/affine/data-view/src/view-presets/table/pc-virtual/group/bottom/stats/column-stats-bar.ts @@ -4,7 +4,7 @@ import { css, html } from 'lit'; import { property } from 'lit/decorators.js'; import { repeat } from 'lit/directives/repeat.js'; -import type { GroupData } from '../../../../../../core/group-by/trait'; +import type { Group } from '../../../../../../core/group-by/trait'; import { LEFT_TOOL_BAR_WIDTH, STATS_BAR_HEIGHT } from '../../../../consts'; import type { TableSingleView } from '../../../../table-view-manager'; @@ -38,7 +38,7 @@ export class VirtualDataBaseColumnStats extends SignalWatcher( } @property({ attribute: false }) - accessor group: GroupData | undefined = undefined; + accessor group: Group | undefined = undefined; @property({ attribute: false }) accessor view!: TableSingleView; 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 f74faf9de..0eb1e74dc 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 @@ -15,7 +15,7 @@ import { property } from 'lit/decorators.js'; import { styleMap } from 'lit/directives/style-map.js'; import { typeSystem } from '../../../../../../core'; -import type { GroupData } from '../../../../../../core/group-by/trait'; +import type { Group } from '../../../../../../core/group-by/trait'; import { statsFunctions } from '../../../../../../core/statistics'; import type { StatisticsConfig } from '../../../../../../core/statistics/types'; import type { TableProperty } from '../../../../table-view-manager'; @@ -236,7 +236,7 @@ export class VirtualDatabaseColumnStatsCell extends SignalWatcher( } @property({ attribute: false }) - accessor group: GroupData | undefined = undefined; + accessor group: Group | undefined = undefined; } declare global { 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 0712e9c4f..3bf8c5ccd 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 @@ -4,7 +4,7 @@ import { nothing } from 'lit'; import { html } from 'lit/static-html.js'; import { - type GroupData, + type Group, type GroupRenderProps, renderUniLit, } from '../../../../../core'; @@ -18,7 +18,7 @@ import { show, } from './group-title-css'; -function GroupHeaderCount(group: GroupData) { +function GroupHeaderCount(group: Group) { const cards = group.rows; if (!cards.length) { return; @@ -27,7 +27,7 @@ function GroupHeaderCount(group: GroupData) { } export const GroupTitle = ( - groupData: GroupData, + groupData: Group, ops: { groupHover: boolean; readonly: boolean; @@ -35,24 +35,20 @@ export const GroupTitle = ( clickOps: (evt: MouseEvent) => void; } ) => { - const data = groupData.manager.config$.value; - if (!data) return nothing; + const view = groupData.view; + const type = groupData.property.dataType$.value; + if (!view || !type) { + return nothing; + } const icon = groupData.value == null ? '' : html` `; const props: GroupRenderProps = { - value: groupData.value, - data: groupData.property.data$.value, - updateData: groupData.manager.updateData, - updateValue: value => - groupData.manager.updateValue( - groupData.rows.map(row => row.rowId), - value - ), + group: groupData, readonly: ops.readonly, }; @@ -65,7 +61,7 @@ export const GroupTitle = ( const opsClass = clsx(ops.groupHover && show, groupHeaderOps); return html`
- ${icon} ${renderUniLit(data.view, props)} ${columnName} + ${icon} ${renderUniLit(view, props)} ${columnName} ${GroupHeaderCount(groupData)}
${!ops.readonly 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 119f2cd9a..323f0451a 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 @@ -7,7 +7,7 @@ import { repeat } from 'lit/directives/repeat.js'; 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 { Group } from '../../../../../../core/group-by/trait'; import type { Row } from '../../../../../../core/view-manager/row'; import type { TableProperty, @@ -84,7 +84,7 @@ export class DataViewColumnPreview extends SignalWatcher( accessor container!: HTMLElement; @property({ attribute: false }) - accessor group: GroupData | undefined = undefined; + accessor group: Group | undefined = undefined; } declare global { diff --git a/blocksuite/affine/data-view/src/view-presets/table/pc-virtual/group/top/header/single-column-header.ts b/blocksuite/affine/data-view/src/view-presets/table/pc-virtual/group/top/header/single-column-header.ts index 3d807a75c..899393b3d 100644 --- a/blocksuite/affine/data-view/src/view-presets/table/pc-virtual/group/top/header/single-column-header.ts +++ b/blocksuite/affine/data-view/src/view-presets/table/pc-virtual/group/top/header/single-column-header.ts @@ -40,7 +40,10 @@ import { } from '../../../../../../core/utils/wc-dnd/dnd-context'; import type { Property } from '../../../../../../core/view-manager/property'; import { numberFormats } from '../../../../../../property-presets/number/utils/formats'; -import { ShowQuickSettingBarContextKey } from '../../../../../../widget-presets/quick-setting-bar/context'; +import { + createDefaultShowQuickSettingBar, + ShowQuickSettingBarKey, +} from '../../../../../../widget-presets/quick-setting-bar/context'; import { DEFAULT_COLUMN_TITLE_HEIGHT } from '../../../../consts'; import type { TableProperty, @@ -193,7 +196,10 @@ export class DatabaseHeaderColumn extends SignalWatcher( } private _toggleQuickSettingBar(show = true) { - const map = this.tableViewManager.contextGet(ShowQuickSettingBarContextKey); + const map = this.tableViewManager.serviceGetOrCreate( + ShowQuickSettingBarKey, + createDefaultShowQuickSettingBar + ); map.value = { ...map.value, [this.tableViewManager.id]: show, diff --git a/blocksuite/affine/data-view/src/view-presets/table/pc/group.ts b/blocksuite/affine/data-view/src/view-presets/table/pc/group.ts index 6100a313e..b3ef78737 100644 --- a/blocksuite/affine/data-view/src/view-presets/table/pc/group.ts +++ b/blocksuite/affine/data-view/src/view-presets/table/pc/group.ts @@ -14,7 +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 { Group } from '../../../core/group-by/trait.js'; import type { Row } from '../../../core/index.js'; import { createDndContext } from '../../../core/utils/wc-dnd/dnd-context.js'; import { defaultActivators } from '../../../core/utils/wc-dnd/sensors/index.js'; @@ -150,7 +150,7 @@ export class TableGroup extends SignalWatcher( }; @property({ attribute: false }) - accessor group: GroupData | undefined = undefined; + accessor group: Group | undefined = undefined; @property({ attribute: false }) accessor view!: TableSingleView; 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 72c05d954..e591fc3ba 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 @@ -7,7 +7,7 @@ import { repeat } from 'lit/directives/repeat.js'; 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 { Group } from '../../../../core/group-by/trait.js'; import type { Row } from '../../../../core/index.js'; import type { TableProperty, @@ -84,7 +84,7 @@ export class DataViewColumnPreview extends SignalWatcher( accessor container!: HTMLElement; @property({ attribute: false }) - accessor group: GroupData | undefined = undefined; + accessor group: Group | undefined = undefined; } declare global { diff --git a/blocksuite/affine/data-view/src/view-presets/table/pc/header/database-header-column.ts b/blocksuite/affine/data-view/src/view-presets/table/pc/header/database-header-column.ts index ddcee8822..4db317051 100644 --- a/blocksuite/affine/data-view/src/view-presets/table/pc/header/database-header-column.ts +++ b/blocksuite/affine/data-view/src/view-presets/table/pc/header/database-header-column.ts @@ -40,7 +40,10 @@ import { } from '../../../../core/utils/wc-dnd/dnd-context.js'; import type { Property } from '../../../../core/view-manager/property.js'; import { numberFormats } from '../../../../property-presets/number/utils/formats.js'; -import { ShowQuickSettingBarContextKey } from '../../../../widget-presets/quick-setting-bar/context.js'; +import { + createDefaultShowQuickSettingBar, + ShowQuickSettingBarKey, +} from '../../../../widget-presets/quick-setting-bar/context.js'; import { DEFAULT_COLUMN_TITLE_HEIGHT } from '../../consts.js'; import type { TableProperty, @@ -193,7 +196,10 @@ export class DatabaseHeaderColumn extends SignalWatcher( } private _toggleQuickSettingBar(show = true) { - const map = this.tableViewManager.contextGet(ShowQuickSettingBarContextKey); + const map = this.tableViewManager.serviceGetOrCreate( + ShowQuickSettingBarKey, + createDefaultShowQuickSettingBar + ); map.value = { ...map.value, [this.tableViewManager.id]: show, diff --git a/blocksuite/affine/data-view/src/view-presets/table/stats/column-stats-bar.ts b/blocksuite/affine/data-view/src/view-presets/table/stats/column-stats-bar.ts index 8fe1f356f..39ffe76d8 100644 --- a/blocksuite/affine/data-view/src/view-presets/table/stats/column-stats-bar.ts +++ b/blocksuite/affine/data-view/src/view-presets/table/stats/column-stats-bar.ts @@ -4,7 +4,7 @@ import { css, html } from 'lit'; import { property } from 'lit/decorators.js'; import { repeat } from 'lit/directives/repeat.js'; -import type { GroupData } from '../../../core/group-by/trait.js'; +import type { Group } from '../../../core/group-by/trait.js'; import { LEFT_TOOL_BAR_WIDTH, STATS_BAR_HEIGHT } from '../consts.js'; import type { TableSingleView } from '../table-view-manager.js'; @@ -38,7 +38,7 @@ export class DataBaseColumnStats extends SignalWatcher( } @property({ attribute: false }) - accessor group: GroupData | undefined = undefined; + accessor group: Group | undefined = undefined; @property({ attribute: false }) accessor view!: TableSingleView; diff --git a/blocksuite/affine/data-view/src/view-presets/table/stats/column-stats-column.ts b/blocksuite/affine/data-view/src/view-presets/table/stats/column-stats-column.ts index 56e345225..f39d30846 100644 --- a/blocksuite/affine/data-view/src/view-presets/table/stats/column-stats-column.ts +++ b/blocksuite/affine/data-view/src/view-presets/table/stats/column-stats-column.ts @@ -14,7 +14,7 @@ import { css, html } from 'lit'; import { property } from 'lit/decorators.js'; import { styleMap } from 'lit/directives/style-map.js'; -import type { GroupData } from '../../../core/group-by/trait.js'; +import type { Group } from '../../../core/group-by/trait.js'; import { typeSystem } from '../../../core/index.js'; import { statsFunctions } from '../../../core/statistics/index.js'; import type { StatisticsConfig } from '../../../core/statistics/types.js'; @@ -236,7 +236,7 @@ export class DatabaseColumnStatsCell extends SignalWatcher( } @property({ attribute: false }) - accessor group: GroupData | undefined = undefined; + accessor group: Group | undefined = undefined; } declare global { diff --git a/blocksuite/affine/data-view/src/widget-presets/quick-setting-bar/context.ts b/blocksuite/affine/data-view/src/widget-presets/quick-setting-bar/context.ts index af2538315..c667dc7db 100644 --- a/blocksuite/affine/data-view/src/widget-presets/quick-setting-bar/context.ts +++ b/blocksuite/affine/data-view/src/widget-presets/quick-setting-bar/context.ts @@ -1,7 +1,10 @@ +import { createIdentifier } from '@blocksuite/global/di'; import { type Signal, signal } from '@preact/signals-core'; -import { createContextKey } from '../../core/index.js'; - -export const ShowQuickSettingBarContextKey = createContextKey< +export const ShowQuickSettingBarKey = createIdentifier< Signal> ->('show-quick-setting-bar', signal({})); +>('show-quick-setting-bar'); + +export const createDefaultShowQuickSettingBar = () => { + return signal>({}); +}; diff --git a/blocksuite/affine/data-view/src/widget-presets/quick-setting-bar/index.ts b/blocksuite/affine/data-view/src/widget-presets/quick-setting-bar/index.ts index 2cde6a1fe..833d222b9 100644 --- a/blocksuite/affine/data-view/src/widget-presets/quick-setting-bar/index.ts +++ b/blocksuite/affine/data-view/src/widget-presets/quick-setting-bar/index.ts @@ -6,7 +6,10 @@ import { type DataViewWidgetProps, defineUniComponent, } from '../../core/index.js'; -import { ShowQuickSettingBarContextKey } from './context.js'; +import { + createDefaultShowQuickSettingBar, + ShowQuickSettingBarKey, +} from './context.js'; import { renderFilterBar } from './filter/index.js'; import { renderSortBar } from './sort/index.js'; @@ -17,7 +20,12 @@ export const widgetQuickSettingBar = defineUniComponent( Boolean ); if (!IS_MOBILE) { - if (!view.contextGet(ShowQuickSettingBarContextKey).value[view.id]) { + if ( + !view.serviceGetOrCreate( + ShowQuickSettingBarKey, + createDefaultShowQuickSettingBar + ).value[view.id] + ) { return html``; } if (!barList.length) { diff --git a/blocksuite/affine/data-view/src/widget-presets/tools/presets/filter/filter.ts b/blocksuite/affine/data-view/src/widget-presets/tools/presets/filter/filter.ts index d61eb616a..cb37be605 100644 --- a/blocksuite/affine/data-view/src/widget-presets/tools/presets/filter/filter.ts +++ b/blocksuite/affine/data-view/src/widget-presets/tools/presets/filter/filter.ts @@ -11,7 +11,10 @@ import { filterTraitKey } from '../../../../core/filter/trait.js'; import type { FilterGroup } from '../../../../core/filter/types.js'; import { emptyFilterGroup } from '../../../../core/filter/utils.js'; import { WidgetBase } from '../../../../core/widget/widget-base.js'; -import { ShowQuickSettingBarContextKey } from '../../../quick-setting-bar/context.js'; +import { + createDefaultShowQuickSettingBar, + ShowQuickSettingBarKey, +} from '../../../quick-setting-bar/context.js'; const styles = css` .affine-database-filter-button { @@ -100,7 +103,10 @@ export class DataViewHeaderToolsFilter extends WidgetBase { } toggleShowFilter(show?: boolean) { - const map = this.view.contextGet(ShowQuickSettingBarContextKey); + const map = this.view.serviceGetOrCreate( + ShowQuickSettingBarKey, + createDefaultShowQuickSettingBar + ); map.value = { ...map.value, [this.view.id]: show ?? !map.value[this.view.id], diff --git a/blocksuite/affine/data-view/src/widget-presets/tools/presets/sort/sort.ts b/blocksuite/affine/data-view/src/widget-presets/tools/presets/sort/sort.ts index 7d4ca7e82..58c17a931 100644 --- a/blocksuite/affine/data-view/src/widget-presets/tools/presets/sort/sort.ts +++ b/blocksuite/affine/data-view/src/widget-presets/tools/presets/sort/sort.ts @@ -10,7 +10,10 @@ import { popCreateSort } from '../../../../core/sort/add-sort.js'; import { sortTraitKey } from '../../../../core/sort/manager.js'; import { createSortUtils } from '../../../../core/sort/utils.js'; import { WidgetBase } from '../../../../core/widget/widget-base.js'; -import { ShowQuickSettingBarContextKey } from '../../../quick-setting-bar/context.js'; +import { + createDefaultShowQuickSettingBar, + ShowQuickSettingBarKey, +} from '../../../quick-setting-bar/context.js'; import { popSortRoot } from '../../../quick-setting-bar/sort/root-panel.js'; const styles = css` @@ -106,7 +109,10 @@ export class DataViewHeaderToolsSort extends WidgetBase { } toggleShowQuickSettingBar(show?: boolean) { - const map = this.view.contextGet(ShowQuickSettingBarContextKey); + const map = this.view.serviceGetOrCreate( + ShowQuickSettingBarKey, + createDefaultShowQuickSettingBar + ); map.value = { ...map.value, [this.view.id]: show ?? !map.value[this.view.id], diff --git a/packages/frontend/core/src/blocksuite/database-block/group-by/index.tsx b/packages/frontend/core/src/blocksuite/database-block/group-by/index.tsx new file mode 100644 index 000000000..a5cc7831d --- /dev/null +++ b/packages/frontend/core/src/blocksuite/database-block/group-by/index.tsx @@ -0,0 +1,147 @@ +import { Avatar, uniReactRoot } from '@affine/component'; +import { + createGroupByConfig, + type GroupRenderProps, + t, + ungroups, +} from '@blocksuite/affine/blocks/database'; +import type { UserService } from '@blocksuite/affine-shared/services'; + +import { useMemberInfo } from '../hooks/use-member-info'; +import { + avatar, + memberName, + memberPreviewContainer, +} from '../properties/member/style.css'; + +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 MemberGroupView = (props: GroupRenderProps) => { + const tType = props.group.tType; + if (!t.user.is(tType)) return 'Ungroup'; + const memberId = props.group.value; + if (memberId == null) return 'Ungroup'; + + return ( + + ); +}; + +const MultiMemberGroupView = (props: GroupRenderProps) => { + const tType = props.group.tType; + if (!t.array.is(tType) || !t.user.is(tType.element)) return 'Ungroup'; + const memberId = props.group.value; + if (memberId == null) return 'Ungroup'; + + return ( + + ); +}; + +export const groupByConfigList = [ + createGroupByConfig({ + name: 'member', + matchType: t.user.instance(), + groupName: (type, value: string | null) => { + if (t.user.is(type) && typeof value === 'string') { + const userService = type.data?.userService; + if (userService) { + const userInfo = userService.userInfo$(value).value; + if (userInfo && !userInfo?.removed) { + return userInfo.name ?? 'Unnamed'; + } + } + } + return ''; + }, + defaultKeys: () => { + return [ungroups]; + }, + valuesGroup: value => { + if (typeof value !== 'string') { + return [ungroups]; + } + return [ + { + key: value, + value: value, + }, + ]; + }, + view: uniReactRoot.createUniComponent(MemberGroupView), + }), + createGroupByConfig({ + name: 'multi-member', + matchType: t.array.instance(t.user.instance()), + groupName: (_type, value: string | null) => { + if ( + t.array.is(_type) && + t.user.is(_type.element) && + typeof value === 'string' + ) { + const userService = _type.element.data?.userService; + if (userService) { + const userInfo = userService.userInfo$(value).value; + if (userInfo && !userInfo?.removed) { + return userInfo.name ?? 'Unnamed'; + } + } + } + return ''; + }, + defaultKeys: _type => { + return [ungroups]; + }, + valuesGroup: (value, _type) => { + if (!Array.isArray(value)) { + return [ungroups]; + } + return value.map(id => ({ + key: id, + value: id, + })); + }, + addToGroup: (value, old) => { + if (value == null) { + return old; + } + return Array.isArray(old) ? [...old, value] : [value]; + }, + removeFromGroup: (value, old) => { + if (Array.isArray(old)) { + return old.filter(v => v !== value); + } + return old; + }, + view: uniReactRoot.createUniComponent(MultiMemberGroupView), + }), +]; diff --git a/packages/frontend/core/src/blocksuite/database-block/hooks/use-member-info.ts b/packages/frontend/core/src/blocksuite/database-block/hooks/use-member-info.ts new file mode 100644 index 000000000..f14744547 --- /dev/null +++ b/packages/frontend/core/src/blocksuite/database-block/hooks/use-member-info.ts @@ -0,0 +1,14 @@ +import type { UserService } from '@blocksuite/affine-shared/services'; +import { useEffect } from 'react'; + +import { useSignalValue } from '../../../modules/doc-info/utils'; + +export const useMemberInfo = ( + id: string, + userService: UserService | null | undefined +) => { + useEffect(() => { + userService?.revalidateUserInfo(id); + }, [id, userService]); + return useSignalValue(userService?.userInfo$(id)); +}; diff --git a/packages/frontend/core/src/blocksuite/database-block/properties/created-by/define.ts b/packages/frontend/core/src/blocksuite/database-block/properties/created-by/define.ts index 621792cc4..ac5b54fb6 100644 --- a/packages/frontend/core/src/blocksuite/database-block/properties/created-by/define.ts +++ b/packages/frontend/core/src/blocksuite/database-block/properties/created-by/define.ts @@ -1,4 +1,12 @@ -import { propertyType, t } from '@blocksuite/affine/blocks/database'; +import { + EditorHostKey, + propertyType, + t, +} from '@blocksuite/affine/blocks/database'; +import { + UserListProvider, + UserProvider, +} from '@blocksuite/affine/shared/services'; import zod from 'zod'; export const createdByColumnType = propertyType('created-by'); @@ -26,6 +34,19 @@ export const createdByPropertyModelConfig = createdByColumnType.modelConfig({ jsonValue: { schema: zod.string().nullable(), isEmpty: () => false, - type: () => t.string.instance(), + type: ({ dataSource }) => { + const host = dataSource.serviceGet(EditorHostKey); + const userService = host?.std.getOptional(UserProvider); + const userListService = host?.std.getOptional(UserListProvider); + + return t.user.instance( + userListService && userService + ? { + userService, + userListService, + } + : undefined + ); + }, }, }); 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 index 01f581d31..6a7085561 100644 --- 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 @@ -3,7 +3,7 @@ import { type CellRenderProps, createIcon, type DataViewCellLifeCycle, - HostContextKey, + EditorHostKey, } from '@blocksuite/affine/blocks/database'; import { UserProvider, @@ -14,11 +14,11 @@ import { forwardRef, type ForwardRefRenderFunction, type ReactNode, - useEffect, useImperativeHandle, } from 'react'; import { useSignalValue } from '../../../../modules/doc-info/utils'; +import { useMemberInfo } from '../../hooks/use-member-info'; import { createdByPropertyModelConfig } from './define'; const cellContainer = css({ @@ -64,7 +64,7 @@ const CreatedByCellComponent: ForwardRefRenderFunction< }), [] ); - const host = props.cell.view.contextGet(HostContextKey); + const host = props.cell.view.serviceGet(EditorHostKey); const userService = host?.std.getOptional(UserProvider); const memberId = useSignalValue(props.cell.value$); if (!memberId) { @@ -83,16 +83,6 @@ const CreatedByCellComponent: ForwardRefRenderFunction< ); }; -const useMemberInfo = ( - id: string, - userService: UserService | null | undefined -) => { - useEffect(() => { - userService?.revalidateUserInfo(id); - }, [id, userService]); - return useSignalValue(userService?.userInfo$(id)); -}; - const MemberPreview = ({ memberId, userService, diff --git a/packages/frontend/core/src/blocksuite/database-block/properties/file/view.tsx b/packages/frontend/core/src/blocksuite/database-block/properties/file/view.tsx index 53efcdde0..29a94e189 100644 --- a/packages/frontend/core/src/blocksuite/database-block/properties/file/view.tsx +++ b/packages/frontend/core/src/blocksuite/database-block/properties/file/view.tsx @@ -7,7 +7,7 @@ import { type CellRenderProps, createIcon, type DataViewCellLifeCycle, - HostContextKey, + EditorHostKey, } from '@blocksuite/affine/blocks/database'; import { openFileOrFiles } from '@blocksuite/affine/shared/utils'; import type { BlobEngine } from '@blocksuite/affine/sync'; @@ -224,8 +224,8 @@ class FileCellManager { this.cell = props.cell; this.selectCurrentCell = props.selectCurrentCell; this.isEditing = props.isEditing$; - this.blobSync = this.cell?.view?.contextGet - ? this.cell.view.contextGet(HostContextKey)?.store.blobSync + this.blobSync = this.cell?.view?.serviceGet + ? this.cell.view.serviceGet(EditorHostKey)?.store.blobSync : undefined; this.fileUploadManager = this.blobSync diff --git a/packages/frontend/core/src/blocksuite/database-block/properties/member/define.ts b/packages/frontend/core/src/blocksuite/database-block/properties/member/define.ts index 3462e54f4..8f855ce1b 100644 --- a/packages/frontend/core/src/blocksuite/database-block/properties/member/define.ts +++ b/packages/frontend/core/src/blocksuite/database-block/properties/member/define.ts @@ -1,4 +1,12 @@ -import { propertyType, t } from '@blocksuite/affine/blocks/database'; +import { + EditorHostKey, + propertyType, + t, +} from '@blocksuite/affine/blocks/database'; +import { + UserListProvider, + UserProvider, +} from '@blocksuite/affine/shared/services'; import zod from 'zod'; export const memberColumnType = propertyType('member'); @@ -31,7 +39,21 @@ export const memberPropertyModelConfig = memberColumnType.modelConfig({ }, jsonValue: { schema: MemberCellJsonValueTypeSchema, - type: () => t.array.instance(t.string.instance()), + type: ({ dataSource }) => { + const host = dataSource.serviceGet(EditorHostKey); + const userService = host?.std.getOptional(UserProvider); + const userListService = host?.std.getOptional(UserListProvider); + return t.array.instance( + t.user.instance( + userListService && userService + ? { + userService: userService, + userListService: userListService, + } + : undefined + ) + ); + }, isEmpty: ({ value }) => value.length === 0, }, }); 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 209c9300c..bf32a7724 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 @@ -16,6 +16,7 @@ import { import { useSignalValue } from '../../../../../modules/doc-info/utils'; import { Spinner } from '../../../components/loading'; +import { useMemberInfo } from '../../../hooks/use-member-info'; import * as styles from './style.css'; type BaseOptions = { @@ -172,13 +173,6 @@ class MemberManager { }; } -export const useMemberInfo = (id: string, memberManager: MemberManager) => { - useEffect(() => { - memberManager.userService?.revalidateUserInfo(id); - }, [id, memberManager.userService]); - return useSignalValue(memberManager.userService?.userInfo$(id)); -}; - export const MemberListItem = (props: { member: ExistedUserInfo; memberManager: MemberManager; @@ -225,7 +219,7 @@ export const MemberPreview = ({ memberManager: MemberManager; onDelete?: () => void; }) => { - const userInfo = useMemberInfo(memberId, memberManager); + const userInfo = useMemberInfo(memberId, memberManager.userService); if (!userInfo) { return null; } diff --git a/packages/frontend/core/src/blocksuite/database-block/properties/member/style.css.ts b/packages/frontend/core/src/blocksuite/database-block/properties/member/style.css.ts index 8fa71ba77..7eef5d24d 100644 --- a/packages/frontend/core/src/blocksuite/database-block/properties/member/style.css.ts +++ b/packages/frontend/core/src/blocksuite/database-block/properties/member/style.css.ts @@ -1,4 +1,3 @@ -import { cssVarV2 } from '@toeverything/theme/v2'; import { style } from '@vanilla-extract/css'; export const memberPopoverContainer = style({ @@ -10,45 +9,10 @@ export const memberPopoverContent = style({ padding: '0', }); -export const searchContainer = style({ - padding: '12px 12px 8px 12px', -}); - export const searchInput = style({ width: '100%', }); -export const memberListContainer = style({ - display: 'flex', - flexDirection: 'column', - maxHeight: '300px', - overflow: 'auto', -}); - -export const memberItem = style({ - display: 'flex', - justifyContent: 'space-between', - padding: '8px 12px', - gap: '8px', - overflow: 'hidden', - cursor: 'pointer', - borderRadius: '4px', - transition: 'background-color 0.2s ease', - ':hover': { - backgroundColor: cssVarV2.layer.background.hoverOverlay, - }, - ':active': { - backgroundColor: cssVarV2.layer.background.secondary, - }, -}); - -export const memberItemContent = style({ - display: 'flex', - alignItems: 'center', - gap: '8px', - overflow: 'hidden', -}); - export const memberName = style({ overflow: 'hidden', textOverflow: 'ellipsis', @@ -70,21 +34,6 @@ export const avatar = style({ flexShrink: 0, }); -export const loadingContainer = style({ - display: 'flex', - justifyContent: 'center', - alignItems: 'center', - padding: '16px', -}); - -export const noResultContainer = style({ - display: 'flex', - justifyContent: 'center', - alignItems: 'center', - padding: '16px', - color: cssVarV2.text.secondary, -}); - export const memberPreviewContainer = style({ display: 'flex', alignItems: 'center', diff --git a/packages/frontend/core/src/blocksuite/database-block/properties/member/view.tsx b/packages/frontend/core/src/blocksuite/database-block/properties/member/view.tsx index 797180921..3d9b599a2 100644 --- a/packages/frontend/core/src/blocksuite/database-block/properties/member/view.tsx +++ b/packages/frontend/core/src/blocksuite/database-block/properties/member/view.tsx @@ -4,7 +4,7 @@ import { type CellRenderProps, createIcon, type DataViewCellLifeCycle, - HostContextKey, + EditorHostKey, } from '@blocksuite/affine/blocks/database'; import { UserListProvider, @@ -17,12 +17,12 @@ import { forwardRef, type ForwardRefRenderFunction, type ReactNode, - useEffect, useImperativeHandle, useMemo, } from 'react'; import { useSignalValue } from '../../../../modules/doc-info/utils'; +import { useMemberInfo } from '../../hooks/use-member-info'; import type { MemberCellJsonValueType, MemberCellRawValueType, @@ -55,7 +55,7 @@ class MemberManager { this.cell = props.cell; this.selectCurrentCell = props.selectCurrentCell; this.isEditing = props.isEditing$; - const host = this.cell.view.contextGet(HostContextKey); + const host = this.cell.view.serviceGet(EditorHostKey); this.userService = host?.std.getOptional(UserProvider); this.userListService = host?.std.getOptional(UserListProvider); } @@ -140,13 +140,6 @@ const MemberCellComponent: ForwardRefRenderFunction< ); }; -const useMemberInfo = (id: string, memberManager: MemberManager) => { - useEffect(() => { - memberManager.userService?.revalidateUserInfo(id); - }, [id, memberManager.userService]); - return useSignalValue(memberManager.userService?.userInfo$(id)); -}; - const MemberPreview = ({ memberId, memberManager, @@ -154,7 +147,7 @@ const MemberPreview = ({ memberId: string; memberManager: MemberManager; }) => { - const userInfo = useMemberInfo(memberId, memberManager); + const userInfo = useMemberInfo(memberId, memberManager.userService); if (!userInfo) { return null; } diff --git a/packages/frontend/core/src/blocksuite/extensions/database-block-config-service.ts b/packages/frontend/core/src/blocksuite/extensions/database-block-config-service.ts index 7463ea8e1..26ad9b119 100644 --- a/packages/frontend/core/src/blocksuite/extensions/database-block-config-service.ts +++ b/packages/frontend/core/src/blocksuite/extensions/database-block-config-service.ts @@ -1,12 +1,20 @@ -import { DatabaseBlockDataSource } from '@blocksuite/affine/blocks/database'; +import { + DatabaseBlockDataSource, + ExternalGroupByConfigProvider, +} from '@blocksuite/affine/blocks/database'; import type { ExtensionType } from '@blocksuite/affine/store'; +import { groupByConfigList } from '../database-block/group-by'; import { propertiesPresets } from '../database-block/properties'; export function patchDatabaseBlockConfigService(): ExtensionType { //TODO use service DatabaseBlockDataSource.externalProperties.value = propertiesPresets; return { - setup: () => {}, + setup: di => { + groupByConfigList.forEach(config => { + di.addValue(ExternalGroupByConfigProvider(config.name), config); + }); + }, }; }