From e54be7dc023b173d54dc855f0a523ca8a8e9083d Mon Sep 17 00:00:00 2001 From: EYHN Date: Fri, 2 Aug 2024 07:17:01 +0000 Subject: [PATCH] feat(core): loading ui for favorite and organize (#7700) --- .../infra/src/modules/db/entities/db.ts | 28 +++++++ .../infra/src/modules/db/entities/table.ts | 33 ++++++++ packages/common/infra/src/modules/db/index.ts | 6 +- .../infra/src/modules/db/services/db.ts | 84 ++++++++++++------- packages/common/infra/src/orm/core/client.ts | 2 +- packages/common/infra/src/sync/doc/index.ts | 23 ++++- packages/common/infra/src/sync/doc/local.ts | 4 +- .../views/sections/favorites/empty.tsx | 7 ++ .../views/sections/favorites/index.tsx | 3 + .../views/sections/organize/empty.tsx | 7 ++ .../views/sections/organize/index.tsx | 5 +- .../favorite/entities/favorite-list.ts | 1 + .../src/modules/favorite/stores/favorite.ts | 6 ++ .../modules/organize/entities/folder-tree.ts | 2 + .../src/modules/organize/stores/folder.ts | 4 + 15 files changed, 179 insertions(+), 36 deletions(-) create mode 100644 packages/common/infra/src/modules/db/entities/db.ts create mode 100644 packages/common/infra/src/modules/db/entities/table.ts diff --git a/packages/common/infra/src/modules/db/entities/db.ts b/packages/common/infra/src/modules/db/entities/db.ts new file mode 100644 index 0000000000..0ea850b220 --- /dev/null +++ b/packages/common/infra/src/modules/db/entities/db.ts @@ -0,0 +1,28 @@ +import { Entity } from '../../../framework'; +import type { DBSchemaBuilder, TableMap } from '../../../orm'; +import { Table } from './table'; + +export class DB extends Entity<{ + db: TableMap; + schema: Schema; + storageDocId: (tableName: string) => string; +}> { + readonly db = this.props.db; + + constructor() { + super(); + Object.entries(this.props.schema).forEach(([tableName]) => { + const table = this.framework.createEntity(Table, { + table: this.db[tableName], + storageDocId: this.props.storageDocId(tableName), + }); + Object.defineProperty(this, tableName, { + get: () => table, + }); + }); + } +} + +export type DBWithTables = DB & { + [K in keyof Schema]: Table; +}; diff --git a/packages/common/infra/src/modules/db/entities/table.ts b/packages/common/infra/src/modules/db/entities/table.ts new file mode 100644 index 0000000000..c300ce9968 --- /dev/null +++ b/packages/common/infra/src/modules/db/entities/table.ts @@ -0,0 +1,33 @@ +import { Entity } from '../../../framework'; +import type { Table as OrmTable, TableSchemaBuilder } from '../../../orm/core'; +import type { WorkspaceService } from '../../workspace'; + +export class Table extends Entity<{ + table: OrmTable; + storageDocId: string; +}> { + readonly table = this.props.table; + + constructor(private readonly workspaceService: WorkspaceService) { + super(); + } + + isSyncing$ = this.workspaceService.workspace.engine.doc + .docState$(this.props.storageDocId) + .map(docState => docState.syncing); + + isLoading$ = this.workspaceService.workspace.engine.doc + .docState$(this.props.storageDocId) + .map(docState => docState.loading); + + create = this.table.create.bind(this.table); + update = this.table.update.bind(this.table); + get = this.table.get.bind(this.table); + // eslint-disable-next-line rxjs/finnish + get$ = this.table.get$.bind(this.table); + find = this.table.find.bind(this.table); + // eslint-disable-next-line rxjs/finnish + find$ = this.table.find$.bind(this.table); + keys = this.table.keys.bind(this.table); + delete = this.table.delete.bind(this.table); +} diff --git a/packages/common/infra/src/modules/db/index.ts b/packages/common/infra/src/modules/db/index.ts index 7d9399a671..9df0747f50 100644 --- a/packages/common/infra/src/modules/db/index.ts +++ b/packages/common/infra/src/modules/db/index.ts @@ -1,5 +1,7 @@ import type { Framework } from '../../framework'; import { WorkspaceScope, WorkspaceService } from '../workspace'; +import { DB } from './entities/db'; +import { Table } from './entities/table'; import { WorkspaceDBService } from './services/db'; export { AFFiNE_WORKSPACE_DB_SCHEMA } from './schema'; @@ -8,5 +10,7 @@ export { WorkspaceDBService } from './services/db'; export function configureWorkspaceDBModule(framework: Framework) { framework .scope(WorkspaceScope) - .service(WorkspaceDBService, [WorkspaceService]); + .service(WorkspaceDBService, [WorkspaceService]) + .entity(DB) + .entity(Table, [WorkspaceService]); } diff --git a/packages/common/infra/src/modules/db/services/db.ts b/packages/common/infra/src/modules/db/services/db.ts index eecdfe336f..eaffd2050a 100644 --- a/packages/common/infra/src/modules/db/services/db.ts +++ b/packages/common/infra/src/modules/db/services/db.ts @@ -1,9 +1,10 @@ import { Doc as YDoc } from 'yjs'; import { Service } from '../../../framework'; -import { createORMClient, type TableMap, YjsDBAdapter } from '../../../orm'; +import { createORMClient, YjsDBAdapter } from '../../../orm'; import { ObjectPool } from '../../../utils'; import type { WorkspaceService } from '../../workspace'; +import { DB, type DBWithTables } from '../entities/db'; import { AFFiNE_WORKSPACE_DB_SCHEMA } from '../schema'; import { AFFiNE_WORKSPACE_USERDATA_DB_SCHEMA } from '../schema/schema'; @@ -11,11 +12,13 @@ const WorkspaceDBClient = createORMClient(AFFiNE_WORKSPACE_DB_SCHEMA); const WorkspaceUserdataDBClient = createORMClient( AFFiNE_WORKSPACE_USERDATA_DB_SCHEMA ); -type WorkspaceUserdataDBClient = InstanceType; export class WorkspaceDBService extends Service { - db: TableMap; - userdataDBPool = new ObjectPool({ + db: DBWithTables; + userdataDBPool = new ObjectPool< + string, + DB + >({ onDangling() { return false; // never release }, @@ -23,19 +26,27 @@ export class WorkspaceDBService extends Service { constructor(private readonly workspaceService: WorkspaceService) { super(); - this.db = new WorkspaceDBClient( - new YjsDBAdapter(AFFiNE_WORKSPACE_DB_SCHEMA, { - getDoc: guid => { - const ydoc = new YDoc({ - // guid format: db${workspaceId}${guid} - guid: `db$${this.workspaceService.workspace.id}$${guid}`, - }); - this.workspaceService.workspace.engine.doc.addDoc(ydoc, false); - this.workspaceService.workspace.engine.doc.setPriority(ydoc.guid, 50); - return ydoc; - }, - }) - ); + this.db = this.framework.createEntity(DB, { + db: new WorkspaceDBClient( + new YjsDBAdapter(AFFiNE_WORKSPACE_DB_SCHEMA, { + getDoc: guid => { + const ydoc = new YDoc({ + // guid format: db${workspaceId}${guid} + guid: `db$${this.workspaceService.workspace.id}$${guid}`, + }); + this.workspaceService.workspace.engine.doc.addDoc(ydoc, false); + this.workspaceService.workspace.engine.doc.setPriority( + ydoc.guid, + 50 + ); + return ydoc; + }, + }) + ), + schema: AFFiNE_WORKSPACE_DB_SCHEMA, + storageDocId: tableName => + `db$${this.workspaceService.workspace.id}$${tableName}`, + }) as DBWithTables; } // eslint-disable-next-line @typescript-eslint/ban-types @@ -43,25 +54,36 @@ export class WorkspaceDBService extends Service { // __local__ for local workspace const userdataDb = this.userdataDBPool.get(userId); if (userdataDb) { - return userdataDb.obj; + return userdataDb.obj as DBWithTables; } - const newDB = new WorkspaceUserdataDBClient( - new YjsDBAdapter(AFFiNE_WORKSPACE_USERDATA_DB_SCHEMA, { - getDoc: guid => { - const ydoc = new YDoc({ - // guid format: userdata${userId}${workspaceId}${guid} - guid: `userdata$${userId}$${this.workspaceService.workspace.id}$${guid}`, - }); - this.workspaceService.workspace.engine.doc.addDoc(ydoc, false); - this.workspaceService.workspace.engine.doc.setPriority(ydoc.guid, 50); - return ydoc; - }, - }) + const newDB = this.framework.createEntity( + DB, + { + db: new WorkspaceUserdataDBClient( + new YjsDBAdapter(AFFiNE_WORKSPACE_USERDATA_DB_SCHEMA, { + getDoc: guid => { + const ydoc = new YDoc({ + // guid format: userdata${userId}${workspaceId}${guid} + guid: `userdata$${userId}$${this.workspaceService.workspace.id}$${guid}`, + }); + this.workspaceService.workspace.engine.doc.addDoc(ydoc, false); + this.workspaceService.workspace.engine.doc.setPriority( + ydoc.guid, + 50 + ); + return ydoc; + }, + }) + ), + schema: AFFiNE_WORKSPACE_USERDATA_DB_SCHEMA, + storageDocId: tableName => + `userdata$${userId}$${this.workspaceService.workspace.id}$${tableName}`, + } ); this.userdataDBPool.put(userId, newDB); - return newDB; + return newDB as DBWithTables; } static isDBDocId(docId: string) { diff --git a/packages/common/infra/src/orm/core/client.ts b/packages/common/infra/src/orm/core/client.ts index 755d9bb836..335e047888 100644 --- a/packages/common/infra/src/orm/core/client.ts +++ b/packages/common/infra/src/orm/core/client.ts @@ -5,7 +5,7 @@ import { validators } from './validators'; export class ORMClient { static hooksMap: Map[]> = new Map(); - private readonly tables = new Map>(); + readonly tables = new Map>(); constructor( protected readonly db: DBSchemaBuilder, protected readonly adapter: DBAdapter diff --git a/packages/common/infra/src/sync/doc/index.ts b/packages/common/infra/src/sync/doc/index.ts index d15464071d..3737d2e7a1 100644 --- a/packages/common/infra/src/sync/doc/index.ts +++ b/packages/common/infra/src/sync/doc/index.ts @@ -22,6 +22,25 @@ export { ReadonlyStorage as ReadonlyDocStorage, } from './storage'; +export interface DocEngineDocState { + /** + * is syncing with the server + */ + syncing: boolean; + /** + * is saving to local storage + */ + saving: boolean; + /** + * is loading from local storage + */ + loading: boolean; + retrying: boolean; + ready: boolean; + errorMessage: string | null; + serverClock: number | null; +} + export class DocEngine { readonly clientId: string; localPart: DocEngineLocalPart; @@ -53,13 +72,14 @@ export class DocEngine { docState$(docId: string) { const localState$ = this.localPart.docState$(docId); const remoteState$ = this.remotePart?.docState$(docId); - return LiveData.computed(get => { + return LiveData.computed(get => { const localState = get(localState$); const remoteState = remoteState$ ? get(remoteState$) : null; if (remoteState) { return { syncing: remoteState.syncing, saving: localState.syncing, + loading: localState.syncing, retrying: remoteState.retrying, ready: localState.ready, errorMessage: remoteState.errorMessage, @@ -69,6 +89,7 @@ export class DocEngine { return { syncing: localState.syncing, saving: localState.syncing, + loading: localState.syncing, ready: localState.ready, retrying: false, errorMessage: null, diff --git a/packages/common/infra/src/sync/doc/local.ts b/packages/common/infra/src/sync/doc/local.ts index 2bf6ad3f34..0ec180b839 100644 --- a/packages/common/infra/src/sync/doc/local.ts +++ b/packages/common/infra/src/sync/doc/local.ts @@ -40,6 +40,7 @@ export interface LocalEngineState { export interface LocalDocState { ready: boolean; + loading: boolean; syncing: boolean; } @@ -81,6 +82,7 @@ export class DocEngineLocalPart { const next = () => { subscribe.next({ ready: this.status.readyDocs.has(docId) ?? false, + loading: this.status.connectedDocs.has(docId), syncing: (this.status.jobMap.get(docId)?.length ?? 0) > 0 || this.status.currentJob?.docId === docId, @@ -91,7 +93,7 @@ export class DocEngineLocalPart { if (updatedId === docId) next(); }); }), - { ready: false, syncing: false } + { ready: false, loading: false, syncing: false } ); } diff --git a/packages/frontend/core/src/modules/explorer/views/sections/favorites/empty.tsx b/packages/frontend/core/src/modules/explorer/views/sections/favorites/empty.tsx index 8d1f339d4f..95ed4f631c 100644 --- a/packages/frontend/core/src/modules/explorer/views/sections/favorites/empty.tsx +++ b/packages/frontend/core/src/modules/explorer/views/sections/favorites/empty.tsx @@ -1,6 +1,7 @@ import { type DropTargetDropEvent, type DropTargetOptions, + Skeleton, useDropTarget, } from '@affine/component'; import type { AffineDNDData } from '@affine/core/types/dnd'; @@ -13,11 +14,13 @@ import { DropEffect, type ExplorerTreeNodeDropEffect } from '../../tree'; export const RootEmpty = ({ onDrop, canDrop, + isLoading, dropEffect, }: { onDrop?: (data: DropTargetDropEvent) => void; canDrop?: DropTargetOptions['canDrop']; dropEffect?: ExplorerTreeNodeDropEffect; + isLoading?: boolean; }) => { const t = useI18n(); @@ -33,6 +36,10 @@ export const RootEmpty = ({ [onDrop, canDrop] ); + if (isLoading) { + return ; + } + return ( { const favorites = useLiveData(favoriteService.favoriteList.sortedList$); + const isLoading = useLiveData(favoriteService.favoriteList.isLoading$); + const t = useI18n(); const handleDrop = useCallback( @@ -262,6 +264,7 @@ export const ExplorerFavorites = () => { onDrop={handleDrop} canDrop={handleCanDrop} dropEffect={handleDropEffect} + isLoading={isLoading} /> } > diff --git a/packages/frontend/core/src/modules/explorer/views/sections/organize/empty.tsx b/packages/frontend/core/src/modules/explorer/views/sections/organize/empty.tsx index 0081b6d7de..cc877eff7b 100644 --- a/packages/frontend/core/src/modules/explorer/views/sections/organize/empty.tsx +++ b/packages/frontend/core/src/modules/explorer/views/sections/organize/empty.tsx @@ -1,3 +1,4 @@ +import { Skeleton } from '@affine/component'; import { useI18n } from '@affine/i18n'; import { FolderIcon } from '@blocksuite/icons/rc'; @@ -5,11 +6,17 @@ import { ExplorerEmptySection } from '../../layouts/empty-section'; export const RootEmpty = ({ onClickCreate, + isLoading, }: { onClickCreate?: () => void; + isLoading?: boolean; }) => { const t = useI18n(); + if (isLoading) { + return ; + } + return ( { const rootFolder = organizeService.folderTree.rootFolder; const folders = useLiveData(rootFolder.sortedChildren$); + const isLoading = useLiveData(organizeService.folderTree.isLoading$); const handleCreateFolder = useCallback(() => { const newFolderId = rootFolder.createFolder( @@ -128,7 +129,9 @@ export const ExplorerOrganize = () => { } > } + placeholder={ + + } > {folders.map(child => ( v.sort((a, b) => (a.index > b.index ? 1 : -1)) ); + isLoading$ = this.store.watchIsLoading(); constructor(private readonly store: FavoriteStore) { super(); diff --git a/packages/frontend/core/src/modules/favorite/stores/favorite.ts b/packages/frontend/core/src/modules/favorite/stores/favorite.ts index 2177589db8..71ca84ad43 100644 --- a/packages/frontend/core/src/modules/favorite/stores/favorite.ts +++ b/packages/frontend/core/src/modules/favorite/stores/favorite.ts @@ -37,6 +37,12 @@ export class FavoriteStore extends Store { }); } + watchIsLoading() { + return this.userdataDB$ + .map(db => LiveData.from(db.favorite.isLoading$, false)) + .flat(); + } + watchFavorites() { return this.userdataDB$ .map(db => LiveData.from(db.favorite.find$(), [])) diff --git a/packages/frontend/core/src/modules/organize/entities/folder-tree.ts b/packages/frontend/core/src/modules/organize/entities/folder-tree.ts index aeffc737ed..9cd8cf4ae7 100644 --- a/packages/frontend/core/src/modules/organize/entities/folder-tree.ts +++ b/packages/frontend/core/src/modules/organize/entities/folder-tree.ts @@ -13,6 +13,8 @@ export class FolderTree extends Entity { id: null, }); + isLoading$ = this.folderStore.watchIsLoading(); + // get folder by id folderNode$(id: string) { return LiveData.from( diff --git a/packages/frontend/core/src/modules/organize/stores/folder.ts b/packages/frontend/core/src/modules/organize/stores/folder.ts index 6ff08fae93..fd107324b4 100644 --- a/packages/frontend/core/src/modules/organize/stores/folder.ts +++ b/packages/frontend/core/src/modules/organize/stores/folder.ts @@ -16,6 +16,10 @@ export class FolderStore extends Store { }); } + watchIsLoading() { + return this.dbService.db.folders.isLoading$; + } + isAncestor(childId: string, ancestorId: string): boolean { if (childId === ancestorId) { return false;