refactor(core): implement doc created/updated by service (#12150)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Documents now automatically track and display "created by" and "updated by" user information. - Document creation and update timestamps are now managed and shown more accurately. - Workspace and document metadata (name, avatar) updates are more responsive and reliable. - Document creation supports middleware for customizing properties and behavior. - **Improvements** - Simplified and unified event handling for document list updates, reducing redundant event subscriptions. - Enhanced integration of editor and theme settings into the document creation process. - Explicit Yjs document initialization for improved workspace stability and reliability. - Consolidated journal-related metadata display in document icons and titles for clarity. - **Bug Fixes** - Fixed inconsistencies in how workspace and document names are set and updated. - Improved accuracy of "last updated" indicators by handling timestamps automatically. - **Refactor** - Removed deprecated event subjects and direct metadata manipulation in favor of more robust, reactive patterns. - Streamlined document creation logic across various features (quick search, journal, recording, etc.). - Simplified user avatar display components and removed cloud metadata dependencies. - Removed legacy editor setting and theme service dependencies from multiple modules. - **Chores** - Updated internal APIs and interfaces to support new metadata and event handling mechanisms. - Cleaned up unused code and dependencies related to editor settings and theme services. - Skipped flaky end-to-end test to improve test suite stability. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
import type { DocMode, RootBlockModel } from '@blocksuite/affine/model';
|
||||
import { Entity } from '@toeverything/infra';
|
||||
import { throttle } from 'lodash-es';
|
||||
import type { Transaction } from 'yjs';
|
||||
|
||||
import type { DocProperties } from '../../db';
|
||||
import type { WorkspaceService } from '../../workspace';
|
||||
@@ -13,6 +15,25 @@ export class Doc extends Entity {
|
||||
private readonly workspaceService: WorkspaceService
|
||||
) {
|
||||
super();
|
||||
|
||||
const handleTransactionThrottled = throttle(
|
||||
(trx: Transaction) => {
|
||||
if (trx.local) {
|
||||
this.setUpdatedAt(Date.now());
|
||||
}
|
||||
},
|
||||
1000,
|
||||
{
|
||||
leading: true,
|
||||
trailing: true,
|
||||
}
|
||||
);
|
||||
this.yDoc.on('afterTransaction', handleTransactionThrottled);
|
||||
|
||||
this.disposables.push(() => {
|
||||
this.yDoc.off('afterTransaction', handleTransactionThrottled);
|
||||
handleTransactionThrottled.cancel();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -26,6 +47,7 @@ export class Doc extends Entity {
|
||||
return this.scope.props.docId;
|
||||
}
|
||||
|
||||
public readonly yDoc = this.scope.props.blockSuiteDoc.spaceDoc;
|
||||
public readonly blockSuiteDoc = this.scope.props.blockSuiteDoc;
|
||||
public readonly record = this.scope.props.record;
|
||||
|
||||
@@ -34,6 +56,26 @@ export class Doc extends Entity {
|
||||
readonly primaryMode$ = this.record.primaryMode$;
|
||||
readonly title$ = this.record.title$;
|
||||
readonly trash$ = this.record.trash$;
|
||||
readonly createdAt$ = this.record.createdAt$;
|
||||
readonly updatedAt$ = this.record.updatedAt$;
|
||||
readonly createdBy$ = this.record.createdBy$;
|
||||
readonly updatedBy$ = this.record.updatedBy$;
|
||||
|
||||
setCreatedAt(createdAt: number) {
|
||||
this.record.setMeta({ createDate: createdAt });
|
||||
}
|
||||
|
||||
setUpdatedAt(updatedAt: number) {
|
||||
this.record.setMeta({ updatedDate: updatedAt });
|
||||
}
|
||||
|
||||
setCreatedBy(createdBy: string) {
|
||||
this.setProperty('createdBy', createdBy);
|
||||
}
|
||||
|
||||
setUpdatedBy(updatedBy: string) {
|
||||
this.setProperty('updatedBy', updatedBy);
|
||||
}
|
||||
|
||||
customProperty$(propertyId: string) {
|
||||
return this.record.customProperty$(propertyId);
|
||||
|
||||
@@ -30,6 +30,12 @@ export class DocRecord extends Entity<{ id: string }> {
|
||||
{ id: this.id }
|
||||
);
|
||||
|
||||
property$(propertyId: string) {
|
||||
return this.properties$.selector(p => p[propertyId]) as LiveData<
|
||||
string | undefined | null
|
||||
>;
|
||||
}
|
||||
|
||||
customProperty$(propertyId: string) {
|
||||
return this.properties$.selector(
|
||||
p => p['custom:' + propertyId]
|
||||
@@ -87,4 +93,28 @@ export class DocRecord extends Entity<{ id: string }> {
|
||||
title$ = this.meta$.map(meta => meta.title ?? '');
|
||||
|
||||
trash$ = this.meta$.map(meta => meta.trash ?? false);
|
||||
|
||||
createdAt$ = this.meta$.map(meta => meta.createDate);
|
||||
|
||||
updatedAt$ = this.meta$.map(meta => meta.updatedDate);
|
||||
|
||||
createdBy$ = this.property$('createdBy');
|
||||
|
||||
updatedBy$ = this.property$('updatedBy');
|
||||
|
||||
setCreatedAt(createdAt: number) {
|
||||
this.setMeta({ createDate: createdAt });
|
||||
}
|
||||
|
||||
setUpdatedAt(updatedAt: number) {
|
||||
this.setMeta({ updatedDate: updatedAt });
|
||||
}
|
||||
|
||||
setCreatedBy(createdBy: string) {
|
||||
this.setProperty('createdBy', createdBy);
|
||||
}
|
||||
|
||||
setUpdatedBy(updatedBy: string) {
|
||||
this.setProperty('updatedBy', updatedBy);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,11 @@ import { createEvent } from '@toeverything/infra';
|
||||
|
||||
import type { Doc } from '../entities/doc';
|
||||
import type { DocRecord } from '../entities/record';
|
||||
import type { DocCreateOptions } from '../types';
|
||||
|
||||
export const DocCreated = createEvent<DocRecord>('DocCreated');
|
||||
export const DocCreated = createEvent<{
|
||||
doc: DocRecord;
|
||||
docCreateOptions: DocCreateOptions;
|
||||
}>('DocCreated');
|
||||
|
||||
export const DocInitialized = createEvent<Doc>('DocInitialized');
|
||||
|
||||
@@ -14,16 +14,23 @@ import { Doc } from './entities/doc';
|
||||
import { DocPropertyList } from './entities/property-list';
|
||||
import { DocRecord } from './entities/record';
|
||||
import { DocRecordList } from './entities/record-list';
|
||||
import { DocCreateMiddleware } from './providers/doc-create-middleware';
|
||||
import { DocScope } from './scopes/doc';
|
||||
import { DocService } from './services/doc';
|
||||
import { DocsService } from './services/docs';
|
||||
import { DocPropertiesStore } from './stores/doc-properties';
|
||||
import { DocsStore } from './stores/docs';
|
||||
|
||||
export { DocCreateMiddleware } from './providers/doc-create-middleware';
|
||||
|
||||
export function configureDocModule(framework: Framework) {
|
||||
framework
|
||||
.scope(WorkspaceScope)
|
||||
.service(DocsService, [DocsStore, DocPropertiesStore])
|
||||
.service(DocsService, [
|
||||
DocsStore,
|
||||
DocPropertiesStore,
|
||||
[DocCreateMiddleware],
|
||||
])
|
||||
.store(DocPropertiesStore, [WorkspaceService, WorkspaceDBService])
|
||||
.store(DocsStore, [WorkspaceService, DocPropertiesStore])
|
||||
.entity(DocRecord, [DocsStore, DocPropertiesStore])
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { createIdentifier } from '@toeverything/infra';
|
||||
|
||||
import type { DocRecord } from '../entities/record';
|
||||
import type { DocCreateOptions } from '../types';
|
||||
|
||||
export interface DocCreateMiddleware {
|
||||
beforeCreate?: (docCreateOptions: DocCreateOptions) => DocCreateOptions;
|
||||
afterCreate?: (doc: DocRecord, docCreateOptions: DocCreateOptions) => void;
|
||||
}
|
||||
|
||||
export const DocCreateMiddleware = createIdentifier<DocCreateMiddleware>(
|
||||
'DocCreateMiddleware'
|
||||
);
|
||||
@@ -1,6 +1,5 @@
|
||||
import { DebugLogger } from '@affine/debug';
|
||||
import { Unreachable } from '@affine/env/constant';
|
||||
import type { DocMode } from '@blocksuite/affine/model';
|
||||
import { replaceIdMiddleware } from '@blocksuite/affine/shared/adapters';
|
||||
import type { AffineTextAttributes } from '@blocksuite/affine/shared/types';
|
||||
import type { DeltaInsert } from '@blocksuite/affine/store';
|
||||
@@ -9,19 +8,18 @@ import { LiveData, ObjectPool, Service } from '@toeverything/infra';
|
||||
import { omitBy } from 'lodash-es';
|
||||
import { combineLatest, map } from 'rxjs';
|
||||
|
||||
import {
|
||||
type DocProps,
|
||||
initDocFromProps,
|
||||
} from '../../../blocksuite/initialization';
|
||||
import { initDocFromProps } from '../../../blocksuite/initialization';
|
||||
import type { DocProperties } from '../../db';
|
||||
import { getAFFiNEWorkspaceSchema } from '../../workspace';
|
||||
import type { Doc } from '../entities/doc';
|
||||
import { DocPropertyList } from '../entities/property-list';
|
||||
import { DocRecordList } from '../entities/record-list';
|
||||
import { DocCreated, DocInitialized } from '../events';
|
||||
import type { DocCreateMiddleware } from '../providers/doc-create-middleware';
|
||||
import { DocScope } from '../scopes/doc';
|
||||
import type { DocPropertiesStore } from '../stores/doc-properties';
|
||||
import type { DocsStore } from '../stores/docs';
|
||||
import type { DocCreateOptions } from '../types';
|
||||
import { DocService } from './doc';
|
||||
|
||||
const logger = new DebugLogger('DocsService');
|
||||
@@ -58,7 +56,8 @@ export class DocsService extends Service {
|
||||
|
||||
constructor(
|
||||
private readonly store: DocsStore,
|
||||
private readonly docPropertiesStore: DocPropertiesStore
|
||||
private readonly docPropertiesStore: DocPropertiesStore,
|
||||
private readonly docCreateMiddlewares: DocCreateMiddleware[]
|
||||
) {
|
||||
super();
|
||||
}
|
||||
@@ -110,16 +109,21 @@ export class DocsService extends Service {
|
||||
return { doc: obj, release };
|
||||
}
|
||||
|
||||
createDoc(
|
||||
options: {
|
||||
primaryMode?: DocMode;
|
||||
docProps?: DocProps;
|
||||
isTemplate?: boolean;
|
||||
} = {}
|
||||
) {
|
||||
const doc = this.store.createBlockSuiteDoc();
|
||||
initDocFromProps(doc, options.docProps);
|
||||
const docRecord = this.list.doc$(doc.id).value;
|
||||
createDoc(options: DocCreateOptions = {}) {
|
||||
for (const middleware of this.docCreateMiddlewares) {
|
||||
options = middleware.beforeCreate
|
||||
? middleware.beforeCreate(options)
|
||||
: options;
|
||||
}
|
||||
const id = this.store.createDoc(options.id);
|
||||
const docStore = this.store.getBlockSuiteDoc(id);
|
||||
if (!docStore) {
|
||||
throw new Error('Failed to create doc');
|
||||
}
|
||||
if (options.skipInit !== true) {
|
||||
initDocFromProps(docStore, options.docProps, options);
|
||||
}
|
||||
const docRecord = this.list.doc$(id).value;
|
||||
if (!docRecord) {
|
||||
throw new Unreachable();
|
||||
}
|
||||
@@ -129,7 +133,14 @@ export class DocsService extends Service {
|
||||
if (options.isTemplate) {
|
||||
docRecord.setProperty('isTemplate', true);
|
||||
}
|
||||
this.eventBus.emit(DocCreated, docRecord);
|
||||
for (const middleware of this.docCreateMiddlewares) {
|
||||
middleware.afterCreate?.(docRecord, options);
|
||||
}
|
||||
docRecord.setCreatedAt(Date.now());
|
||||
this.eventBus.emit(DocCreated, {
|
||||
doc: docRecord,
|
||||
docCreateOptions: options,
|
||||
});
|
||||
return docRecord;
|
||||
}
|
||||
|
||||
@@ -200,7 +211,14 @@ export class DocsService extends Service {
|
||||
schema: getAFFiNEWorkspaceSchema(),
|
||||
blobCRUD: collection.blobSync,
|
||||
docCRUD: {
|
||||
create: (id: string) => collection.createDoc(id).getStore({ id }),
|
||||
create: (id: string) => {
|
||||
this.createDoc({ id });
|
||||
const store = collection.getDoc(id)?.getStore({ id });
|
||||
if (!store) {
|
||||
throw new Error('Failed to create doc');
|
||||
}
|
||||
return store;
|
||||
},
|
||||
get: (id: string) => collection.getDoc(id)?.getStore({ id }) ?? null,
|
||||
delete: (id: string) => collection.removeDoc(id),
|
||||
},
|
||||
@@ -293,7 +311,14 @@ export class DocsService extends Service {
|
||||
schema: getAFFiNEWorkspaceSchema(),
|
||||
blobCRUD: collection.blobSync,
|
||||
docCRUD: {
|
||||
create: (id: string) => collection.createDoc(id).getStore({ id }),
|
||||
create: (id: string) => {
|
||||
this.createDoc({ id });
|
||||
const store = collection.getDoc(id)?.getStore({ id });
|
||||
if (!store) {
|
||||
throw new Error('Failed to create doc');
|
||||
}
|
||||
return store;
|
||||
},
|
||||
get: (id: string) => collection.getDoc(id)?.getStore({ id }) ?? null,
|
||||
delete: (id: string) => collection.removeDoc(id),
|
||||
},
|
||||
|
||||
@@ -6,8 +6,9 @@ import {
|
||||
yjsObserveByPath,
|
||||
yjsObserveDeep,
|
||||
} from '@toeverything/infra';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { distinctUntilChanged, map, switchMap } from 'rxjs';
|
||||
import { Array as YArray, Map as YMap } from 'yjs';
|
||||
import { Array as YArray, Map as YMap, transact } from 'yjs';
|
||||
|
||||
import type { WorkspaceService } from '../../workspace';
|
||||
import type { DocPropertiesStore } from './doc-properties';
|
||||
@@ -32,9 +33,33 @@ export class DocsStore extends Store {
|
||||
return this.workspaceService.workspace.docCollection;
|
||||
}
|
||||
|
||||
createBlockSuiteDoc() {
|
||||
const doc = this.workspaceService.workspace.docCollection.createDoc();
|
||||
return doc.getStore({ id: doc.id });
|
||||
createDoc(docId?: string) {
|
||||
const id = docId ?? nanoid();
|
||||
|
||||
transact(
|
||||
this.workspaceService.workspace.rootYDoc,
|
||||
() => {
|
||||
const docs = this.workspaceService.workspace.rootYDoc
|
||||
.getMap('meta')
|
||||
.get('pages');
|
||||
|
||||
if (!docs || !(docs instanceof YArray)) {
|
||||
return;
|
||||
}
|
||||
|
||||
docs.push([
|
||||
new YMap([
|
||||
['id', id],
|
||||
['title', ''],
|
||||
['createDate', Date.now()],
|
||||
['tags', new YArray()],
|
||||
]),
|
||||
]);
|
||||
},
|
||||
{ force: true }
|
||||
);
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
watchDocIds() {
|
||||
|
||||
11
packages/frontend/core/src/modules/doc/types.ts
Normal file
11
packages/frontend/core/src/modules/doc/types.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import type { DocProps } from '@affine/core/blocksuite/initialization';
|
||||
import type { DocMode } from '@blocksuite/affine/model';
|
||||
|
||||
export interface DocCreateOptions {
|
||||
id?: string;
|
||||
title?: string;
|
||||
primaryMode?: DocMode;
|
||||
skipInit?: boolean;
|
||||
docProps?: DocProps;
|
||||
isTemplate?: boolean;
|
||||
}
|
||||
Reference in New Issue
Block a user