mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-14 21:27:20 +00:00
feat(editor): add grouping support for member property of the database block (#12243)
close: BS-3433 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## 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. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -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<boolean>;
|
||||
@@ -65,7 +70,9 @@ export interface DataSource {
|
||||
propertyDelete(id: string): void;
|
||||
propertyCanDelete(propertyId: string): boolean;
|
||||
|
||||
contextGet<T>(key: DataViewContextKey<T>): T;
|
||||
provider: ServiceProvider;
|
||||
serviceGet<T>(key: GeneralServiceIdentifier<T>): T | null;
|
||||
serviceGetOrCreate<T>(key: GeneralServiceIdentifier<T>, create: () => T): T;
|
||||
|
||||
viewConverts: ViewConvertConfig[];
|
||||
viewManager: ViewManager;
|
||||
@@ -91,6 +98,8 @@ export interface DataSource {
|
||||
viewMetaGetById$(viewId: string): ReadonlySignal<ViewMeta | undefined>;
|
||||
}
|
||||
|
||||
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<symbol, unknown>();
|
||||
protected container = new Container();
|
||||
|
||||
abstract get parentProvider(): ServiceProvider;
|
||||
|
||||
abstract featureFlags$: ReadonlySignal<DatabaseFlags>;
|
||||
|
||||
@@ -144,12 +155,26 @@ export abstract class DataSourceBase implements DataSource {
|
||||
return computed(() => this.cellValueGet(rowId, propertyId));
|
||||
}
|
||||
|
||||
contextGet<T>(key: DataViewContextKey<T>): T {
|
||||
return (this.context.get(key.key) as T) ?? key.defaultValue;
|
||||
get provider() {
|
||||
return this.container.provider(DataSourceScope, this.parentProvider);
|
||||
}
|
||||
|
||||
contextSet<T>(key: DataViewContextKey<T>, value: T): void {
|
||||
this.context.set(key.key, value);
|
||||
serviceGet<T>(key: GeneralServiceIdentifier<T>): T | null {
|
||||
return this.provider.getOptional(key);
|
||||
}
|
||||
|
||||
serviceSet<T>(key: GeneralServiceIdentifier<T>, value: T): void {
|
||||
this.container.addValue(key, value, { scope: DataSourceScope });
|
||||
}
|
||||
|
||||
serviceGetOrCreate<T>(key: GeneralServiceIdentifier<T>, create: () => T): T {
|
||||
const result = this.serviceGet(key);
|
||||
if (result != null) {
|
||||
return result;
|
||||
}
|
||||
const value = create();
|
||||
this.serviceSet(key, value);
|
||||
return value;
|
||||
}
|
||||
|
||||
abstract propertyAdd(
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
export interface DataViewContextKey<T> {
|
||||
key: symbol;
|
||||
defaultValue: T;
|
||||
}
|
||||
|
||||
export const createContextKey = <T>(
|
||||
name: string,
|
||||
defaultValue: T
|
||||
): DataViewContextKey<T> => ({
|
||||
key: Symbol(name),
|
||||
defaultValue,
|
||||
});
|
||||
@@ -1,2 +1 @@
|
||||
export * from './base.js';
|
||||
export * from './context.js';
|
||||
|
||||
@@ -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<unknown>
|
||||
): 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
|
||||
|
||||
@@ -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<GroupByConfig>();
|
||||
const ungroups = {
|
||||
export const createGroupByConfig = <
|
||||
Data extends Record<string, unknown>,
|
||||
MatchType extends TypeInstance,
|
||||
GroupValue = unknown,
|
||||
>(
|
||||
config: GroupByConfig<Data, MatchType, GroupValue>
|
||||
): 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() ?? ''}`;
|
||||
},
|
||||
|
||||
@@ -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` <div class="group-header-count">${cards.length}</div>`;
|
||||
}
|
||||
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` <uni-lit
|
||||
class="group-header-icon"
|
||||
.uni="${groupData.manager.property$.value?.icon}"
|
||||
.uni="${groupData.property.icon}"
|
||||
></uni-lit>`;
|
||||
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 = (
|
||||
<div
|
||||
style="display:flex;align-items:center;gap: 8px;overflow: hidden;height: 22px;"
|
||||
>
|
||||
${icon} ${renderUniLit(data.view, props)} ${columnName}
|
||||
${icon} ${renderUniLit(groupData.view, props)} ${columnName}
|
||||
${GroupHeaderCount(groupData)}
|
||||
</div>
|
||||
${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` <uni-lit
|
||||
class="group-header-icon"
|
||||
.uni="${groupData.manager.property$.value?.icon}"
|
||||
.uni="${groupData.property.icon}"
|
||||
></uni-lit>`;
|
||||
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 = (
|
||||
<div
|
||||
style="display:flex;align-items:center;gap: 8px;overflow: hidden;height: 22px;"
|
||||
>
|
||||
${icon} ${renderUniLit(data.view, props)} ${columnName}
|
||||
${icon} ${renderUniLit(groupData.view, props)} ${columnName}
|
||||
${GroupHeaderCount(groupData)}
|
||||
</div>
|
||||
${ops.readonly
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
export * from './define.js';
|
||||
export * from './matcher.js';
|
||||
export * from './trait.js';
|
||||
|
||||
@@ -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<GroupByConfig>(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<GroupByService>('group-by-service');
|
||||
|
||||
export const getGroupByService = (dataSource: DataSource) => {
|
||||
return dataSource.serviceGetOrCreate(
|
||||
GroupByProvider,
|
||||
() => new GroupByService(dataSource)
|
||||
);
|
||||
};
|
||||
|
||||
export const ExternalGroupByConfigProvider = createIdentifier<GroupByConfig>(
|
||||
'external-group-by-config'
|
||||
);
|
||||
|
||||
@@ -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<Data extends NonNullable<unknown>, Value>
|
||||
export class BaseGroup<JsonValue, Data extends Record<string, unknown>>
|
||||
extends SignalWatcher(WithDisposable(ShadowlessElement))
|
||||
implements GroupRenderProps<Data, Value>
|
||||
implements GroupRenderProps<JsonValue, Data>
|
||||
{
|
||||
@property({ attribute: false })
|
||||
accessor data!: Data;
|
||||
accessor group!: Group<unknown, JsonValue, Data>;
|
||||
|
||||
@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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { css, html } from 'lit';
|
||||
|
||||
import { BaseGroup } from './base.js';
|
||||
|
||||
export class BooleanGroupView extends BaseGroup<NonNullable<unknown>, boolean> {
|
||||
export class BooleanGroupView extends BaseGroup<boolean, NonNullable<unknown>> {
|
||||
static override styles = css`
|
||||
.data-view-group-title-boolean-view {
|
||||
display: flex;
|
||||
|
||||
@@ -7,7 +7,7 @@ import { css, html } from 'lit';
|
||||
|
||||
import { BaseGroup } from './base.js';
|
||||
|
||||
export class NumberGroupView extends BaseGroup<NonNullable<unknown>, number> {
|
||||
export class NumberGroupView extends BaseGroup<number, NonNullable<unknown>> {
|
||||
static override styles = css`
|
||||
.data-view-group-title-number-view {
|
||||
border-radius: 8px;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -7,7 +7,7 @@ import { css, html } from 'lit';
|
||||
|
||||
import { BaseGroup } from './base.js';
|
||||
|
||||
export class StringGroupView extends BaseGroup<NonNullable<unknown>, string> {
|
||||
export class StringGroupView extends BaseGroup<string, NonNullable<unknown>> {
|
||||
static override styles = css`
|
||||
.data-view-group-title-string-view {
|
||||
border-radius: 8px;
|
||||
|
||||
@@ -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` <div
|
||||
${sortable(group.key)}
|
||||
${dragHandler(group.key)}
|
||||
class="dv-hover dv-round-4 group-item"
|
||||
>
|
||||
<div class="group-item-drag-bar"></div>
|
||||
<div style="padding: 0 4px;position:relative;">
|
||||
${renderUniLit(config?.view, props)}
|
||||
<div
|
||||
style="padding: 0 4px;position:relative;pointer-events: none"
|
||||
>
|
||||
${renderUniLit(group.view, props)}
|
||||
<div
|
||||
style="position:absolute;left: 0;top: 0;right: 0;bottom: 0;"
|
||||
></div>
|
||||
@@ -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<MenuConfig>(property => {
|
||||
return menu.action({
|
||||
|
||||
@@ -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<string, unknown> = Record<string, unknown>,
|
||||
> = {
|
||||
config: GroupByConfig;
|
||||
property: Property<RawValue, JsonValue, Data>;
|
||||
tType: TypeInstance;
|
||||
};
|
||||
|
||||
export class Group<
|
||||
RawValue = unknown,
|
||||
JsonValue = unknown,
|
||||
Data extends Record<string, unknown> = Record<string, unknown>,
|
||||
> {
|
||||
rows: Row[] = [];
|
||||
constructor(
|
||||
public readonly key: string,
|
||||
public readonly value: JsonValue,
|
||||
private readonly groupInfo: GroupInfo<RawValue, JsonValue, Data>,
|
||||
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<GroupInfo | undefined>(() => {
|
||||
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<string, Omit<GroupData, 'rows'>> | 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<Record<string, GroupData> | 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<string, GroupData> = Object.fromEntries(
|
||||
Object.entries(staticGroupMap).map(([k, v]) => [k, { ...v, rows: [] }])
|
||||
);
|
||||
const { staticMap, groupInfo } = staticInfo;
|
||||
const groupMap: Record<string, Group> = { ...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<unknown>) => {
|
||||
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<GroupBy | undefined>,
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<unknown> = NonNullable<unknown>,
|
||||
JsonValue = unknown,
|
||||
Data extends Record<string, unknown> = Record<string, unknown>,
|
||||
> {
|
||||
data: Data;
|
||||
updateData?: (data: Data) => void;
|
||||
value: JsonValue;
|
||||
updateValue?: (value: JsonValue) => void;
|
||||
group: Group<unknown, JsonValue, Data>;
|
||||
readonly: boolean;
|
||||
}
|
||||
|
||||
export type GroupByConfig<
|
||||
JsonValue = unknown,
|
||||
Data extends NonNullable<unknown> = NonNullable<unknown>,
|
||||
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<MatchType> | null,
|
||||
type: MatchType
|
||||
) => {
|
||||
key: string;
|
||||
value: JsonValue;
|
||||
value: GroupValue | null;
|
||||
}[];
|
||||
addToGroup?: (value: JsonValue, oldValue: JsonValue) => JsonValue;
|
||||
removeFromGroup?: (value: JsonValue, oldValue: JsonValue) => JsonValue;
|
||||
view: UniComponent<GroupRenderProps<Data, JsonValue>>;
|
||||
addToGroup?: (
|
||||
value: GroupValue | null,
|
||||
oldValue: ValueTypeOf<MatchType> | null
|
||||
) => ValueTypeOf<MatchType> | null;
|
||||
removeFromGroup?: (
|
||||
value: GroupValue | null,
|
||||
oldValue: ValueTypeOf<MatchType> | null
|
||||
) => ValueTypeOf<MatchType> | null;
|
||||
view: UniComponent<GroupRenderProps<GroupValue | null, Data>>;
|
||||
};
|
||||
|
||||
@@ -68,3 +68,46 @@ export class Matcher<Data, Type extends TypeInstance = TypeInstance> {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
export class Matcher_<Value, Type extends TypeInstance> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<UserService>(() => true),
|
||||
userListService: Zod.custom<UserListService>(() => true),
|
||||
});
|
||||
export type UserInfo = Zod.TypeOf<typeof UserInfoSchema>;
|
||||
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),
|
||||
];
|
||||
|
||||
@@ -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<T>(key: DataViewContextKey<T>): T;
|
||||
serviceGet<T>(key: GeneralServiceIdentifier<T>): T | null;
|
||||
serviceGetOrCreate<T>(key: GeneralServiceIdentifier<T>, create: () => T): T;
|
||||
|
||||
traitGet<T>(key: TraitKey<T>): T | undefined;
|
||||
|
||||
@@ -201,8 +202,12 @@ export abstract class SingleViewBase<
|
||||
return new CellBase(this, propertyId, rowId);
|
||||
}
|
||||
|
||||
contextGet<T>(key: DataViewContextKey<T>): T {
|
||||
return this.dataSource.contextGet(key);
|
||||
serviceGet<T>(key: GeneralServiceIdentifier<T>): T | null {
|
||||
return this.dataSource.serviceGet(key);
|
||||
}
|
||||
|
||||
serviceGetOrCreate<T>(key: GeneralServiceIdentifier<T>, create: () => T): T {
|
||||
return this.dataSource.serviceGetOrCreate(key, create);
|
||||
}
|
||||
|
||||
dataUpdate(updater: (viewData: ViewData) => Partial<ViewData>): void {
|
||||
|
||||
Reference in New Issue
Block a user