feat(core): loading ui for favorite and organize (#7700)

This commit is contained in:
EYHN
2024-08-02 07:17:01 +00:00
parent 94c5effdd5
commit e54be7dc02
15 changed files with 179 additions and 36 deletions

View File

@@ -0,0 +1,28 @@
import { Entity } from '../../../framework';
import type { DBSchemaBuilder, TableMap } from '../../../orm';
import { Table } from './table';
export class DB<Schema extends DBSchemaBuilder> extends Entity<{
db: TableMap<Schema>;
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<Schema extends DBSchemaBuilder> = DB<Schema> & {
[K in keyof Schema]: Table<Schema[K]>;
};

View File

@@ -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<Schema extends TableSchemaBuilder> extends Entity<{
table: OrmTable<Schema>;
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);
}

View File

@@ -1,5 +1,7 @@
import type { Framework } from '../../framework'; import type { Framework } from '../../framework';
import { WorkspaceScope, WorkspaceService } from '../workspace'; import { WorkspaceScope, WorkspaceService } from '../workspace';
import { DB } from './entities/db';
import { Table } from './entities/table';
import { WorkspaceDBService } from './services/db'; import { WorkspaceDBService } from './services/db';
export { AFFiNE_WORKSPACE_DB_SCHEMA } from './schema'; export { AFFiNE_WORKSPACE_DB_SCHEMA } from './schema';
@@ -8,5 +10,7 @@ export { WorkspaceDBService } from './services/db';
export function configureWorkspaceDBModule(framework: Framework) { export function configureWorkspaceDBModule(framework: Framework) {
framework framework
.scope(WorkspaceScope) .scope(WorkspaceScope)
.service(WorkspaceDBService, [WorkspaceService]); .service(WorkspaceDBService, [WorkspaceService])
.entity(DB)
.entity(Table, [WorkspaceService]);
} }

View File

@@ -1,9 +1,10 @@
import { Doc as YDoc } from 'yjs'; import { Doc as YDoc } from 'yjs';
import { Service } from '../../../framework'; import { Service } from '../../../framework';
import { createORMClient, type TableMap, YjsDBAdapter } from '../../../orm'; import { createORMClient, YjsDBAdapter } from '../../../orm';
import { ObjectPool } from '../../../utils'; import { ObjectPool } from '../../../utils';
import type { WorkspaceService } from '../../workspace'; import type { WorkspaceService } from '../../workspace';
import { DB, type DBWithTables } from '../entities/db';
import { AFFiNE_WORKSPACE_DB_SCHEMA } from '../schema'; import { AFFiNE_WORKSPACE_DB_SCHEMA } from '../schema';
import { AFFiNE_WORKSPACE_USERDATA_DB_SCHEMA } from '../schema/schema'; import { AFFiNE_WORKSPACE_USERDATA_DB_SCHEMA } from '../schema/schema';
@@ -11,11 +12,13 @@ const WorkspaceDBClient = createORMClient(AFFiNE_WORKSPACE_DB_SCHEMA);
const WorkspaceUserdataDBClient = createORMClient( const WorkspaceUserdataDBClient = createORMClient(
AFFiNE_WORKSPACE_USERDATA_DB_SCHEMA AFFiNE_WORKSPACE_USERDATA_DB_SCHEMA
); );
type WorkspaceUserdataDBClient = InstanceType<typeof WorkspaceUserdataDBClient>;
export class WorkspaceDBService extends Service { export class WorkspaceDBService extends Service {
db: TableMap<AFFiNE_WORKSPACE_DB_SCHEMA>; db: DBWithTables<AFFiNE_WORKSPACE_DB_SCHEMA>;
userdataDBPool = new ObjectPool<string, WorkspaceUserdataDBClient>({ userdataDBPool = new ObjectPool<
string,
DB<AFFiNE_WORKSPACE_USERDATA_DB_SCHEMA>
>({
onDangling() { onDangling() {
return false; // never release return false; // never release
}, },
@@ -23,19 +26,27 @@ export class WorkspaceDBService extends Service {
constructor(private readonly workspaceService: WorkspaceService) { constructor(private readonly workspaceService: WorkspaceService) {
super(); super();
this.db = new WorkspaceDBClient( this.db = this.framework.createEntity(DB<AFFiNE_WORKSPACE_DB_SCHEMA>, {
new YjsDBAdapter(AFFiNE_WORKSPACE_DB_SCHEMA, { db: new WorkspaceDBClient(
getDoc: guid => { new YjsDBAdapter(AFFiNE_WORKSPACE_DB_SCHEMA, {
const ydoc = new YDoc({ getDoc: guid => {
// guid format: db${workspaceId}${guid} const ydoc = new YDoc({
guid: `db$${this.workspaceService.workspace.id}$${guid}`, // 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); this.workspaceService.workspace.engine.doc.addDoc(ydoc, false);
return ydoc; 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<AFFiNE_WORKSPACE_DB_SCHEMA>;
} }
// eslint-disable-next-line @typescript-eslint/ban-types // eslint-disable-next-line @typescript-eslint/ban-types
@@ -43,25 +54,36 @@ export class WorkspaceDBService extends Service {
// __local__ for local workspace // __local__ for local workspace
const userdataDb = this.userdataDBPool.get(userId); const userdataDb = this.userdataDBPool.get(userId);
if (userdataDb) { if (userdataDb) {
return userdataDb.obj; return userdataDb.obj as DBWithTables<AFFiNE_WORKSPACE_USERDATA_DB_SCHEMA>;
} }
const newDB = new WorkspaceUserdataDBClient( const newDB = this.framework.createEntity(
new YjsDBAdapter(AFFiNE_WORKSPACE_USERDATA_DB_SCHEMA, { DB<AFFiNE_WORKSPACE_USERDATA_DB_SCHEMA>,
getDoc: guid => { {
const ydoc = new YDoc({ db: new WorkspaceUserdataDBClient(
// guid format: userdata${userId}${workspaceId}${guid} new YjsDBAdapter(AFFiNE_WORKSPACE_USERDATA_DB_SCHEMA, {
guid: `userdata$${userId}$${this.workspaceService.workspace.id}$${guid}`, getDoc: guid => {
}); const ydoc = new YDoc({
this.workspaceService.workspace.engine.doc.addDoc(ydoc, false); // guid format: userdata${userId}${workspaceId}${guid}
this.workspaceService.workspace.engine.doc.setPriority(ydoc.guid, 50); guid: `userdata$${userId}$${this.workspaceService.workspace.id}$${guid}`,
return ydoc; });
}, 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); this.userdataDBPool.put(userId, newDB);
return newDB; return newDB as DBWithTables<AFFiNE_WORKSPACE_USERDATA_DB_SCHEMA>;
} }
static isDBDocId(docId: string) { static isDBDocId(docId: string) {

View File

@@ -5,7 +5,7 @@ import { validators } from './validators';
export class ORMClient { export class ORMClient {
static hooksMap: Map<string, Hook<any>[]> = new Map(); static hooksMap: Map<string, Hook<any>[]> = new Map();
private readonly tables = new Map<string, Table<any>>(); readonly tables = new Map<string, Table<any>>();
constructor( constructor(
protected readonly db: DBSchemaBuilder, protected readonly db: DBSchemaBuilder,
protected readonly adapter: DBAdapter protected readonly adapter: DBAdapter

View File

@@ -22,6 +22,25 @@ export {
ReadonlyStorage as ReadonlyDocStorage, ReadonlyStorage as ReadonlyDocStorage,
} from './storage'; } 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 { export class DocEngine {
readonly clientId: string; readonly clientId: string;
localPart: DocEngineLocalPart; localPart: DocEngineLocalPart;
@@ -53,13 +72,14 @@ export class DocEngine {
docState$(docId: string) { docState$(docId: string) {
const localState$ = this.localPart.docState$(docId); const localState$ = this.localPart.docState$(docId);
const remoteState$ = this.remotePart?.docState$(docId); const remoteState$ = this.remotePart?.docState$(docId);
return LiveData.computed(get => { return LiveData.computed<DocEngineDocState>(get => {
const localState = get(localState$); const localState = get(localState$);
const remoteState = remoteState$ ? get(remoteState$) : null; const remoteState = remoteState$ ? get(remoteState$) : null;
if (remoteState) { if (remoteState) {
return { return {
syncing: remoteState.syncing, syncing: remoteState.syncing,
saving: localState.syncing, saving: localState.syncing,
loading: localState.syncing,
retrying: remoteState.retrying, retrying: remoteState.retrying,
ready: localState.ready, ready: localState.ready,
errorMessage: remoteState.errorMessage, errorMessage: remoteState.errorMessage,
@@ -69,6 +89,7 @@ export class DocEngine {
return { return {
syncing: localState.syncing, syncing: localState.syncing,
saving: localState.syncing, saving: localState.syncing,
loading: localState.syncing,
ready: localState.ready, ready: localState.ready,
retrying: false, retrying: false,
errorMessage: null, errorMessage: null,

View File

@@ -40,6 +40,7 @@ export interface LocalEngineState {
export interface LocalDocState { export interface LocalDocState {
ready: boolean; ready: boolean;
loading: boolean;
syncing: boolean; syncing: boolean;
} }
@@ -81,6 +82,7 @@ export class DocEngineLocalPart {
const next = () => { const next = () => {
subscribe.next({ subscribe.next({
ready: this.status.readyDocs.has(docId) ?? false, ready: this.status.readyDocs.has(docId) ?? false,
loading: this.status.connectedDocs.has(docId),
syncing: syncing:
(this.status.jobMap.get(docId)?.length ?? 0) > 0 || (this.status.jobMap.get(docId)?.length ?? 0) > 0 ||
this.status.currentJob?.docId === docId, this.status.currentJob?.docId === docId,
@@ -91,7 +93,7 @@ export class DocEngineLocalPart {
if (updatedId === docId) next(); if (updatedId === docId) next();
}); });
}), }),
{ ready: false, syncing: false } { ready: false, loading: false, syncing: false }
); );
} }

View File

@@ -1,6 +1,7 @@
import { import {
type DropTargetDropEvent, type DropTargetDropEvent,
type DropTargetOptions, type DropTargetOptions,
Skeleton,
useDropTarget, useDropTarget,
} from '@affine/component'; } from '@affine/component';
import type { AffineDNDData } from '@affine/core/types/dnd'; import type { AffineDNDData } from '@affine/core/types/dnd';
@@ -13,11 +14,13 @@ import { DropEffect, type ExplorerTreeNodeDropEffect } from '../../tree';
export const RootEmpty = ({ export const RootEmpty = ({
onDrop, onDrop,
canDrop, canDrop,
isLoading,
dropEffect, dropEffect,
}: { }: {
onDrop?: (data: DropTargetDropEvent<AffineDNDData>) => void; onDrop?: (data: DropTargetDropEvent<AffineDNDData>) => void;
canDrop?: DropTargetOptions<AffineDNDData>['canDrop']; canDrop?: DropTargetOptions<AffineDNDData>['canDrop'];
dropEffect?: ExplorerTreeNodeDropEffect; dropEffect?: ExplorerTreeNodeDropEffect;
isLoading?: boolean;
}) => { }) => {
const t = useI18n(); const t = useI18n();
@@ -33,6 +36,10 @@ export const RootEmpty = ({
[onDrop, canDrop] [onDrop, canDrop]
); );
if (isLoading) {
return <Skeleton />;
}
return ( return (
<ExplorerEmptySection <ExplorerEmptySection
ref={dropTargetRef} ref={dropTargetRef}

View File

@@ -44,6 +44,8 @@ export const ExplorerFavorites = () => {
const favorites = useLiveData(favoriteService.favoriteList.sortedList$); const favorites = useLiveData(favoriteService.favoriteList.sortedList$);
const isLoading = useLiveData(favoriteService.favoriteList.isLoading$);
const t = useI18n(); const t = useI18n();
const handleDrop = useCallback( const handleDrop = useCallback(
@@ -262,6 +264,7 @@ export const ExplorerFavorites = () => {
onDrop={handleDrop} onDrop={handleDrop}
canDrop={handleCanDrop} canDrop={handleCanDrop}
dropEffect={handleDropEffect} dropEffect={handleDropEffect}
isLoading={isLoading}
/> />
} }
> >

View File

@@ -1,3 +1,4 @@
import { Skeleton } from '@affine/component';
import { useI18n } from '@affine/i18n'; import { useI18n } from '@affine/i18n';
import { FolderIcon } from '@blocksuite/icons/rc'; import { FolderIcon } from '@blocksuite/icons/rc';
@@ -5,11 +6,17 @@ import { ExplorerEmptySection } from '../../layouts/empty-section';
export const RootEmpty = ({ export const RootEmpty = ({
onClickCreate, onClickCreate,
isLoading,
}: { }: {
onClickCreate?: () => void; onClickCreate?: () => void;
isLoading?: boolean;
}) => { }) => {
const t = useI18n(); const t = useI18n();
if (isLoading) {
return <Skeleton />;
}
return ( return (
<ExplorerEmptySection <ExplorerEmptySection
icon={FolderIcon} icon={FolderIcon}

View File

@@ -39,6 +39,7 @@ export const ExplorerOrganize = () => {
const rootFolder = organizeService.folderTree.rootFolder; const rootFolder = organizeService.folderTree.rootFolder;
const folders = useLiveData(rootFolder.sortedChildren$); const folders = useLiveData(rootFolder.sortedChildren$);
const isLoading = useLiveData(organizeService.folderTree.isLoading$);
const handleCreateFolder = useCallback(() => { const handleCreateFolder = useCallback(() => {
const newFolderId = rootFolder.createFolder( const newFolderId = rootFolder.createFolder(
@@ -128,7 +129,9 @@ export const ExplorerOrganize = () => {
} }
> >
<ExplorerTreeRoot <ExplorerTreeRoot
placeholder={<RootEmpty onClickCreate={handleCreateFolder} />} placeholder={
<RootEmpty onClickCreate={handleCreateFolder} isLoading={isLoading} />
}
> >
{folders.map(child => ( {folders.map(child => (
<ExplorerFolderNode <ExplorerFolderNode

View File

@@ -9,6 +9,7 @@ export class FavoriteList extends Entity {
sortedList$ = this.list$.map(v => sortedList$ = this.list$.map(v =>
v.sort((a, b) => (a.index > b.index ? 1 : -1)) v.sort((a, b) => (a.index > b.index ? 1 : -1))
); );
isLoading$ = this.store.watchIsLoading();
constructor(private readonly store: FavoriteStore) { constructor(private readonly store: FavoriteStore) {
super(); super();

View File

@@ -37,6 +37,12 @@ export class FavoriteStore extends Store {
}); });
} }
watchIsLoading() {
return this.userdataDB$
.map(db => LiveData.from(db.favorite.isLoading$, false))
.flat();
}
watchFavorites() { watchFavorites() {
return this.userdataDB$ return this.userdataDB$
.map(db => LiveData.from(db.favorite.find$(), [])) .map(db => LiveData.from(db.favorite.find$(), []))

View File

@@ -13,6 +13,8 @@ export class FolderTree extends Entity {
id: null, id: null,
}); });
isLoading$ = this.folderStore.watchIsLoading();
// get folder by id // get folder by id
folderNode$(id: string) { folderNode$(id: string) {
return LiveData.from( return LiveData.from(

View File

@@ -16,6 +16,10 @@ export class FolderStore extends Store {
}); });
} }
watchIsLoading() {
return this.dbService.db.folders.isLoading$;
}
isAncestor(childId: string, ancestorId: string): boolean { isAncestor(childId: string, ancestorId: string): boolean {
if (childId === ancestorId) { if (childId === ancestorId) {
return false; return false;