refactor(core): move infra modules to core (#9207)
This commit is contained in:
54
packages/frontend/core/src/modules/doc/constants.ts
Normal file
54
packages/frontend/core/src/modules/doc/constants.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import type { DocCustomPropertyInfo } from '../db';
|
||||
|
||||
/**
|
||||
* default built-in custom property, user can update and delete them
|
||||
*
|
||||
* 'id' and 'type' is request, 'index' is a manually maintained incremental key.
|
||||
*/
|
||||
export const BUILT_IN_CUSTOM_PROPERTY_TYPE = [
|
||||
{
|
||||
id: 'tags',
|
||||
type: 'tags',
|
||||
index: 'a0000001',
|
||||
},
|
||||
{
|
||||
id: 'docPrimaryMode',
|
||||
type: 'docPrimaryMode',
|
||||
show: 'always-hide',
|
||||
index: 'a0000002',
|
||||
},
|
||||
{
|
||||
id: 'journal',
|
||||
type: 'journal',
|
||||
show: 'always-hide',
|
||||
index: 'a0000003',
|
||||
},
|
||||
{
|
||||
id: 'createdAt',
|
||||
type: 'createdAt',
|
||||
index: 'a0000004',
|
||||
},
|
||||
{
|
||||
id: 'updatedAt',
|
||||
type: 'updatedAt',
|
||||
index: 'a0000005',
|
||||
},
|
||||
{
|
||||
id: 'createdBy',
|
||||
type: 'createdBy',
|
||||
show: 'always-hide',
|
||||
index: 'a0000006',
|
||||
},
|
||||
{
|
||||
id: 'edgelessTheme',
|
||||
type: 'edgelessTheme',
|
||||
show: 'always-hide',
|
||||
index: 'a0000007',
|
||||
},
|
||||
{
|
||||
id: 'pageWidth',
|
||||
type: 'pageWidth',
|
||||
show: 'always-hide',
|
||||
index: 'a0000008',
|
||||
},
|
||||
] as DocCustomPropertyInfo[];
|
||||
86
packages/frontend/core/src/modules/doc/entities/doc.ts
Normal file
86
packages/frontend/core/src/modules/doc/entities/doc.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import type { DocMode, RootBlockModel } from '@blocksuite/affine/blocks';
|
||||
import { Entity } from '@toeverything/infra';
|
||||
|
||||
import type { WorkspaceService } from '../../workspace';
|
||||
import type { DocScope } from '../scopes/doc';
|
||||
import type { DocsStore } from '../stores/docs';
|
||||
|
||||
export class Doc extends Entity {
|
||||
constructor(
|
||||
public readonly scope: DocScope,
|
||||
private readonly store: DocsStore,
|
||||
private readonly workspaceService: WorkspaceService
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* for convenience
|
||||
*/
|
||||
get workspace() {
|
||||
return this.workspaceService.workspace;
|
||||
}
|
||||
|
||||
get id() {
|
||||
return this.scope.props.docId;
|
||||
}
|
||||
|
||||
public readonly blockSuiteDoc = this.scope.props.blockSuiteDoc;
|
||||
public readonly record = this.scope.props.record;
|
||||
|
||||
readonly meta$ = this.record.meta$;
|
||||
readonly properties$ = this.record.properties$;
|
||||
readonly primaryMode$ = this.record.primaryMode$;
|
||||
readonly title$ = this.record.title$;
|
||||
readonly trash$ = this.record.trash$;
|
||||
|
||||
customProperty$(propertyId: string) {
|
||||
return this.record.customProperty$(propertyId);
|
||||
}
|
||||
|
||||
setCustomProperty(propertyId: string, value: string) {
|
||||
return this.record.setCustomProperty(propertyId, value);
|
||||
}
|
||||
|
||||
setPrimaryMode(mode: DocMode) {
|
||||
return this.record.setPrimaryMode(mode);
|
||||
}
|
||||
|
||||
getPrimaryMode() {
|
||||
return this.record.getPrimaryMode();
|
||||
}
|
||||
|
||||
togglePrimaryMode() {
|
||||
this.setPrimaryMode(
|
||||
(this.getPrimaryMode() === 'edgeless' ? 'page' : 'edgeless') as DocMode
|
||||
);
|
||||
}
|
||||
|
||||
moveToTrash() {
|
||||
return this.record.moveToTrash();
|
||||
}
|
||||
|
||||
restoreFromTrash() {
|
||||
return this.record.restoreFromTrash();
|
||||
}
|
||||
|
||||
waitForSyncReady() {
|
||||
return this.store.waitForDocLoadReady(this.id);
|
||||
}
|
||||
|
||||
setPriorityLoad(priority: number) {
|
||||
return this.store.setPriorityLoad(this.id, priority);
|
||||
}
|
||||
|
||||
changeDocTitle(newTitle: string) {
|
||||
const pageBlock = this.blockSuiteDoc.getBlocksByFlavour('affine:page').at(0)
|
||||
?.model as RootBlockModel | undefined;
|
||||
if (pageBlock) {
|
||||
this.blockSuiteDoc.transact(() => {
|
||||
pageBlock.title.delete(0, pageBlock.title.length);
|
||||
pageBlock.title.insert(newTitle, 0);
|
||||
});
|
||||
this.record.setMeta({ title: newTitle });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import {
|
||||
Entity,
|
||||
generateFractionalIndexingKeyBetween,
|
||||
LiveData,
|
||||
} from '@toeverything/infra';
|
||||
|
||||
import type { DocCustomPropertyInfo } from '../../db/schema/schema';
|
||||
import type { DocPropertiesStore } from '../stores/doc-properties';
|
||||
|
||||
export class DocPropertyList extends Entity {
|
||||
constructor(private readonly docPropertiesStore: DocPropertiesStore) {
|
||||
super();
|
||||
}
|
||||
|
||||
properties$ = LiveData.from(
|
||||
this.docPropertiesStore.watchDocPropertyInfoList(),
|
||||
[]
|
||||
);
|
||||
|
||||
sortedProperties$ = this.properties$.map(list =>
|
||||
// default index key is '', so always before any others
|
||||
list.toSorted((a, b) => ((a.index ?? '') > (b.index ?? '') ? 1 : -1))
|
||||
);
|
||||
|
||||
propertyInfo$(id: string) {
|
||||
return this.properties$.map(list => list.find(info => info.id === id));
|
||||
}
|
||||
|
||||
updatePropertyInfo(id: string, properties: Partial<DocCustomPropertyInfo>) {
|
||||
this.docPropertiesStore.updateDocPropertyInfo(id, properties);
|
||||
}
|
||||
|
||||
createProperty(
|
||||
properties: Omit<DocCustomPropertyInfo, 'id'> & { id?: string }
|
||||
) {
|
||||
return this.docPropertiesStore.createDocPropertyInfo(properties);
|
||||
}
|
||||
|
||||
removeProperty(id: string) {
|
||||
this.docPropertiesStore.removeDocPropertyInfo(id);
|
||||
}
|
||||
|
||||
indexAt(at: 'before' | 'after', targetId?: string) {
|
||||
const sortedChildren = this.sortedProperties$.value.filter(
|
||||
node => node.index
|
||||
) as (DocCustomPropertyInfo & { index: string })[];
|
||||
const targetIndex = targetId
|
||||
? sortedChildren.findIndex(node => node.id === targetId)
|
||||
: -1;
|
||||
if (targetIndex === -1) {
|
||||
if (at === 'before') {
|
||||
const first = sortedChildren.at(0);
|
||||
return generateFractionalIndexingKeyBetween(null, first?.index ?? null);
|
||||
} else {
|
||||
const last = sortedChildren.at(-1);
|
||||
return generateFractionalIndexingKeyBetween(last?.index ?? null, null);
|
||||
}
|
||||
} else {
|
||||
const target = sortedChildren[targetIndex];
|
||||
const before: DocCustomPropertyInfo | null =
|
||||
sortedChildren[targetIndex - 1] || null;
|
||||
const after: DocCustomPropertyInfo | null =
|
||||
sortedChildren[targetIndex + 1] || null;
|
||||
if (at === 'before') {
|
||||
return generateFractionalIndexingKeyBetween(
|
||||
before?.index ?? null,
|
||||
target.index
|
||||
);
|
||||
} else {
|
||||
return generateFractionalIndexingKeyBetween(
|
||||
target.index,
|
||||
after?.index ?? null
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import type { DocMode } from '@blocksuite/affine/blocks';
|
||||
import { Entity, LiveData } from '@toeverything/infra';
|
||||
import { map } from 'rxjs';
|
||||
|
||||
import type { DocsStore } from '../stores/docs';
|
||||
import { DocRecord } from './record';
|
||||
|
||||
export class DocRecordList extends Entity {
|
||||
constructor(private readonly store: DocsStore) {
|
||||
super();
|
||||
}
|
||||
|
||||
private readonly pool = new Map<string, DocRecord>();
|
||||
|
||||
public readonly docsMap$ = LiveData.from<Map<string, DocRecord>>(
|
||||
this.store.watchDocIds().pipe(
|
||||
map(
|
||||
ids =>
|
||||
new Map(
|
||||
ids.map(id => {
|
||||
const exists = this.pool.get(id);
|
||||
if (exists) {
|
||||
return [id, exists];
|
||||
}
|
||||
const record = this.framework.createEntity(DocRecord, { id });
|
||||
this.pool.set(id, record);
|
||||
return [id, record];
|
||||
})
|
||||
)
|
||||
)
|
||||
),
|
||||
new Map()
|
||||
);
|
||||
|
||||
public readonly docs$ = this.docsMap$.selector(d => Array.from(d.values()));
|
||||
|
||||
public readonly trashDocs$ = LiveData.from<DocRecord[]>(
|
||||
this.store.watchTrashDocIds().pipe(
|
||||
map(ids =>
|
||||
ids.map(id => {
|
||||
const exists = this.pool.get(id);
|
||||
if (exists) {
|
||||
return exists;
|
||||
}
|
||||
const record = this.framework.createEntity(DocRecord, { id });
|
||||
this.pool.set(id, record);
|
||||
return record;
|
||||
})
|
||||
)
|
||||
),
|
||||
[]
|
||||
);
|
||||
|
||||
public readonly isReady$ = LiveData.from(
|
||||
this.store.watchDocListReady(),
|
||||
false
|
||||
);
|
||||
|
||||
public doc$(id: string) {
|
||||
return this.docsMap$.selector(map => map.get(id));
|
||||
}
|
||||
|
||||
public setPrimaryMode(id: string, mode: DocMode) {
|
||||
return this.store.setDocPrimaryModeSetting(id, mode);
|
||||
}
|
||||
|
||||
public getPrimaryMode(id: string) {
|
||||
return this.store.getDocPrimaryModeSetting(id);
|
||||
}
|
||||
|
||||
public togglePrimaryMode(id: string) {
|
||||
const mode = (
|
||||
this.getPrimaryMode(id) === 'edgeless' ? 'page' : 'edgeless'
|
||||
) as DocMode;
|
||||
this.setPrimaryMode(id, mode);
|
||||
return this.getPrimaryMode(id);
|
||||
}
|
||||
|
||||
public primaryMode$(id: string) {
|
||||
return LiveData.from(
|
||||
this.store.watchDocPrimaryModeSetting(id),
|
||||
this.getPrimaryMode(id)
|
||||
);
|
||||
}
|
||||
}
|
||||
79
packages/frontend/core/src/modules/doc/entities/record.ts
Normal file
79
packages/frontend/core/src/modules/doc/entities/record.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import type { DocMode } from '@blocksuite/affine/blocks';
|
||||
import type { DocMeta } from '@blocksuite/affine/store';
|
||||
import { Entity, LiveData } from '@toeverything/infra';
|
||||
|
||||
import type { DocProperties } from '../../db';
|
||||
import type { DocPropertiesStore } from '../stores/doc-properties';
|
||||
import type { DocsStore } from '../stores/docs';
|
||||
|
||||
/**
|
||||
* # DocRecord
|
||||
*
|
||||
* Some data you can use without open a doc.
|
||||
*/
|
||||
export class DocRecord extends Entity<{ id: string }> {
|
||||
id: string = this.props.id;
|
||||
constructor(
|
||||
private readonly docsStore: DocsStore,
|
||||
private readonly docPropertiesStore: DocPropertiesStore
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
meta$ = LiveData.from<Partial<DocMeta>>(
|
||||
this.docsStore.watchDocMeta(this.id),
|
||||
{}
|
||||
);
|
||||
|
||||
properties$ = LiveData.from<DocProperties>(
|
||||
this.docPropertiesStore.watchDocProperties(this.id),
|
||||
{ id: this.id }
|
||||
);
|
||||
|
||||
customProperty$(propertyId: string) {
|
||||
return this.properties$.selector(
|
||||
p => p['custom:' + propertyId]
|
||||
) as LiveData<string | undefined | null>;
|
||||
}
|
||||
|
||||
setCustomProperty(propertyId: string, value: string) {
|
||||
this.docPropertiesStore.updateDocProperties(this.id, {
|
||||
['custom:' + propertyId]: value,
|
||||
});
|
||||
}
|
||||
|
||||
setProperty(propertyId: string, value: string) {
|
||||
this.docPropertiesStore.updateDocProperties(this.id, {
|
||||
[propertyId]: value,
|
||||
});
|
||||
}
|
||||
|
||||
setMeta(meta: Partial<DocMeta>): void {
|
||||
this.docsStore.setDocMeta(this.id, meta);
|
||||
}
|
||||
|
||||
primaryMode$: LiveData<DocMode> = LiveData.from(
|
||||
this.docsStore.watchDocPrimaryModeSetting(this.id),
|
||||
'page' as DocMode
|
||||
).map(mode => (mode === 'edgeless' ? 'edgeless' : 'page') as DocMode);
|
||||
|
||||
setPrimaryMode(mode: DocMode) {
|
||||
return this.docsStore.setDocPrimaryModeSetting(this.id, mode);
|
||||
}
|
||||
|
||||
getPrimaryMode() {
|
||||
return this.docsStore.getDocPrimaryModeSetting(this.id);
|
||||
}
|
||||
|
||||
moveToTrash() {
|
||||
return this.setMeta({ trash: true, trashDate: Date.now() });
|
||||
}
|
||||
|
||||
restoreFromTrash() {
|
||||
return this.setMeta({ trash: false, trashDate: undefined });
|
||||
}
|
||||
|
||||
title$ = this.meta$.map(meta => meta.title ?? '');
|
||||
|
||||
trash$ = this.meta$.map(meta => meta.trash ?? false);
|
||||
}
|
||||
5
packages/frontend/core/src/modules/doc/events/index.ts
Normal file
5
packages/frontend/core/src/modules/doc/events/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { createEvent } from '@toeverything/infra';
|
||||
|
||||
import type { DocRecord } from '../entities/record';
|
||||
|
||||
export const DocCreated = createEvent<DocRecord>('DocCreated');
|
||||
35
packages/frontend/core/src/modules/doc/index.ts
Normal file
35
packages/frontend/core/src/modules/doc/index.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
export { Doc } from './entities/doc';
|
||||
export { DocRecord } from './entities/record';
|
||||
export { DocRecordList } from './entities/record-list';
|
||||
export { DocCreated } from './events';
|
||||
export { DocScope } from './scopes/doc';
|
||||
export { DocService } from './services/doc';
|
||||
export { DocsService } from './services/docs';
|
||||
|
||||
import type { Framework } from '@toeverything/infra';
|
||||
|
||||
import { WorkspaceDBService } from '../db';
|
||||
import { WorkspaceScope, WorkspaceService } from '../workspace';
|
||||
import { Doc } from './entities/doc';
|
||||
import { DocPropertyList } from './entities/property-list';
|
||||
import { DocRecord } from './entities/record';
|
||||
import { DocRecordList } from './entities/record-list';
|
||||
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 function configureDocModule(framework: Framework) {
|
||||
framework
|
||||
.scope(WorkspaceScope)
|
||||
.service(DocsService, [DocsStore])
|
||||
.store(DocPropertiesStore, [WorkspaceService, WorkspaceDBService])
|
||||
.store(DocsStore, [WorkspaceService, DocPropertiesStore])
|
||||
.entity(DocRecord, [DocsStore, DocPropertiesStore])
|
||||
.entity(DocRecordList, [DocsStore])
|
||||
.entity(DocPropertyList, [DocPropertiesStore])
|
||||
.scope(DocScope)
|
||||
.entity(Doc, [DocScope, DocsStore, WorkspaceService])
|
||||
.service(DocService);
|
||||
}
|
||||
10
packages/frontend/core/src/modules/doc/scopes/doc.ts
Normal file
10
packages/frontend/core/src/modules/doc/scopes/doc.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import type { Doc as BlockSuiteDoc } from '@blocksuite/affine/store';
|
||||
import { Scope } from '@toeverything/infra';
|
||||
|
||||
import type { DocRecord } from '../entities/record';
|
||||
|
||||
export class DocScope extends Scope<{
|
||||
docId: string;
|
||||
record: DocRecord;
|
||||
blockSuiteDoc: BlockSuiteDoc;
|
||||
}> {}
|
||||
7
packages/frontend/core/src/modules/doc/services/doc.ts
Normal file
7
packages/frontend/core/src/modules/doc/services/doc.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { Service } from '@toeverything/infra';
|
||||
|
||||
import { Doc } from '../entities/doc';
|
||||
|
||||
export class DocService extends Service {
|
||||
public readonly doc = this.framework.createEntity(Doc);
|
||||
}
|
||||
127
packages/frontend/core/src/modules/doc/services/docs.ts
Normal file
127
packages/frontend/core/src/modules/doc/services/docs.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
import { DebugLogger } from '@affine/debug';
|
||||
import { Unreachable } from '@affine/env/constant';
|
||||
import type { DocMode } from '@blocksuite/affine/blocks';
|
||||
import type { DeltaInsert } from '@blocksuite/affine/inline';
|
||||
import type { AffineTextAttributes } from '@blocksuite/affine-shared/types';
|
||||
import {
|
||||
type DocProps,
|
||||
initDocFromProps,
|
||||
ObjectPool,
|
||||
Service,
|
||||
} from '@toeverything/infra';
|
||||
|
||||
import type { Doc } from '../entities/doc';
|
||||
import { DocPropertyList } from '../entities/property-list';
|
||||
import { DocRecordList } from '../entities/record-list';
|
||||
import { DocCreated } from '../events';
|
||||
import { DocScope } from '../scopes/doc';
|
||||
import type { DocsStore } from '../stores/docs';
|
||||
import { DocService } from './doc';
|
||||
|
||||
const logger = new DebugLogger('DocsService');
|
||||
|
||||
export class DocsService extends Service {
|
||||
list = this.framework.createEntity(DocRecordList);
|
||||
|
||||
pool = new ObjectPool<string, Doc>({
|
||||
onDelete(obj) {
|
||||
obj.scope.dispose();
|
||||
},
|
||||
});
|
||||
|
||||
propertyList = this.framework.createEntity(DocPropertyList);
|
||||
|
||||
constructor(private readonly store: DocsStore) {
|
||||
super();
|
||||
}
|
||||
|
||||
open(docId: string) {
|
||||
const docRecord = this.list.doc$(docId).value;
|
||||
if (!docRecord) {
|
||||
throw new Error('Doc record not found');
|
||||
}
|
||||
const blockSuiteDoc = this.store.getBlockSuiteDoc(docId);
|
||||
if (!blockSuiteDoc) {
|
||||
throw new Error('Doc not found');
|
||||
}
|
||||
|
||||
const exists = this.pool.get(docId);
|
||||
if (exists) {
|
||||
return { doc: exists.obj, release: exists.release };
|
||||
}
|
||||
|
||||
const docScope = this.framework.createScope(DocScope, {
|
||||
docId,
|
||||
blockSuiteDoc,
|
||||
record: docRecord,
|
||||
});
|
||||
|
||||
try {
|
||||
blockSuiteDoc.load();
|
||||
} catch (e) {
|
||||
logger.error('Failed to load doc', {
|
||||
docId,
|
||||
error: e,
|
||||
});
|
||||
}
|
||||
|
||||
const doc = docScope.get(DocService).doc;
|
||||
|
||||
const { obj, release } = this.pool.put(docId, doc);
|
||||
|
||||
return { doc: obj, release };
|
||||
}
|
||||
|
||||
createDoc(
|
||||
options: {
|
||||
primaryMode?: DocMode;
|
||||
docProps?: DocProps;
|
||||
} = {}
|
||||
) {
|
||||
const doc = this.store.createBlockSuiteDoc();
|
||||
initDocFromProps(doc, options.docProps);
|
||||
this.store.markDocSyncStateAsReady(doc.id);
|
||||
const docRecord = this.list.doc$(doc.id).value;
|
||||
if (!docRecord) {
|
||||
throw new Unreachable();
|
||||
}
|
||||
if (options.primaryMode) {
|
||||
docRecord.setPrimaryMode(options.primaryMode);
|
||||
}
|
||||
this.eventBus.emit(DocCreated, docRecord);
|
||||
return docRecord;
|
||||
}
|
||||
|
||||
async addLinkedDoc(targetDocId: string, linkedDocId: string) {
|
||||
const { doc, release } = this.open(targetDocId);
|
||||
doc.setPriorityLoad(10);
|
||||
await doc.waitForSyncReady();
|
||||
const text = new doc.blockSuiteDoc.Text([
|
||||
{
|
||||
insert: ' ',
|
||||
attributes: {
|
||||
reference: {
|
||||
type: 'LinkedPage',
|
||||
pageId: linkedDocId,
|
||||
},
|
||||
},
|
||||
},
|
||||
] as DeltaInsert<AffineTextAttributes>[]);
|
||||
const [frame] = doc.blockSuiteDoc.getBlocksByFlavour('affine:note');
|
||||
frame &&
|
||||
doc.blockSuiteDoc.addBlock(
|
||||
'affine:paragraph' as never, // TODO(eyhn): fix type
|
||||
{ text },
|
||||
frame.id
|
||||
);
|
||||
release();
|
||||
}
|
||||
|
||||
async changeDocTitle(docId: string, newTitle: string) {
|
||||
const { doc, release } = this.open(docId);
|
||||
doc.setPriorityLoad(10);
|
||||
await doc.waitForSyncReady();
|
||||
doc.changeDocTitle(newTitle);
|
||||
release();
|
||||
}
|
||||
}
|
||||
258
packages/frontend/core/src/modules/doc/stores/doc-properties.ts
Normal file
258
packages/frontend/core/src/modules/doc/stores/doc-properties.ts
Normal file
@@ -0,0 +1,258 @@
|
||||
import { Store, yjsObserveByPath, yjsObserveDeep } from '@toeverything/infra';
|
||||
import { differenceBy, isNil, omitBy } from 'lodash-es';
|
||||
import { combineLatest, map, switchMap } from 'rxjs';
|
||||
import { AbstractType as YAbstractType } from 'yjs';
|
||||
|
||||
import type { WorkspaceDBService } from '../../db';
|
||||
import type {
|
||||
DocCustomPropertyInfo,
|
||||
DocProperties,
|
||||
} from '../../db/schema/schema';
|
||||
import type { WorkspaceService } from '../../workspace';
|
||||
import { BUILT_IN_CUSTOM_PROPERTY_TYPE } from '../constants';
|
||||
|
||||
interface LegacyDocProperties {
|
||||
custom?: Record<string, { value: unknown } | undefined>;
|
||||
system?: Record<string, { value: unknown } | undefined>;
|
||||
}
|
||||
|
||||
type LegacyDocPropertyInfo = {
|
||||
id?: string;
|
||||
name?: string;
|
||||
type?: string;
|
||||
icon?: string;
|
||||
};
|
||||
|
||||
type LegacyDocPropertyInfoList = Record<
|
||||
string,
|
||||
LegacyDocPropertyInfo | undefined
|
||||
>;
|
||||
|
||||
export class DocPropertiesStore extends Store {
|
||||
constructor(
|
||||
private readonly workspaceService: WorkspaceService,
|
||||
private readonly dbService: WorkspaceDBService
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
updateDocProperties(id: string, config: Partial<DocProperties>) {
|
||||
return this.dbService.db.docProperties.create({
|
||||
id,
|
||||
...config,
|
||||
});
|
||||
}
|
||||
|
||||
getDocPropertyInfoList() {
|
||||
const db = this.dbService.db.docCustomPropertyInfo.find();
|
||||
const legacy = this.upgradeLegacyDocPropertyInfoList(
|
||||
this.getLegacyDocPropertyInfoList()
|
||||
);
|
||||
const builtIn = BUILT_IN_CUSTOM_PROPERTY_TYPE;
|
||||
const withLegacy = [...db, ...differenceBy(legacy, db, i => i.id)];
|
||||
const all = [
|
||||
...withLegacy,
|
||||
...differenceBy(builtIn, withLegacy, i => i.id),
|
||||
];
|
||||
return all.filter(i => !i.isDeleted);
|
||||
}
|
||||
|
||||
createDocPropertyInfo(
|
||||
config: Omit<DocCustomPropertyInfo, 'id'> & { id?: string }
|
||||
) {
|
||||
return this.dbService.db.docCustomPropertyInfo.create(config);
|
||||
}
|
||||
|
||||
removeDocPropertyInfo(id: string) {
|
||||
this.updateDocPropertyInfo(id, {
|
||||
additionalData: {}, // also remove additional data to reduce size
|
||||
isDeleted: true,
|
||||
});
|
||||
}
|
||||
|
||||
updateDocPropertyInfo(id: string, config: Partial<DocCustomPropertyInfo>) {
|
||||
const needMigration = !this.dbService.db.docCustomPropertyInfo.get(id);
|
||||
const isBuiltIn =
|
||||
needMigration && BUILT_IN_CUSTOM_PROPERTY_TYPE.some(i => i.id === id);
|
||||
if (isBuiltIn) {
|
||||
this.createPropertyFromBuiltIn(id, config);
|
||||
} else if (needMigration) {
|
||||
// if this property is not in db, we need to migration it from legacy to db, only type and name is needed
|
||||
this.migrateLegacyDocPropertyInfo(id, config);
|
||||
} else {
|
||||
this.dbService.db.docCustomPropertyInfo.update(id, config);
|
||||
}
|
||||
}
|
||||
|
||||
migrateLegacyDocPropertyInfo(
|
||||
id: string,
|
||||
override: Partial<DocCustomPropertyInfo>
|
||||
) {
|
||||
const legacy = this.getLegacyDocPropertyInfo(id);
|
||||
this.dbService.db.docCustomPropertyInfo.create({
|
||||
id,
|
||||
type:
|
||||
legacy?.type ??
|
||||
'unknown' /* should never reach here, just for safety, we need handle unknown property type */,
|
||||
name: legacy?.name,
|
||||
...override,
|
||||
});
|
||||
}
|
||||
|
||||
createPropertyFromBuiltIn(
|
||||
id: string,
|
||||
override: Partial<DocCustomPropertyInfo>
|
||||
) {
|
||||
const builtIn = BUILT_IN_CUSTOM_PROPERTY_TYPE.find(i => i.id === id);
|
||||
if (!builtIn) {
|
||||
return;
|
||||
}
|
||||
this.createDocPropertyInfo({ ...builtIn, ...override });
|
||||
}
|
||||
|
||||
watchDocPropertyInfoList() {
|
||||
return combineLatest([
|
||||
this.watchLegacyDocPropertyInfoList().pipe(
|
||||
map(this.upgradeLegacyDocPropertyInfoList)
|
||||
),
|
||||
this.dbService.db.docCustomPropertyInfo.find$(),
|
||||
]).pipe(
|
||||
map(([legacy, db]) => {
|
||||
const builtIn = BUILT_IN_CUSTOM_PROPERTY_TYPE;
|
||||
const withLegacy = [...db, ...differenceBy(legacy, db, i => i.id)];
|
||||
const all = [
|
||||
...withLegacy,
|
||||
...differenceBy(builtIn, withLegacy, i => i.id),
|
||||
];
|
||||
return all.filter(i => !i.isDeleted);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
getDocProperties(id: string) {
|
||||
return {
|
||||
...this.upgradeLegacyDocProperties(this.getLegacyDocProperties(id)),
|
||||
...omitBy(this.dbService.db.docProperties.get(id), isNil),
|
||||
// db always override legacy, but nil value should not override
|
||||
};
|
||||
}
|
||||
|
||||
watchDocProperties(id: string) {
|
||||
return combineLatest([
|
||||
this.watchLegacyDocProperties(id).pipe(
|
||||
map(this.upgradeLegacyDocProperties)
|
||||
),
|
||||
this.dbService.db.docProperties.get$(id),
|
||||
]).pipe(
|
||||
map(
|
||||
([legacy, db]) =>
|
||||
({
|
||||
...legacy,
|
||||
...omitBy(db, isNil), // db always override legacy, but nil value should not override
|
||||
}) as DocProperties
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private upgradeLegacyDocProperties(properties?: LegacyDocProperties) {
|
||||
if (!properties) {
|
||||
return {};
|
||||
}
|
||||
const newProperties: Record<string, string> = {};
|
||||
for (const [key, info] of Object.entries(properties.system ?? {})) {
|
||||
if (info?.value !== undefined && info.value !== null) {
|
||||
newProperties[key] = info.value.toString();
|
||||
}
|
||||
}
|
||||
for (const [key, info] of Object.entries(properties.custom ?? {})) {
|
||||
if (info?.value !== undefined && info.value !== null) {
|
||||
newProperties['custom:' + key] = info.value.toString();
|
||||
}
|
||||
}
|
||||
return newProperties;
|
||||
}
|
||||
|
||||
private upgradeLegacyDocPropertyInfoList(
|
||||
infoList?: LegacyDocPropertyInfoList
|
||||
) {
|
||||
if (!infoList) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const newInfoList: DocCustomPropertyInfo[] = [];
|
||||
|
||||
for (const [id, info] of Object.entries(infoList ?? {})) {
|
||||
if (info?.type) {
|
||||
newInfoList.push({
|
||||
id,
|
||||
name: info.name,
|
||||
type: info.type,
|
||||
icon: info.icon,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return newInfoList;
|
||||
}
|
||||
|
||||
private getLegacyDocProperties(id: string) {
|
||||
return this.workspaceService.workspace.rootYDoc
|
||||
.getMap<any>('affine:workspace-properties')
|
||||
.get('pageProperties')
|
||||
?.get(id)
|
||||
?.toJSON() as LegacyDocProperties | undefined;
|
||||
}
|
||||
|
||||
private watchLegacyDocProperties(id: string) {
|
||||
return yjsObserveByPath(
|
||||
this.workspaceService.workspace.rootYDoc.getMap<any>(
|
||||
'affine:workspace-properties'
|
||||
),
|
||||
`pageProperties.${id}`
|
||||
).pipe(
|
||||
switchMap(yjsObserveDeep),
|
||||
map(
|
||||
p =>
|
||||
(p instanceof YAbstractType ? p.toJSON() : p) as
|
||||
| LegacyDocProperties
|
||||
| undefined
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private getLegacyDocPropertyInfoList() {
|
||||
return this.workspaceService.workspace.rootYDoc
|
||||
.getMap<any>('affine:workspace-properties')
|
||||
.get('schema')
|
||||
?.get('pageProperties')
|
||||
?.get('custom')
|
||||
?.toJSON() as LegacyDocPropertyInfoList | undefined;
|
||||
}
|
||||
|
||||
private watchLegacyDocPropertyInfoList() {
|
||||
return yjsObserveByPath(
|
||||
this.workspaceService.workspace.rootYDoc.getMap<any>(
|
||||
'affine:workspace-properties'
|
||||
),
|
||||
'schema.pageProperties.custom'
|
||||
).pipe(
|
||||
switchMap(yjsObserveDeep),
|
||||
map(
|
||||
p =>
|
||||
(p instanceof YAbstractType ? p.toJSON() : p) as
|
||||
| LegacyDocPropertyInfoList
|
||||
| undefined
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private getLegacyDocPropertyInfo(id: string) {
|
||||
return this.workspaceService.workspace.rootYDoc
|
||||
.getMap<any>('affine:workspace-properties')
|
||||
.get('schema')
|
||||
?.get('pageProperties')
|
||||
?.get('custom')
|
||||
?.get(id)
|
||||
?.toJSON() as LegacyDocPropertyInfo | undefined;
|
||||
}
|
||||
}
|
||||
144
packages/frontend/core/src/modules/doc/stores/docs.ts
Normal file
144
packages/frontend/core/src/modules/doc/stores/docs.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
import type { DocMode } from '@blocksuite/affine/blocks';
|
||||
import type { DocMeta } from '@blocksuite/affine/store';
|
||||
import {
|
||||
Store,
|
||||
yjsObserve,
|
||||
yjsObserveByPath,
|
||||
yjsObserveDeep,
|
||||
} from '@toeverything/infra';
|
||||
import { distinctUntilChanged, map, switchMap } from 'rxjs';
|
||||
import { Array as YArray, Map as YMap } from 'yjs';
|
||||
|
||||
import type { WorkspaceService } from '../../workspace';
|
||||
import type { DocPropertiesStore } from './doc-properties';
|
||||
|
||||
export class DocsStore extends Store {
|
||||
constructor(
|
||||
private readonly workspaceService: WorkspaceService,
|
||||
private readonly docPropertiesStore: DocPropertiesStore
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
getBlockSuiteDoc(id: string) {
|
||||
return this.workspaceService.workspace.docCollection.getDoc(id);
|
||||
}
|
||||
|
||||
createBlockSuiteDoc() {
|
||||
return this.workspaceService.workspace.docCollection.createDoc();
|
||||
}
|
||||
|
||||
watchDocIds() {
|
||||
return yjsObserveByPath(
|
||||
this.workspaceService.workspace.rootYDoc.getMap('meta'),
|
||||
'pages'
|
||||
).pipe(
|
||||
switchMap(yjsObserve),
|
||||
map(meta => {
|
||||
if (meta instanceof YArray) {
|
||||
return meta.map(v => v.get('id') as string);
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
watchTrashDocIds() {
|
||||
return yjsObserveByPath(
|
||||
this.workspaceService.workspace.rootYDoc.getMap('meta'),
|
||||
'pages'
|
||||
).pipe(
|
||||
switchMap(yjsObserveDeep),
|
||||
map(meta => {
|
||||
if (meta instanceof YArray) {
|
||||
return meta
|
||||
.map(v => (v.get('trash') ? v.get('id') : null))
|
||||
.filter(Boolean) as string[];
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
watchDocMeta(id: string) {
|
||||
let docMetaIndexCache = -1;
|
||||
return yjsObserveByPath(
|
||||
this.workspaceService.workspace.rootYDoc.getMap('meta'),
|
||||
'pages'
|
||||
).pipe(
|
||||
switchMap(yjsObserve),
|
||||
map(meta => {
|
||||
if (meta instanceof YArray) {
|
||||
if (docMetaIndexCache >= 0) {
|
||||
const doc = meta.get(docMetaIndexCache);
|
||||
if (doc && doc.get('id') === id) {
|
||||
return doc as YMap<any>;
|
||||
}
|
||||
}
|
||||
|
||||
// meta is YArray, `for-of` is faster then `for`
|
||||
let i = 0;
|
||||
for (const doc of meta) {
|
||||
if (doc && doc.get('id') === id) {
|
||||
docMetaIndexCache = i;
|
||||
return doc as YMap<any>;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
return null;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}),
|
||||
switchMap(yjsObserveDeep),
|
||||
map(meta => {
|
||||
if (meta instanceof YMap) {
|
||||
return meta.toJSON() as Partial<DocMeta>;
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
watchDocListReady() {
|
||||
return this.workspaceService.workspace.engine.rootDocState$
|
||||
.map(state => !state.syncing)
|
||||
.asObservable();
|
||||
}
|
||||
|
||||
setDocMeta(id: string, meta: Partial<DocMeta>) {
|
||||
this.workspaceService.workspace.docCollection.setDocMeta(id, meta);
|
||||
}
|
||||
|
||||
setDocPrimaryModeSetting(id: string, mode: DocMode) {
|
||||
return this.docPropertiesStore.updateDocProperties(id, {
|
||||
primaryMode: mode,
|
||||
});
|
||||
}
|
||||
|
||||
getDocPrimaryModeSetting(id: string) {
|
||||
return this.docPropertiesStore.getDocProperties(id)?.primaryMode;
|
||||
}
|
||||
|
||||
watchDocPrimaryModeSetting(id: string) {
|
||||
return this.docPropertiesStore.watchDocProperties(id).pipe(
|
||||
map(config => config?.primaryMode),
|
||||
distinctUntilChanged((p, c) => p === c)
|
||||
);
|
||||
}
|
||||
|
||||
waitForDocLoadReady(id: string) {
|
||||
return this.workspaceService.workspace.engine.doc.waitForReady(id);
|
||||
}
|
||||
|
||||
setPriorityLoad(id: string, priority: number) {
|
||||
return this.workspaceService.workspace.engine.doc.setPriority(id, priority);
|
||||
}
|
||||
|
||||
markDocSyncStateAsReady(id: string) {
|
||||
this.workspaceService.workspace.engine.doc.markAsReady(id);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user