mirror of
https://github.com/toeverything/AFFiNE.git
synced 2024-12-02 14:33:54 +03:00
feat(core): loading ui for favorite and organize (#7700)
This commit is contained in:
parent
94c5effdd5
commit
e54be7dc02
28
packages/common/infra/src/modules/db/entities/db.ts
Normal file
28
packages/common/infra/src/modules/db/entities/db.ts
Normal 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]>;
|
||||
};
|
33
packages/common/infra/src/modules/db/entities/table.ts
Normal file
33
packages/common/infra/src/modules/db/entities/table.ts
Normal 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);
|
||||
}
|
@ -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]);
|
||||
}
|
||||
|
@ -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<typeof WorkspaceUserdataDBClient>;
|
||||
|
||||
export class WorkspaceDBService extends Service {
|
||||
db: TableMap<AFFiNE_WORKSPACE_DB_SCHEMA>;
|
||||
userdataDBPool = new ObjectPool<string, WorkspaceUserdataDBClient>({
|
||||
db: DBWithTables<AFFiNE_WORKSPACE_DB_SCHEMA>;
|
||||
userdataDBPool = new ObjectPool<
|
||||
string,
|
||||
DB<AFFiNE_WORKSPACE_USERDATA_DB_SCHEMA>
|
||||
>({
|
||||
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<AFFiNE_WORKSPACE_DB_SCHEMA>, {
|
||||
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<AFFiNE_WORKSPACE_DB_SCHEMA>;
|
||||
}
|
||||
|
||||
// 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<AFFiNE_WORKSPACE_USERDATA_DB_SCHEMA>;
|
||||
}
|
||||
|
||||
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<AFFiNE_WORKSPACE_USERDATA_DB_SCHEMA>,
|
||||
{
|
||||
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<AFFiNE_WORKSPACE_USERDATA_DB_SCHEMA>;
|
||||
}
|
||||
|
||||
static isDBDocId(docId: string) {
|
||||
|
@ -5,7 +5,7 @@ import { validators } from './validators';
|
||||
|
||||
export class ORMClient {
|
||||
static hooksMap: Map<string, Hook<any>[]> = new Map();
|
||||
private readonly tables = new Map<string, Table<any>>();
|
||||
readonly tables = new Map<string, Table<any>>();
|
||||
constructor(
|
||||
protected readonly db: DBSchemaBuilder,
|
||||
protected readonly adapter: DBAdapter
|
||||
|
@ -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<DocEngineDocState>(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,
|
||||
|
@ -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 }
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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<AffineDNDData>) => void;
|
||||
canDrop?: DropTargetOptions<AffineDNDData>['canDrop'];
|
||||
dropEffect?: ExplorerTreeNodeDropEffect;
|
||||
isLoading?: boolean;
|
||||
}) => {
|
||||
const t = useI18n();
|
||||
|
||||
@ -33,6 +36,10 @@ export const RootEmpty = ({
|
||||
[onDrop, canDrop]
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return <Skeleton />;
|
||||
}
|
||||
|
||||
return (
|
||||
<ExplorerEmptySection
|
||||
ref={dropTargetRef}
|
||||
|
@ -44,6 +44,8 @@ export const ExplorerFavorites = () => {
|
||||
|
||||
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}
|
||||
/>
|
||||
}
|
||||
>
|
||||
|
@ -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 <Skeleton />;
|
||||
}
|
||||
|
||||
return (
|
||||
<ExplorerEmptySection
|
||||
icon={FolderIcon}
|
||||
|
@ -39,6 +39,7 @@ export const ExplorerOrganize = () => {
|
||||
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 = () => {
|
||||
}
|
||||
>
|
||||
<ExplorerTreeRoot
|
||||
placeholder={<RootEmpty onClickCreate={handleCreateFolder} />}
|
||||
placeholder={
|
||||
<RootEmpty onClickCreate={handleCreateFolder} isLoading={isLoading} />
|
||||
}
|
||||
>
|
||||
{folders.map(child => (
|
||||
<ExplorerFolderNode
|
||||
|
@ -9,6 +9,7 @@ export class FavoriteList extends Entity {
|
||||
sortedList$ = this.list$.map(v =>
|
||||
v.sort((a, b) => (a.index > b.index ? 1 : -1))
|
||||
);
|
||||
isLoading$ = this.store.watchIsLoading();
|
||||
|
||||
constructor(private readonly store: FavoriteStore) {
|
||||
super();
|
||||
|
@ -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$(), []))
|
||||
|
@ -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(
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user