feat(core): add user list service for blocksuite (#10627)
This commit is contained in:
@@ -5,8 +5,8 @@ export type RemovedUserInfo = {
|
|||||||
|
|
||||||
export type ExistedUserInfo = {
|
export type ExistedUserInfo = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name?: string | null;
|
||||||
avatar: string;
|
avatar?: string | null;
|
||||||
removed?: false;
|
removed?: false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -255,6 +255,8 @@ export default tseslint.config(
|
|||||||
|
|
||||||
types: {
|
types: {
|
||||||
'^LiveData$': true,
|
'^LiveData$': true,
|
||||||
|
'^Signal$': true,
|
||||||
|
'^ReadonlySignal$': true,
|
||||||
'^Doc$': false,
|
'^Doc$': false,
|
||||||
'^Awareness$': false,
|
'^Awareness$': false,
|
||||||
'^UndoManager$': false,
|
'^UndoManager$': false,
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
"@affine/env": "workspace:*",
|
"@affine/env": "workspace:*",
|
||||||
"@affine/templates": "workspace:*",
|
"@affine/templates": "workspace:*",
|
||||||
"@datastructures-js/binary-search-tree": "^5.3.2",
|
"@datastructures-js/binary-search-tree": "^5.3.2",
|
||||||
|
"@preact/signals-core": "^1.8.0",
|
||||||
"eventemitter2": "^6.4.9",
|
"eventemitter2": "^6.4.9",
|
||||||
"foxact": "^0.2.43",
|
"foxact": "^0.2.43",
|
||||||
"fractional-indexing": "^3.2.0",
|
"fractional-indexing": "^3.2.0",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { DebugLogger } from '@affine/debug';
|
import { DebugLogger } from '@affine/debug';
|
||||||
|
import { type ReadonlySignal, type Signal, signal } from '@preact/signals-core';
|
||||||
import type {
|
import type {
|
||||||
InteropObservable,
|
InteropObservable,
|
||||||
Observer,
|
Observer,
|
||||||
@@ -140,6 +141,17 @@ export class LiveData<T = unknown>
|
|||||||
return data$;
|
return data$;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static fromSignal<T>(signal: ReadonlySignal<T>): LiveData<T> {
|
||||||
|
return LiveData.from(
|
||||||
|
new Observable(subscriber => {
|
||||||
|
signal.subscribe(value => {
|
||||||
|
subscriber.next(value);
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
signal.value
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private static GLOBAL_COMPUTED_RECURSIVE_COUNT = 0;
|
private static GLOBAL_COMPUTED_RECURSIVE_COUNT = 0;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -286,6 +298,19 @@ export class LiveData<T = unknown>
|
|||||||
this.next(v);
|
this.next(v);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _signal: Signal<T> | undefined;
|
||||||
|
|
||||||
|
get signal(): ReadonlySignal<T> {
|
||||||
|
if (!this._signal) {
|
||||||
|
this._signal = signal(this.value);
|
||||||
|
this.subscribe(v => {
|
||||||
|
// oxlint-disable-next-line no-non-null-assertion
|
||||||
|
this._signal!.value = v;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return this._signal;
|
||||||
|
}
|
||||||
|
|
||||||
next = (v: T) => {
|
next = (v: T) => {
|
||||||
if (this.isPoisoned) {
|
if (this.isPoisoned) {
|
||||||
throw this.poisonedError;
|
throw this.poisonedError;
|
||||||
@@ -379,7 +404,6 @@ export class LiveData<T = unknown>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line rxjs/finnish
|
|
||||||
asObservable(): Observable<T> {
|
asObservable(): Observable<T> {
|
||||||
return new Observable<T>(subscriber => {
|
return new Observable<T>(subscriber => {
|
||||||
return this.subscribe(subscriber);
|
return this.subscribe(subscriber);
|
||||||
@@ -421,7 +445,7 @@ export class LiveData<T = unknown>
|
|||||||
override pipe(...args: any[]) {
|
override pipe(...args: any[]) {
|
||||||
return new Observable(subscriber => {
|
return new Observable(subscriber => {
|
||||||
this.ops$.next('watch');
|
this.ops$.next('watch');
|
||||||
// eslint-disable-next-line prefer-spread
|
|
||||||
const subscription = this.raw$.pipe
|
const subscription = this.raw$.pipe
|
||||||
.apply(this.raw$, args as any)
|
.apply(this.raw$, args as any)
|
||||||
.subscribe(subscriber);
|
.subscribe(subscriber);
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
|||||||
import { JournalService } from '@affine/core/modules/journal';
|
import { JournalService } from '@affine/core/modules/journal';
|
||||||
import { toURLSearchParams } from '@affine/core/modules/navigation';
|
import { toURLSearchParams } from '@affine/core/modules/navigation';
|
||||||
import { PeekViewService } from '@affine/core/modules/peek-view/services/peek-view';
|
import { PeekViewService } from '@affine/core/modules/peek-view/services/peek-view';
|
||||||
|
import { MemberSearchService } from '@affine/core/modules/permissions';
|
||||||
import { WorkspaceService } from '@affine/core/modules/workspace';
|
import { WorkspaceService } from '@affine/core/modules/workspace';
|
||||||
import track from '@affine/track';
|
import track from '@affine/track';
|
||||||
import {
|
import {
|
||||||
@@ -69,6 +70,7 @@ import {
|
|||||||
type ReferenceReactRenderer,
|
type ReferenceReactRenderer,
|
||||||
} from '../extensions/reference-renderer';
|
} from '../extensions/reference-renderer';
|
||||||
import { patchSideBarService } from '../extensions/side-bar-service';
|
import { patchSideBarService } from '../extensions/side-bar-service';
|
||||||
|
import { patchUserListExtensions } from '../extensions/user-list';
|
||||||
import { BiDirectionalLinkPanel } from './bi-directional-link-panel';
|
import { BiDirectionalLinkPanel } from './bi-directional-link-panel';
|
||||||
import { BlocksuiteEditorJournalDocTitle } from './journal-doc-title';
|
import { BlocksuiteEditorJournalDocTitle } from './journal-doc-title';
|
||||||
import { StarterBar } from './starter-bar';
|
import { StarterBar } from './starter-bar';
|
||||||
@@ -90,6 +92,7 @@ const usePatchSpecs = (mode: DocMode) => {
|
|||||||
editorService,
|
editorService,
|
||||||
workspaceService,
|
workspaceService,
|
||||||
featureFlagService,
|
featureFlagService,
|
||||||
|
memberSearchService,
|
||||||
} = useServices({
|
} = useServices({
|
||||||
PeekViewService,
|
PeekViewService,
|
||||||
DocService,
|
DocService,
|
||||||
@@ -97,6 +100,7 @@ const usePatchSpecs = (mode: DocMode) => {
|
|||||||
WorkspaceService,
|
WorkspaceService,
|
||||||
EditorService,
|
EditorService,
|
||||||
FeatureFlagService,
|
FeatureFlagService,
|
||||||
|
MemberSearchService,
|
||||||
});
|
});
|
||||||
const framework = useFramework();
|
const framework = useFramework();
|
||||||
const referenceRenderer: ReferenceReactRenderer = useMemo(() => {
|
const referenceRenderer: ReferenceReactRenderer = useMemo(() => {
|
||||||
@@ -151,6 +155,7 @@ const usePatchSpecs = (mode: DocMode) => {
|
|||||||
patchPeekViewService(peekViewService),
|
patchPeekViewService(peekViewService),
|
||||||
patchOpenDocExtension(),
|
patchOpenDocExtension(),
|
||||||
EdgelessClipboardWatcher,
|
EdgelessClipboardWatcher,
|
||||||
|
patchUserListExtensions(memberSearchService),
|
||||||
patchDocUrlExtensions(framework),
|
patchDocUrlExtensions(framework),
|
||||||
patchQuickSearchService(framework),
|
patchQuickSearchService(framework),
|
||||||
patchSideBarService(framework),
|
patchSideBarService(framework),
|
||||||
@@ -173,18 +178,19 @@ const usePatchSpecs = (mode: DocMode) => {
|
|||||||
|
|
||||||
return builder.value;
|
return builder.value;
|
||||||
}, [
|
}, [
|
||||||
|
framework,
|
||||||
mode,
|
mode,
|
||||||
|
enableAI,
|
||||||
|
reactToLit,
|
||||||
|
referenceRenderer,
|
||||||
confirmModal,
|
confirmModal,
|
||||||
|
peekViewService,
|
||||||
|
memberSearchService,
|
||||||
docService,
|
docService,
|
||||||
docsService,
|
docsService,
|
||||||
editorService,
|
editorService,
|
||||||
framework,
|
|
||||||
peekViewService,
|
|
||||||
reactToLit,
|
|
||||||
referenceRenderer,
|
|
||||||
featureFlagService,
|
|
||||||
enableAI,
|
|
||||||
enableTurboRenderer,
|
enableTurboRenderer,
|
||||||
|
featureFlagService.flags.enable_pdf_embed_preview.value,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import type { MemberSearchService } from '@affine/core/modules/permissions';
|
||||||
|
import { UserListServiceExtension } from '@blocksuite/affine/blocks';
|
||||||
|
|
||||||
|
export function patchUserListExtensions(memberSearch: MemberSearchService) {
|
||||||
|
return UserListServiceExtension({
|
||||||
|
// eslint-disable-next-line rxjs/finnish
|
||||||
|
hasMore$: memberSearch.hasMore$.signal,
|
||||||
|
loadMore() {
|
||||||
|
memberSearch.loadMore();
|
||||||
|
},
|
||||||
|
search(keyword) {
|
||||||
|
memberSearch.search(keyword);
|
||||||
|
},
|
||||||
|
// eslint-disable-next-line rxjs/finnish
|
||||||
|
users$: memberSearch.result$.map(users =>
|
||||||
|
users.map(u => ({
|
||||||
|
id: u.id,
|
||||||
|
name: u.name,
|
||||||
|
avatar: u.avatarUrl,
|
||||||
|
}))
|
||||||
|
).signal,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -2,14 +2,14 @@ import {
|
|||||||
DatabaseBlockDataSource,
|
DatabaseBlockDataSource,
|
||||||
type DatabaseBlockModel,
|
type DatabaseBlockModel,
|
||||||
} from '@blocksuite/affine/blocks';
|
} from '@blocksuite/affine/blocks';
|
||||||
import { Service } from '@toeverything/infra';
|
import { LiveData, Service } from '@toeverything/infra';
|
||||||
import { isEqual } from 'lodash-es';
|
import { isEqual } from 'lodash-es';
|
||||||
import { combineLatest, distinctUntilChanged, map, Observable } from 'rxjs';
|
import { combineLatest, distinctUntilChanged, map, Observable } from 'rxjs';
|
||||||
|
|
||||||
import type { DocsService } from '../../doc';
|
import type { DocsService } from '../../doc';
|
||||||
import type { DocsSearchService } from '../../docs-search';
|
import type { DocsSearchService } from '../../docs-search';
|
||||||
import type { DatabaseRow, DatabaseValueCell } from '../types';
|
import type { DatabaseRow, DatabaseValueCell } from '../types';
|
||||||
import { signalToLiveData, signalToObservable } from '../utils';
|
import { signalToObservable } from '../utils';
|
||||||
|
|
||||||
const equalComparator = <T>(a: T, b: T) => {
|
const equalComparator = <T>(a: T, b: T) => {
|
||||||
return isEqual(a, b);
|
return isEqual(a, b);
|
||||||
@@ -50,14 +50,14 @@ export class DocDatabaseBacklinksService extends Service {
|
|||||||
.map<DatabaseValueCell>(id => {
|
.map<DatabaseValueCell>(id => {
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
value$: signalToLiveData(
|
value$: LiveData.fromSignal(
|
||||||
dataSource.cellValueGet$(rowId, id)
|
dataSource.cellValueGet$(rowId, id)
|
||||||
).distinctUntilChanged(equalComparator),
|
).distinctUntilChanged(equalComparator),
|
||||||
property: {
|
property: {
|
||||||
id,
|
id,
|
||||||
type$: signalToLiveData(dataSource.propertyTypeGet$(id)),
|
type$: LiveData.fromSignal(dataSource.propertyTypeGet$(id)),
|
||||||
name$: signalToLiveData(dataSource.propertyNameGet$(id)),
|
name$: LiveData.fromSignal(dataSource.propertyNameGet$(id)),
|
||||||
data$: signalToLiveData(dataSource.propertyDataGet$(id)),
|
data$: LiveData.fromSignal(dataSource.propertyDataGet$(id)),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { DebugLogger } from '@affine/debug';
|
|||||||
import { BlockStdScope } from '@blocksuite/affine/block-std';
|
import { BlockStdScope } from '@blocksuite/affine/block-std';
|
||||||
import { PageEditorBlockSpecs } from '@blocksuite/affine/blocks';
|
import { PageEditorBlockSpecs } from '@blocksuite/affine/blocks';
|
||||||
import type { Store } from '@blocksuite/affine/store';
|
import type { Store } from '@blocksuite/affine/store';
|
||||||
import { LiveData } from '@toeverything/infra';
|
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
@@ -25,22 +24,6 @@ export function signalToObservable<T>(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function signalToLiveData<T>(
|
|
||||||
signal: ReadonlySignal<T>,
|
|
||||||
defaultValue: T
|
|
||||||
): LiveData<T>;
|
|
||||||
|
|
||||||
export function signalToLiveData<T>(
|
|
||||||
signal: ReadonlySignal<T>
|
|
||||||
): LiveData<T | undefined>;
|
|
||||||
|
|
||||||
export function signalToLiveData<T>(
|
|
||||||
signal: ReadonlySignal<T>,
|
|
||||||
defaultValue?: T
|
|
||||||
) {
|
|
||||||
return LiveData.from(signalToObservable(signal), defaultValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
// todo(pengx17): use rc pool?
|
// todo(pengx17): use rc pool?
|
||||||
export function createBlockStdScope(doc: Store) {
|
export function createBlockStdScope(doc: Store) {
|
||||||
logger.debug('createBlockStdScope', doc.id);
|
logger.debug('createBlockStdScope', doc.id);
|
||||||
|
|||||||
@@ -13431,6 +13431,7 @@ __metadata:
|
|||||||
"@affine/templates": "workspace:*"
|
"@affine/templates": "workspace:*"
|
||||||
"@datastructures-js/binary-search-tree": "npm:^5.3.2"
|
"@datastructures-js/binary-search-tree": "npm:^5.3.2"
|
||||||
"@emotion/react": "npm:^11.14.0"
|
"@emotion/react": "npm:^11.14.0"
|
||||||
|
"@preact/signals-core": "npm:^1.8.0"
|
||||||
"@swc/core": "npm:^1.10.1"
|
"@swc/core": "npm:^1.10.1"
|
||||||
"@testing-library/dom": "npm:^10.4.0"
|
"@testing-library/dom": "npm:^10.4.0"
|
||||||
"@testing-library/react": "npm:^16.1.0"
|
"@testing-library/react": "npm:^16.1.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user