mirror of
https://github.com/toeverything/AFFiNE.git
synced 2024-11-21 20:44:40 +03:00
refactor(infra): migrate to new infra (#5565)
This commit is contained in:
parent
1e3499c323
commit
329fc19852
@ -64,7 +64,7 @@ const allPackages = [
|
|||||||
'packages/frontend/i18n',
|
'packages/frontend/i18n',
|
||||||
'packages/frontend/native',
|
'packages/frontend/native',
|
||||||
'packages/frontend/templates',
|
'packages/frontend/templates',
|
||||||
'packages/frontend/workspace',
|
'packages/frontend/workspace-impl',
|
||||||
'packages/common/debug',
|
'packages/common/debug',
|
||||||
'packages/common/env',
|
'packages/common/env',
|
||||||
'packages/common/infra',
|
'packages/common/infra',
|
||||||
|
5
.github/labeler.yml
vendored
5
.github/labeler.yml
vendored
@ -29,11 +29,6 @@ mod:plugin-cli:
|
|||||||
- any-glob-to-any-file:
|
- any-glob-to-any-file:
|
||||||
- 'tools/plugin-cli/**/*'
|
- 'tools/plugin-cli/**/*'
|
||||||
|
|
||||||
mod:workspace:
|
|
||||||
- changed-files:
|
|
||||||
- any-glob-to-any-file:
|
|
||||||
- 'packages/common/workspace/**/*'
|
|
||||||
|
|
||||||
mod:workspace-impl:
|
mod:workspace-impl:
|
||||||
- changed-files:
|
- changed-files:
|
||||||
- any-glob-to-any-file:
|
- any-glob-to-any-file:
|
||||||
|
@ -203,6 +203,25 @@ export class ServiceCollection {
|
|||||||
this.services.set(normalizedScope, services);
|
this.services.set(normalizedScope, services);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
remove(identifier: ServiceIdentifierValue, scope: ServiceScope = ROOT_SCOPE) {
|
||||||
|
const normalizedScope = stringifyScope(scope);
|
||||||
|
const normalizedIdentifier = parseIdentifier(identifier);
|
||||||
|
const normalizedVariant =
|
||||||
|
normalizedIdentifier.variant ?? DEFAULT_SERVICE_VARIANT;
|
||||||
|
|
||||||
|
const services = this.services.get(normalizedScope);
|
||||||
|
if (!services) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const variants = services.get(normalizedIdentifier.identifierName);
|
||||||
|
if (!variants) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
variants.delete(normalizedVariant);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a service provider from the collection.
|
* Create a service provider from the collection.
|
||||||
*
|
*
|
||||||
@ -365,7 +384,7 @@ class ServiceCollectionEditor {
|
|||||||
*/
|
*/
|
||||||
override = <
|
override = <
|
||||||
Arg1 extends ServiceIdentifier<any>,
|
Arg1 extends ServiceIdentifier<any>,
|
||||||
Arg2 extends Type<Trait> | ServiceFactory<Trait> | Trait,
|
Arg2 extends Type<Trait> | ServiceFactory<Trait> | Trait | null,
|
||||||
Trait = ServiceIdentifierType<Arg1>,
|
Trait = ServiceIdentifierType<Arg1>,
|
||||||
Deps extends Arg2 extends Type<Trait>
|
Deps extends Arg2 extends Type<Trait>
|
||||||
? TypesToDeps<ConstructorParameters<Arg2>>
|
? TypesToDeps<ConstructorParameters<Arg2>>
|
||||||
@ -378,7 +397,10 @@ class ServiceCollectionEditor {
|
|||||||
arg2: Arg2,
|
arg2: Arg2,
|
||||||
...[arg3]: Arg3 extends [] ? [] : [Arg3]
|
...[arg3]: Arg3 extends [] ? [] : [Arg3]
|
||||||
): this => {
|
): this => {
|
||||||
if (arg2 instanceof Function) {
|
if (arg2 === null) {
|
||||||
|
this.collection.remove(identifier, this.currentScope);
|
||||||
|
return this;
|
||||||
|
} else if (arg2 instanceof Function) {
|
||||||
this.collection.addFactory<any>(
|
this.collection.addFactory<any>(
|
||||||
identifier,
|
identifier,
|
||||||
dependenciesToFactory(arg2, arg3 as any[]),
|
dependenciesToFactory(arg2, arg3 as any[]),
|
||||||
|
@ -26,6 +26,6 @@ export function configureInfraServices(services: ServiceCollection) {
|
|||||||
|
|
||||||
export function configureTestingInfraServices(services: ServiceCollection) {
|
export function configureTestingInfraServices(services: ServiceCollection) {
|
||||||
configureTestingWorkspaceServices(services);
|
configureTestingWorkspaceServices(services);
|
||||||
services.addImpl(GlobalCache, MemoryMemento);
|
services.override(GlobalCache, MemoryMemento);
|
||||||
services.addImpl(GlobalState, MemoryMemento);
|
services.override(GlobalState, MemoryMemento);
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,7 @@ export function configurePageServices(services: ServiceCollection) {
|
|||||||
services
|
services
|
||||||
.scope(WorkspaceScope)
|
.scope(WorkspaceScope)
|
||||||
.add(PageListService, [Workspace])
|
.add(PageListService, [Workspace])
|
||||||
.add(PageManager, [Workspace, ServiceProvider]);
|
.add(PageManager, [Workspace, PageListService, ServiceProvider]);
|
||||||
services
|
services
|
||||||
.scope(PageScope)
|
.scope(PageScope)
|
||||||
.add(CleanupService)
|
.add(CleanupService)
|
||||||
|
@ -2,7 +2,7 @@ import type { PageMeta } from '@blocksuite/store';
|
|||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
import { LiveData } from '../livedata';
|
import { LiveData } from '../livedata';
|
||||||
import type { Workspace } from '../workspace';
|
import { SyncEngineStep, type Workspace } from '../workspace';
|
||||||
|
|
||||||
export class PageListService {
|
export class PageListService {
|
||||||
constructor(private readonly workspace: Workspace) {}
|
constructor(private readonly workspace: Workspace) {}
|
||||||
@ -25,4 +25,26 @@ export class PageListService {
|
|||||||
}),
|
}),
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
public readonly isReady = LiveData.from<boolean>(
|
||||||
|
new Observable(subscriber => {
|
||||||
|
subscriber.next(
|
||||||
|
this.workspace.engine.status.sync.step === SyncEngineStep.Synced
|
||||||
|
);
|
||||||
|
|
||||||
|
const dispose = this.workspace.engine.onStatusChange.on(() => {
|
||||||
|
subscriber.next(
|
||||||
|
this.workspace.engine.status.sync.step === SyncEngineStep.Synced
|
||||||
|
);
|
||||||
|
}).dispose;
|
||||||
|
return () => {
|
||||||
|
dispose();
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
public getPageMetaById(id: string) {
|
||||||
|
return this.pages.value.find(page => page.id === id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import type { PageMeta } from '@blocksuite/store';
|
import type { PageMeta } from '@blocksuite/store';
|
||||||
|
|
||||||
import type { ServiceProvider } from '../di';
|
import type { ServiceProvider } from '../di';
|
||||||
import { ObjectPool, type RcRef } from '../utils/object-pool';
|
import { ObjectPool } from '../utils/object-pool';
|
||||||
import type { Workspace } from '../workspace';
|
import type { Workspace } from '../workspace';
|
||||||
import { configurePageContext } from './context';
|
import { configurePageContext } from './context';
|
||||||
|
import type { PageListService } from './list';
|
||||||
import { Page } from './page';
|
import { Page } from './page';
|
||||||
import { PageScope } from './service-scope';
|
import { PageScope } from './service-scope';
|
||||||
|
|
||||||
@ -12,10 +13,20 @@ export class PageManager {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly workspace: Workspace,
|
private readonly workspace: Workspace,
|
||||||
|
private readonly pageList: PageListService,
|
||||||
private readonly serviceProvider: ServiceProvider
|
private readonly serviceProvider: ServiceProvider
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
open(pageMeta: PageMeta): RcRef<Page> {
|
openByPageId(pageId: string) {
|
||||||
|
const pageMeta = this.pageList.getPageMetaById(pageId);
|
||||||
|
if (!pageMeta) {
|
||||||
|
throw new Error('Page not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.open(pageMeta);
|
||||||
|
}
|
||||||
|
|
||||||
|
open(pageMeta: PageMeta) {
|
||||||
const blockSuitePage = this.workspace.blockSuiteWorkspace.getPage(
|
const blockSuitePage = this.workspace.blockSuiteWorkspace.getPage(
|
||||||
pageMeta.id
|
pageMeta.id
|
||||||
);
|
);
|
||||||
@ -25,7 +36,7 @@ export class PageManager {
|
|||||||
|
|
||||||
const exists = this.pool.get(pageMeta.id);
|
const exists = this.pool.get(pageMeta.id);
|
||||||
if (exists) {
|
if (exists) {
|
||||||
return exists;
|
return { page: exists.obj, release: exists.release };
|
||||||
}
|
}
|
||||||
|
|
||||||
const serviceCollection = this.serviceProvider.collection
|
const serviceCollection = this.serviceProvider.collection
|
||||||
@ -41,6 +52,8 @@ export class PageManager {
|
|||||||
|
|
||||||
const page = provider.get(Page);
|
const page = provider.get(Page);
|
||||||
|
|
||||||
return this.pool.put(pageMeta.id, page);
|
const { obj, release } = this.pool.put(pageMeta.id, page);
|
||||||
|
|
||||||
|
return { page: obj, release };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,10 +11,6 @@ export const AwarenessProvider =
|
|||||||
export class AwarenessEngine {
|
export class AwarenessEngine {
|
||||||
constructor(public readonly providers: AwarenessProvider[]) {}
|
constructor(public readonly providers: AwarenessProvider[]) {}
|
||||||
|
|
||||||
static get EMPTY() {
|
|
||||||
return new AwarenessEngine([]);
|
|
||||||
}
|
|
||||||
|
|
||||||
connect() {
|
connect() {
|
||||||
this.providers.forEach(provider => provider.connect());
|
this.providers.forEach(provider => provider.connect());
|
||||||
}
|
}
|
||||||
|
@ -54,10 +54,6 @@ export class BlobEngine {
|
|||||||
private readonly remotes: BlobStorage[]
|
private readonly remotes: BlobStorage[]
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
static get EMPTY() {
|
|
||||||
return new BlobEngine(createEmptyBlobStorage(), []);
|
|
||||||
}
|
|
||||||
|
|
||||||
start() {
|
start() {
|
||||||
if (this.abort || this._status.isStorageOverCapacity) {
|
if (this.abort || this._status.isStorageOverCapacity) {
|
||||||
return;
|
return;
|
||||||
@ -222,21 +218,19 @@ export class BlobEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createEmptyBlobStorage() {
|
export const EmptyBlobStorage: BlobStorage = {
|
||||||
return {
|
name: 'empty',
|
||||||
name: 'empty',
|
readonly: true,
|
||||||
readonly: true,
|
async get(_key: string) {
|
||||||
async get(_key: string) {
|
return null;
|
||||||
return null;
|
},
|
||||||
},
|
async set(_key: string, _value: Blob) {
|
||||||
async set(_key: string, _value: Blob) {
|
throw new Error('not supported');
|
||||||
throw new Error('not supported');
|
},
|
||||||
},
|
async delete(_key: string) {
|
||||||
async delete(_key: string) {
|
throw new Error('not supported');
|
||||||
throw new Error('not supported');
|
},
|
||||||
},
|
async list() {
|
||||||
async list() {
|
return [];
|
||||||
return [];
|
},
|
||||||
},
|
};
|
||||||
} satisfies BlobStorage;
|
|
||||||
}
|
|
||||||
|
@ -23,3 +23,22 @@ export interface SyncStorage {
|
|||||||
disconnect: (reason: string) => void
|
disconnect: (reason: string) => void
|
||||||
): Promise<() => void>;
|
): Promise<() => void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const EmptySyncStorage: SyncStorage = {
|
||||||
|
name: 'empty',
|
||||||
|
pull: async () => null,
|
||||||
|
push: async () => {},
|
||||||
|
subscribe: async () => () => {},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ReadonlyMappingSyncStorage = (map: {
|
||||||
|
[key: string]: Uint8Array;
|
||||||
|
}): SyncStorage => ({
|
||||||
|
name: 'map',
|
||||||
|
pull: async (id: string) => {
|
||||||
|
const data = map[id];
|
||||||
|
return data ? { data } : null;
|
||||||
|
},
|
||||||
|
push: async () => {},
|
||||||
|
subscribe: async () => () => {},
|
||||||
|
});
|
||||||
|
@ -74,8 +74,14 @@ export function configureWorkspaceServices(services: ServiceCollection) {
|
|||||||
|
|
||||||
export function configureTestingWorkspaceServices(services: ServiceCollection) {
|
export function configureTestingWorkspaceServices(services: ServiceCollection) {
|
||||||
services
|
services
|
||||||
.addImpl(WorkspaceListProvider, TestingLocalWorkspaceListProvider, [
|
.override(WorkspaceListProvider('affine-cloud'), null)
|
||||||
|
.override(WorkspaceFactory('affine-cloud'), null)
|
||||||
|
.override(
|
||||||
|
WorkspaceListProvider('local'),
|
||||||
|
TestingLocalWorkspaceListProvider,
|
||||||
|
[GlobalState]
|
||||||
|
)
|
||||||
|
.override(WorkspaceFactory('local'), TestingLocalWorkspaceFactory, [
|
||||||
GlobalState,
|
GlobalState,
|
||||||
])
|
]);
|
||||||
.addImpl(WorkspaceFactory, TestingLocalWorkspaceFactory, [GlobalState]);
|
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@ import type { Workspace as BlockSuiteWorkspace } from '@blocksuite/store';
|
|||||||
import { applyUpdate, encodeStateAsUpdate } from 'yjs';
|
import { applyUpdate, encodeStateAsUpdate } from 'yjs';
|
||||||
|
|
||||||
import { fixWorkspaceVersion } from '../blocksuite';
|
import { fixWorkspaceVersion } from '../blocksuite';
|
||||||
import type { ServiceProvider } from '../di';
|
import type { ServiceCollection, ServiceProvider } from '../di';
|
||||||
import { ObjectPool } from '../utils/object-pool';
|
import { ObjectPool } from '../utils/object-pool';
|
||||||
import { configureWorkspaceContext } from './context';
|
import { configureWorkspaceContext } from './context';
|
||||||
import type { BlobStorage } from './engine';
|
import type { BlobStorage } from './engine';
|
||||||
@ -90,6 +90,9 @@ export class WorkspaceManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const workspace = this.instantiate(metadata);
|
const workspace = this.instantiate(metadata);
|
||||||
|
// sync information with workspace list, when workspace's avatar and name changed, information will be updated
|
||||||
|
this.list.getInformation(metadata).syncWithWorkspace(workspace);
|
||||||
|
|
||||||
const ref = this.pool.put(workspace.meta.id, workspace);
|
const ref = this.pool.put(workspace.meta.id, workspace);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -164,14 +167,21 @@ export class WorkspaceManager {
|
|||||||
return factory.getWorkspaceBlob(metadata.id, blobKey);
|
return factory.getWorkspaceBlob(metadata.id, blobKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
private instantiate(metadata: WorkspaceMetadata) {
|
instantiate(
|
||||||
|
metadata: WorkspaceMetadata,
|
||||||
|
configureWorkspace?: (serviceCollection: ServiceCollection) => void
|
||||||
|
) {
|
||||||
logger.info(`open workspace [${metadata.flavour}] ${metadata.id} `);
|
logger.info(`open workspace [${metadata.flavour}] ${metadata.id} `);
|
||||||
const factory = this.factories.find(x => x.name === metadata.flavour);
|
|
||||||
if (!factory) {
|
|
||||||
throw new Error(`Unknown workspace flavour: ${metadata.flavour}`);
|
|
||||||
}
|
|
||||||
const serviceCollection = this.serviceProvider.collection.clone();
|
const serviceCollection = this.serviceProvider.collection.clone();
|
||||||
factory.configureWorkspace(serviceCollection);
|
if (configureWorkspace) {
|
||||||
|
configureWorkspace(serviceCollection);
|
||||||
|
} else {
|
||||||
|
const factory = this.factories.find(x => x.name === metadata.flavour);
|
||||||
|
if (!factory) {
|
||||||
|
throw new Error(`Unknown workspace flavour: ${metadata.flavour}`);
|
||||||
|
}
|
||||||
|
factory.configureWorkspace(serviceCollection);
|
||||||
|
}
|
||||||
configureWorkspaceContext(serviceCollection, metadata);
|
configureWorkspaceContext(serviceCollection, metadata);
|
||||||
const provider = serviceCollection.provider(
|
const provider = serviceCollection.provider(
|
||||||
WorkspaceScope,
|
WorkspaceScope,
|
||||||
@ -179,9 +189,6 @@ export class WorkspaceManager {
|
|||||||
);
|
);
|
||||||
const workspace = provider.get(Workspace);
|
const workspace = provider.get(Workspace);
|
||||||
|
|
||||||
// sync information with workspace list, when workspace's avatar and name changed, information will be updated
|
|
||||||
this.list.getInformation(metadata).syncWithWorkspace(workspace);
|
|
||||||
|
|
||||||
// apply compatibility fix
|
// apply compatibility fix
|
||||||
fixWorkspaceVersion(workspace.blockSuiteWorkspace.doc);
|
fixWorkspaceVersion(workspace.blockSuiteWorkspace.doc);
|
||||||
|
|
||||||
|
1
packages/common/workspace/.gitignore
vendored
1
packages/common/workspace/.gitignore
vendored
@ -1 +0,0 @@
|
|||||||
lib
|
|
@ -1,24 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@affine/workspace",
|
|
||||||
"private": true,
|
|
||||||
"main": "./src/index.ts",
|
|
||||||
"exports": {
|
|
||||||
".": "./src/index.ts"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@blocksuite/blocks": "*",
|
|
||||||
"@blocksuite/global": "*",
|
|
||||||
"@blocksuite/store": "*"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@affine/debug": "workspace:*",
|
|
||||||
"@affine/env": "workspace:*",
|
|
||||||
"@toeverything/infra": "workspace:*",
|
|
||||||
"lodash-es": "^4.17.21",
|
|
||||||
"yjs": "^13.6.10"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"vitest": "1.1.3"
|
|
||||||
},
|
|
||||||
"version": "0.12.0"
|
|
||||||
}
|
|
@ -1,4 +0,0 @@
|
|||||||
export interface AwarenessProvider {
|
|
||||||
connect(): void;
|
|
||||||
disconnect(): void;
|
|
||||||
}
|
|
@ -1,233 +0,0 @@
|
|||||||
import { DebugLogger } from '@affine/debug';
|
|
||||||
import { Slot } from '@blocksuite/global/utils';
|
|
||||||
import { difference } from 'lodash-es';
|
|
||||||
|
|
||||||
import { BlobStorageOverCapacity } from './error';
|
|
||||||
|
|
||||||
const logger = new DebugLogger('affine:blob-engine');
|
|
||||||
|
|
||||||
export interface BlobStatus {
|
|
||||||
isStorageOverCapacity: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* # BlobEngine
|
|
||||||
*
|
|
||||||
* sync blobs between storages in background.
|
|
||||||
*
|
|
||||||
* all operations priority use local, then use remote.
|
|
||||||
*/
|
|
||||||
export class BlobEngine {
|
|
||||||
private abort: AbortController | null = null;
|
|
||||||
private _status: BlobStatus = { isStorageOverCapacity: false };
|
|
||||||
onStatusChange = new Slot<BlobStatus>();
|
|
||||||
singleBlobSizeLimit: number = 100 * 1024 * 1024;
|
|
||||||
onAbortLargeBlob = new Slot<Blob>();
|
|
||||||
|
|
||||||
private set status(s: BlobStatus) {
|
|
||||||
logger.debug('status change', s);
|
|
||||||
this._status = s;
|
|
||||||
this.onStatusChange.emit(s);
|
|
||||||
}
|
|
||||||
get status() {
|
|
||||||
return this._status;
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private readonly local: BlobStorage,
|
|
||||||
private readonly remotes: BlobStorage[]
|
|
||||||
) {}
|
|
||||||
|
|
||||||
start() {
|
|
||||||
if (this.abort) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.abort = new AbortController();
|
|
||||||
const abortSignal = this.abort.signal;
|
|
||||||
|
|
||||||
const sync = () => {
|
|
||||||
if (abortSignal.aborted) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.sync()
|
|
||||||
.catch(error => {
|
|
||||||
logger.error('sync blob error', error);
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
// sync every 1 minute
|
|
||||||
setTimeout(sync, 60000);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
sync();
|
|
||||||
}
|
|
||||||
|
|
||||||
stop() {
|
|
||||||
this.abort?.abort();
|
|
||||||
this.abort = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
get storages() {
|
|
||||||
return [this.local, ...this.remotes];
|
|
||||||
}
|
|
||||||
|
|
||||||
async sync() {
|
|
||||||
if (this.local.readonly || this._status.isStorageOverCapacity) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
logger.debug('start syncing blob...');
|
|
||||||
for (const remote of this.remotes) {
|
|
||||||
let localList: string[] = [];
|
|
||||||
let remoteList: string[] = [];
|
|
||||||
|
|
||||||
if (!remote.readonly) {
|
|
||||||
try {
|
|
||||||
localList = await this.local.list();
|
|
||||||
remoteList = await remote.list();
|
|
||||||
} catch (err) {
|
|
||||||
logger.error(`error when sync`, err);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const needUpload = difference(localList, remoteList);
|
|
||||||
for (const key of needUpload) {
|
|
||||||
try {
|
|
||||||
const data = await this.local.get(key);
|
|
||||||
if (data) {
|
|
||||||
await remote.set(key, data);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
if (err instanceof BlobStorageOverCapacity) {
|
|
||||||
this.status = {
|
|
||||||
isStorageOverCapacity: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
logger.error(
|
|
||||||
`error when sync ${key} from [${this.local.name}] to [${remote.name}]`,
|
|
||||||
err
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const needDownload = difference(remoteList, localList);
|
|
||||||
|
|
||||||
for (const key of needDownload) {
|
|
||||||
try {
|
|
||||||
const data = await remote.get(key);
|
|
||||||
if (data) {
|
|
||||||
await this.local.set(key, data);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
logger.error(
|
|
||||||
`error when sync ${key} from [${remote.name}] to [${this.local.name}]`,
|
|
||||||
err
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.debug('finish syncing blob');
|
|
||||||
}
|
|
||||||
|
|
||||||
async get(key: string) {
|
|
||||||
logger.debug('get blob', key);
|
|
||||||
for (const storage of this.storages) {
|
|
||||||
const data = await storage.get(key);
|
|
||||||
if (data) {
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
async set(key: string, value: Blob) {
|
|
||||||
if (this.local.readonly) {
|
|
||||||
throw new Error('local peer is readonly');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value.size > this.singleBlobSizeLimit) {
|
|
||||||
this.onAbortLargeBlob.emit(value);
|
|
||||||
logger.error('blob over limit, abort set');
|
|
||||||
return key;
|
|
||||||
}
|
|
||||||
|
|
||||||
// await upload to the local peer
|
|
||||||
await this.local.set(key, value);
|
|
||||||
|
|
||||||
// uploads to other peers in the background
|
|
||||||
Promise.allSettled(
|
|
||||||
this.remotes
|
|
||||||
.filter(r => !r.readonly)
|
|
||||||
.map(peer =>
|
|
||||||
peer.set(key, value).catch(err => {
|
|
||||||
logger.error('Error when uploading to peer', err);
|
|
||||||
})
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.then(result => {
|
|
||||||
if (result.some(({ status }) => status === 'rejected')) {
|
|
||||||
logger.error(
|
|
||||||
`blob ${key} update finish, but some peers failed to update`
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
logger.debug(`blob ${key} update finish`);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
// Promise.allSettled never reject
|
|
||||||
});
|
|
||||||
|
|
||||||
return key;
|
|
||||||
}
|
|
||||||
|
|
||||||
async delete(_key: string) {
|
|
||||||
// not supported
|
|
||||||
}
|
|
||||||
|
|
||||||
async list() {
|
|
||||||
const blobList = new Set<string>();
|
|
||||||
|
|
||||||
for (const peer of this.storages) {
|
|
||||||
const list = await peer.list();
|
|
||||||
if (list) {
|
|
||||||
for (const blob of list) {
|
|
||||||
blobList.add(blob);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Array.from(blobList);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BlobStorage {
|
|
||||||
name: string;
|
|
||||||
readonly: boolean;
|
|
||||||
get: (key: string) => Promise<Blob | null>;
|
|
||||||
set: (key: string, value: Blob) => Promise<string>;
|
|
||||||
delete: (key: string) => Promise<void>;
|
|
||||||
list: () => Promise<string[]>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createMemoryBlobStorage() {
|
|
||||||
const map = new Map<string, Blob>();
|
|
||||||
return {
|
|
||||||
name: 'memory',
|
|
||||||
readonly: false,
|
|
||||||
async get(key: string) {
|
|
||||||
return map.get(key) ?? null;
|
|
||||||
},
|
|
||||||
async set(key: string, value: Blob) {
|
|
||||||
map.set(key, value);
|
|
||||||
return key;
|
|
||||||
},
|
|
||||||
async delete(key: string) {
|
|
||||||
map.delete(key);
|
|
||||||
},
|
|
||||||
async list() {
|
|
||||||
return Array.from(map.keys());
|
|
||||||
},
|
|
||||||
} satisfies BlobStorage;
|
|
||||||
}
|
|
@ -1,5 +0,0 @@
|
|||||||
export class BlobStorageOverCapacity extends Error {
|
|
||||||
constructor(public originError?: any) {
|
|
||||||
super('Blob storage over capacity.');
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,84 +0,0 @@
|
|||||||
import { Slot } from '@blocksuite/global/utils';
|
|
||||||
|
|
||||||
import { throwIfAborted } from '../utils/throw-if-aborted';
|
|
||||||
import type { AwarenessProvider } from './awareness';
|
|
||||||
import type { BlobEngine, BlobStatus } from './blob';
|
|
||||||
import type { SyncEngine, SyncEngineStatus } from './sync';
|
|
||||||
|
|
||||||
export interface WorkspaceEngineStatus {
|
|
||||||
sync: SyncEngineStatus;
|
|
||||||
blob: BlobStatus;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* # WorkspaceEngine
|
|
||||||
*
|
|
||||||
* sync ydoc, blob, awareness together
|
|
||||||
*/
|
|
||||||
export class WorkspaceEngine {
|
|
||||||
_status: WorkspaceEngineStatus;
|
|
||||||
onStatusChange = new Slot<WorkspaceEngineStatus>();
|
|
||||||
|
|
||||||
get status() {
|
|
||||||
return this._status;
|
|
||||||
}
|
|
||||||
|
|
||||||
set status(status: WorkspaceEngineStatus) {
|
|
||||||
this._status = status;
|
|
||||||
this.onStatusChange.emit(status);
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
public blob: BlobEngine,
|
|
||||||
public sync: SyncEngine,
|
|
||||||
public awareness: AwarenessProvider[]
|
|
||||||
) {
|
|
||||||
this._status = {
|
|
||||||
sync: sync.status,
|
|
||||||
blob: blob.status,
|
|
||||||
};
|
|
||||||
sync.onStatusChange.on(status => {
|
|
||||||
this.status = {
|
|
||||||
sync: status,
|
|
||||||
blob: blob.status,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
blob.onStatusChange.on(status => {
|
|
||||||
this.status = {
|
|
||||||
sync: sync.status,
|
|
||||||
blob: status,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
start() {
|
|
||||||
this.sync.start();
|
|
||||||
for (const awareness of this.awareness) {
|
|
||||||
awareness.connect();
|
|
||||||
}
|
|
||||||
this.blob.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
canGracefulStop() {
|
|
||||||
return this.sync.canGracefulStop();
|
|
||||||
}
|
|
||||||
|
|
||||||
async waitForGracefulStop(abort?: AbortSignal) {
|
|
||||||
await this.sync.waitForGracefulStop(abort);
|
|
||||||
throwIfAborted(abort);
|
|
||||||
this.forceStop();
|
|
||||||
}
|
|
||||||
|
|
||||||
forceStop() {
|
|
||||||
this.sync.forceStop();
|
|
||||||
for (const awareness of this.awareness) {
|
|
||||||
awareness.disconnect();
|
|
||||||
}
|
|
||||||
this.blob.stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export * from './awareness';
|
|
||||||
export * from './blob';
|
|
||||||
export * from './error';
|
|
||||||
export * from './sync';
|
|
@ -1,15 +0,0 @@
|
|||||||
export enum SyncEngineStep {
|
|
||||||
Stopped = 0,
|
|
||||||
Syncing = 1,
|
|
||||||
Synced = 2,
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum SyncPeerStep {
|
|
||||||
Stopped = 0,
|
|
||||||
Retrying = 1,
|
|
||||||
LoadingRootDoc = 2,
|
|
||||||
LoadingSubDoc = 3,
|
|
||||||
Loaded = 4.5,
|
|
||||||
Syncing = 5,
|
|
||||||
Synced = 6,
|
|
||||||
}
|
|
@ -1,278 +0,0 @@
|
|||||||
import { DebugLogger } from '@affine/debug';
|
|
||||||
import { Slot } from '@blocksuite/global/utils';
|
|
||||||
import type { Doc } from 'yjs';
|
|
||||||
|
|
||||||
import { SharedPriorityTarget } from '../../utils/async-queue';
|
|
||||||
import { MANUALLY_STOP, throwIfAborted } from '../../utils/throw-if-aborted';
|
|
||||||
import { SyncEngineStep, SyncPeerStep } from './consts';
|
|
||||||
import { SyncPeer, type SyncPeerStatus } from './peer';
|
|
||||||
import type { SyncStorage } from './storage';
|
|
||||||
|
|
||||||
export interface SyncEngineStatus {
|
|
||||||
step: SyncEngineStep;
|
|
||||||
local: SyncPeerStatus | null;
|
|
||||||
remotes: (SyncPeerStatus | null)[];
|
|
||||||
retrying: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* # SyncEngine
|
|
||||||
*
|
|
||||||
* ```
|
|
||||||
* ┌────────────┐
|
|
||||||
* │ SyncEngine │
|
|
||||||
* └─────┬──────┘
|
|
||||||
* │
|
|
||||||
* ▼
|
|
||||||
* ┌────────────┐
|
|
||||||
* │ SyncPeer │
|
|
||||||
* ┌─────────┤ local ├─────────┐
|
|
||||||
* │ └─────┬──────┘ │
|
|
||||||
* │ │ │
|
|
||||||
* ▼ ▼ ▼
|
|
||||||
* ┌────────────┐ ┌────────────┐ ┌────────────┐
|
|
||||||
* │ SyncPeer │ │ SyncPeer │ │ SyncPeer │
|
|
||||||
* │ Remote │ │ Remote │ │ Remote │
|
|
||||||
* └────────────┘ └────────────┘ └────────────┘
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* Sync engine manage sync peers
|
|
||||||
*
|
|
||||||
* Sync steps:
|
|
||||||
* 1. start local sync
|
|
||||||
* 2. wait for local sync complete
|
|
||||||
* 3. start remote sync
|
|
||||||
* 4. continuously sync local and remote
|
|
||||||
*/
|
|
||||||
export class SyncEngine {
|
|
||||||
get rootDocId() {
|
|
||||||
return this.rootDoc.guid;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger = new DebugLogger('affine:sync-engine:' + this.rootDocId);
|
|
||||||
private _status: SyncEngineStatus;
|
|
||||||
onStatusChange = new Slot<SyncEngineStatus>();
|
|
||||||
private set status(s: SyncEngineStatus) {
|
|
||||||
this.logger.debug('status change', s);
|
|
||||||
this._status = s;
|
|
||||||
this.onStatusChange.emit(s);
|
|
||||||
}
|
|
||||||
|
|
||||||
priorityTarget = new SharedPriorityTarget();
|
|
||||||
|
|
||||||
get status() {
|
|
||||||
return this._status;
|
|
||||||
}
|
|
||||||
|
|
||||||
private abort = new AbortController();
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private readonly rootDoc: Doc,
|
|
||||||
private readonly local: SyncStorage,
|
|
||||||
private readonly remotes: SyncStorage[]
|
|
||||||
) {
|
|
||||||
this._status = {
|
|
||||||
step: SyncEngineStep.Stopped,
|
|
||||||
local: null,
|
|
||||||
remotes: remotes.map(() => null),
|
|
||||||
retrying: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
start() {
|
|
||||||
if (this.status.step !== SyncEngineStep.Stopped) {
|
|
||||||
this.forceStop();
|
|
||||||
}
|
|
||||||
this.abort = new AbortController();
|
|
||||||
|
|
||||||
this.sync(this.abort.signal).catch(err => {
|
|
||||||
// should never reach here
|
|
||||||
this.logger.error(err);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
canGracefulStop() {
|
|
||||||
return !!this.status.local && this.status.local.pendingPushUpdates === 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
async waitForGracefulStop(abort?: AbortSignal) {
|
|
||||||
await Promise.race([
|
|
||||||
new Promise((_, reject) => {
|
|
||||||
if (abort?.aborted) {
|
|
||||||
reject(abort?.reason);
|
|
||||||
}
|
|
||||||
abort?.addEventListener('abort', () => {
|
|
||||||
reject(abort.reason);
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
new Promise<void>(resolve => {
|
|
||||||
this.onStatusChange.on(() => {
|
|
||||||
if (this.canGracefulStop()) {
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
throwIfAborted(abort);
|
|
||||||
this.forceStop();
|
|
||||||
}
|
|
||||||
|
|
||||||
forceStop() {
|
|
||||||
this.abort.abort(MANUALLY_STOP);
|
|
||||||
this._status = {
|
|
||||||
step: SyncEngineStep.Stopped,
|
|
||||||
local: null,
|
|
||||||
remotes: this.remotes.map(() => null),
|
|
||||||
retrying: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// main sync process, should never return until abort
|
|
||||||
async sync(signal: AbortSignal) {
|
|
||||||
const state: {
|
|
||||||
localPeer: SyncPeer | null;
|
|
||||||
remotePeers: (SyncPeer | null)[];
|
|
||||||
} = {
|
|
||||||
localPeer: null,
|
|
||||||
remotePeers: this.remotes.map(() => null),
|
|
||||||
};
|
|
||||||
|
|
||||||
const cleanUp: (() => void)[] = [];
|
|
||||||
try {
|
|
||||||
// Step 1: start local sync peer
|
|
||||||
state.localPeer = new SyncPeer(
|
|
||||||
this.rootDoc,
|
|
||||||
this.local,
|
|
||||||
this.priorityTarget
|
|
||||||
);
|
|
||||||
|
|
||||||
cleanUp.push(
|
|
||||||
state.localPeer.onStatusChange.on(() => {
|
|
||||||
if (!signal.aborted)
|
|
||||||
this.updateSyncingState(state.localPeer, state.remotePeers);
|
|
||||||
}).dispose
|
|
||||||
);
|
|
||||||
|
|
||||||
this.updateSyncingState(state.localPeer, state.remotePeers);
|
|
||||||
|
|
||||||
// Step 2: wait for local sync complete
|
|
||||||
await state.localPeer.waitForLoaded(signal);
|
|
||||||
|
|
||||||
// Step 3: start remote sync peer
|
|
||||||
state.remotePeers = this.remotes.map(remote => {
|
|
||||||
const peer = new SyncPeer(this.rootDoc, remote, this.priorityTarget);
|
|
||||||
cleanUp.push(
|
|
||||||
peer.onStatusChange.on(() => {
|
|
||||||
if (!signal.aborted)
|
|
||||||
this.updateSyncingState(state.localPeer, state.remotePeers);
|
|
||||||
}).dispose
|
|
||||||
);
|
|
||||||
return peer;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.updateSyncingState(state.localPeer, state.remotePeers);
|
|
||||||
|
|
||||||
// Step 4: continuously sync local and remote
|
|
||||||
|
|
||||||
// wait for abort
|
|
||||||
await new Promise((_, reject) => {
|
|
||||||
if (signal.aborted) {
|
|
||||||
reject(signal.reason);
|
|
||||||
}
|
|
||||||
signal.addEventListener('abort', () => {
|
|
||||||
reject(signal.reason);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
if (error === MANUALLY_STOP || signal.aborted) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
} finally {
|
|
||||||
// stop peers
|
|
||||||
state.localPeer?.stop();
|
|
||||||
for (const remotePeer of state.remotePeers) {
|
|
||||||
remotePeer?.stop();
|
|
||||||
}
|
|
||||||
for (const clean of cleanUp) {
|
|
||||||
clean();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateSyncingState(local: SyncPeer | null, remotes: (SyncPeer | null)[]) {
|
|
||||||
let step = SyncEngineStep.Synced;
|
|
||||||
const allPeer = [local, ...remotes];
|
|
||||||
for (const peer of allPeer) {
|
|
||||||
if (!peer || peer.status.step !== SyncPeerStep.Synced) {
|
|
||||||
step = SyncEngineStep.Syncing;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.status = {
|
|
||||||
step,
|
|
||||||
local: local?.status ?? null,
|
|
||||||
remotes: remotes.map(peer => peer?.status ?? null),
|
|
||||||
retrying: allPeer.some(
|
|
||||||
peer => peer?.status.step === SyncPeerStep.Retrying
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async waitForSynced(abort?: AbortSignal) {
|
|
||||||
if (this.status.step === SyncEngineStep.Synced) {
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
return Promise.race([
|
|
||||||
new Promise<void>(resolve => {
|
|
||||||
this.onStatusChange.on(status => {
|
|
||||||
if (status.step === SyncEngineStep.Synced) {
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
new Promise((_, reject) => {
|
|
||||||
if (abort?.aborted) {
|
|
||||||
reject(abort?.reason);
|
|
||||||
}
|
|
||||||
abort?.addEventListener('abort', () => {
|
|
||||||
reject(abort.reason);
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async waitForLoadedRootDoc(abort?: AbortSignal) {
|
|
||||||
function isLoadedRootDoc(status: SyncEngineStatus) {
|
|
||||||
return ![status.local, ...status.remotes].some(
|
|
||||||
peer => !peer || peer.step <= SyncPeerStep.LoadingRootDoc
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (isLoadedRootDoc(this.status)) {
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
return Promise.race([
|
|
||||||
new Promise<void>(resolve => {
|
|
||||||
this.onStatusChange.on(status => {
|
|
||||||
if (isLoadedRootDoc(status)) {
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
new Promise((_, reject) => {
|
|
||||||
if (abort?.aborted) {
|
|
||||||
reject(abort?.reason);
|
|
||||||
}
|
|
||||||
abort?.addEventListener('abort', () => {
|
|
||||||
reject(abort.reason);
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setPriorityRule(target: ((id: string) => boolean) | null) {
|
|
||||||
this.priorityTarget.priorityRule = target;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,20 +0,0 @@
|
|||||||
/**
|
|
||||||
*
|
|
||||||
* **SyncEngine**
|
|
||||||
*
|
|
||||||
* Manages one local storage and multiple remote storages.
|
|
||||||
*
|
|
||||||
* Responsible for creating SyncPeers for synchronization, following the local-first strategy.
|
|
||||||
*
|
|
||||||
* **SyncPeer**
|
|
||||||
*
|
|
||||||
* Responsible for synchronizing a single storage with Y.Doc.
|
|
||||||
*
|
|
||||||
* Carries the main synchronization logic.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
export * from './consts';
|
|
||||||
export * from './engine';
|
|
||||||
export * from './peer';
|
|
||||||
export * from './storage';
|
|
@ -1,444 +0,0 @@
|
|||||||
import { DebugLogger } from '@affine/debug';
|
|
||||||
import { Slot } from '@blocksuite/global/utils';
|
|
||||||
import { isEqual } from '@blocksuite/global/utils';
|
|
||||||
import type { Doc } from 'yjs';
|
|
||||||
import { applyUpdate, encodeStateAsUpdate, encodeStateVector } from 'yjs';
|
|
||||||
|
|
||||||
import {
|
|
||||||
PriorityAsyncQueue,
|
|
||||||
SharedPriorityTarget,
|
|
||||||
} from '../../utils/async-queue';
|
|
||||||
import { mergeUpdates } from '../../utils/merge-updates';
|
|
||||||
import { MANUALLY_STOP, throwIfAborted } from '../../utils/throw-if-aborted';
|
|
||||||
import { SyncPeerStep } from './consts';
|
|
||||||
import type { SyncStorage } from './storage';
|
|
||||||
|
|
||||||
export interface SyncPeerStatus {
|
|
||||||
step: SyncPeerStep;
|
|
||||||
totalDocs: number;
|
|
||||||
loadedDocs: number;
|
|
||||||
pendingPullUpdates: number;
|
|
||||||
pendingPushUpdates: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* # SyncPeer
|
|
||||||
* A SyncPeer is responsible for syncing one Storage with one Y.Doc and its subdocs.
|
|
||||||
*
|
|
||||||
* ```
|
|
||||||
* ┌─────┐
|
|
||||||
* │Start│
|
|
||||||
* └──┬──┘
|
|
||||||
* │
|
|
||||||
* ┌──────┐ ┌─────▼──────┐ ┌────┐
|
|
||||||
* │listen◄─────┤pull rootdoc│ │peer│
|
|
||||||
* └──┬───┘ └─────┬──────┘ └──┬─┘
|
|
||||||
* │ │ onLoad() │
|
|
||||||
* ┌──▼───┐ ┌─────▼──────┐ ┌────▼────┐
|
|
||||||
* │listen◄─────┤pull subdocs│ │subscribe│
|
|
||||||
* └──┬───┘ └─────┬──────┘ └────┬────┘
|
|
||||||
* │ │ onReady() │
|
|
||||||
* ┌──▼──┐ ┌─────▼───────┐ ┌──▼──┐
|
|
||||||
* │queue├──────►apply updates◄───────┤queue│
|
|
||||||
* └─────┘ └─────────────┘ └─────┘
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* listen: listen for updates from ydoc, typically from user modifications.
|
|
||||||
* subscribe: listen for updates from storage, typically from other users.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
export class SyncPeer {
|
|
||||||
private _status: SyncPeerStatus = {
|
|
||||||
step: SyncPeerStep.LoadingRootDoc,
|
|
||||||
totalDocs: 1,
|
|
||||||
loadedDocs: 0,
|
|
||||||
pendingPullUpdates: 0,
|
|
||||||
pendingPushUpdates: 0,
|
|
||||||
};
|
|
||||||
onStatusChange = new Slot<SyncPeerStatus>();
|
|
||||||
readonly abort = new AbortController();
|
|
||||||
get name() {
|
|
||||||
return this.storage.name;
|
|
||||||
}
|
|
||||||
logger = new DebugLogger('affine:sync-peer:' + this.name);
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private readonly rootDoc: Doc,
|
|
||||||
private readonly storage: SyncStorage,
|
|
||||||
private readonly priorityTarget = new SharedPriorityTarget()
|
|
||||||
) {
|
|
||||||
this.logger.debug('peer start');
|
|
||||||
|
|
||||||
this.syncRetryLoop(this.abort.signal).catch(err => {
|
|
||||||
// should not reach here
|
|
||||||
console.error(err);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private set status(s: SyncPeerStatus) {
|
|
||||||
if (!isEqual(s, this._status)) {
|
|
||||||
this.logger.debug('status change', s);
|
|
||||||
this._status = s;
|
|
||||||
this.onStatusChange.emit(s);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get status() {
|
|
||||||
return this._status;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* stop sync
|
|
||||||
*
|
|
||||||
* SyncPeer is one-time use, this peer should be discarded after call stop().
|
|
||||||
*/
|
|
||||||
stop() {
|
|
||||||
this.logger.debug('peer stop');
|
|
||||||
this.abort.abort(MANUALLY_STOP);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* auto retry after 5 seconds if sync failed
|
|
||||||
*/
|
|
||||||
async syncRetryLoop(abort: AbortSignal) {
|
|
||||||
while (abort.aborted === false) {
|
|
||||||
try {
|
|
||||||
await this.sync(abort);
|
|
||||||
} catch (err) {
|
|
||||||
if (err === MANUALLY_STOP || abort.aborted) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.logger.error('sync error', err);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
this.logger.error('retry after 5 seconds');
|
|
||||||
this.status = {
|
|
||||||
step: SyncPeerStep.Retrying,
|
|
||||||
totalDocs: 1,
|
|
||||||
loadedDocs: 0,
|
|
||||||
pendingPullUpdates: 0,
|
|
||||||
pendingPushUpdates: 0,
|
|
||||||
};
|
|
||||||
await Promise.race([
|
|
||||||
new Promise<void>(resolve => {
|
|
||||||
setTimeout(resolve, 5 * 1000);
|
|
||||||
}),
|
|
||||||
new Promise((_, reject) => {
|
|
||||||
// exit if manually stopped
|
|
||||||
if (abort.aborted) {
|
|
||||||
reject(abort.reason);
|
|
||||||
}
|
|
||||||
abort.addEventListener('abort', () => {
|
|
||||||
reject(abort.reason);
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
} catch (err) {
|
|
||||||
if (err === MANUALLY_STOP || abort.aborted) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// should never reach here
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly state: {
|
|
||||||
connectedDocs: Map<string, Doc>;
|
|
||||||
pushUpdatesQueue: PriorityAsyncQueue<{
|
|
||||||
id: string;
|
|
||||||
data: Uint8Array[];
|
|
||||||
}>;
|
|
||||||
pushingUpdate: boolean;
|
|
||||||
pullUpdatesQueue: PriorityAsyncQueue<{
|
|
||||||
id: string;
|
|
||||||
data: Uint8Array;
|
|
||||||
}>;
|
|
||||||
subdocLoading: boolean;
|
|
||||||
subdocsLoadQueue: PriorityAsyncQueue<{ id: string; doc: Doc }>;
|
|
||||||
} = {
|
|
||||||
connectedDocs: new Map(),
|
|
||||||
pushUpdatesQueue: new PriorityAsyncQueue([], this.priorityTarget),
|
|
||||||
pushingUpdate: false,
|
|
||||||
pullUpdatesQueue: new PriorityAsyncQueue([], this.priorityTarget),
|
|
||||||
subdocLoading: false,
|
|
||||||
subdocsLoadQueue: new PriorityAsyncQueue([], this.priorityTarget),
|
|
||||||
};
|
|
||||||
|
|
||||||
initState() {
|
|
||||||
this.state.connectedDocs.clear();
|
|
||||||
this.state.pushUpdatesQueue.clear();
|
|
||||||
this.state.pullUpdatesQueue.clear();
|
|
||||||
this.state.subdocsLoadQueue.clear();
|
|
||||||
this.state.pushingUpdate = false;
|
|
||||||
this.state.subdocLoading = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* main synchronization logic
|
|
||||||
*/
|
|
||||||
async sync(abortOuter: AbortSignal) {
|
|
||||||
this.initState();
|
|
||||||
const abortInner = new AbortController();
|
|
||||||
|
|
||||||
abortOuter.addEventListener('abort', reason => {
|
|
||||||
abortInner.abort(reason);
|
|
||||||
});
|
|
||||||
|
|
||||||
let dispose: (() => void) | null = null;
|
|
||||||
try {
|
|
||||||
this.reportSyncStatus();
|
|
||||||
|
|
||||||
// start listen storage updates
|
|
||||||
dispose = await this.storage.subscribe(
|
|
||||||
this.handleStorageUpdates,
|
|
||||||
reason => {
|
|
||||||
// abort if storage disconnect, should trigger retry loop
|
|
||||||
abortInner.abort('subscribe disconnect:' + reason);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
throwIfAborted(abortInner.signal);
|
|
||||||
|
|
||||||
// Step 1: load root doc
|
|
||||||
await this.connectDoc(this.rootDoc, abortInner.signal);
|
|
||||||
|
|
||||||
// Step 2: load subdocs
|
|
||||||
this.state.subdocsLoadQueue.push(
|
|
||||||
...Array.from(this.rootDoc.getSubdocs()).map(doc => ({
|
|
||||||
id: doc.guid,
|
|
||||||
doc,
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
this.reportSyncStatus();
|
|
||||||
|
|
||||||
this.rootDoc.on('subdocs', this.handleSubdocsUpdate);
|
|
||||||
|
|
||||||
// Finally: start sync
|
|
||||||
await Promise.all([
|
|
||||||
// load subdocs
|
|
||||||
(async () => {
|
|
||||||
while (throwIfAborted(abortInner.signal)) {
|
|
||||||
const subdoc = await this.state.subdocsLoadQueue.next(
|
|
||||||
abortInner.signal
|
|
||||||
);
|
|
||||||
this.state.subdocLoading = true;
|
|
||||||
this.reportSyncStatus();
|
|
||||||
await this.connectDoc(subdoc.doc, abortInner.signal);
|
|
||||||
this.state.subdocLoading = false;
|
|
||||||
this.reportSyncStatus();
|
|
||||||
}
|
|
||||||
})(),
|
|
||||||
// pull updates
|
|
||||||
(async () => {
|
|
||||||
while (throwIfAborted(abortInner.signal)) {
|
|
||||||
const { id, data } = await this.state.pullUpdatesQueue.next(
|
|
||||||
abortInner.signal
|
|
||||||
);
|
|
||||||
// don't apply empty data or Uint8Array([0, 0])
|
|
||||||
if (
|
|
||||||
!(
|
|
||||||
data.byteLength === 0 ||
|
|
||||||
(data.byteLength === 2 && data[0] === 0 && data[1] === 0)
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
const subdoc = this.state.connectedDocs.get(id);
|
|
||||||
if (subdoc) {
|
|
||||||
applyUpdate(subdoc, data, this.name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.reportSyncStatus();
|
|
||||||
}
|
|
||||||
})(),
|
|
||||||
// push updates
|
|
||||||
(async () => {
|
|
||||||
while (throwIfAborted(abortInner.signal)) {
|
|
||||||
const { id, data } = await this.state.pushUpdatesQueue.next(
|
|
||||||
abortInner.signal
|
|
||||||
);
|
|
||||||
this.state.pushingUpdate = true;
|
|
||||||
this.reportSyncStatus();
|
|
||||||
|
|
||||||
const merged = mergeUpdates(data);
|
|
||||||
|
|
||||||
// don't push empty data or Uint8Array([0, 0])
|
|
||||||
if (
|
|
||||||
!(
|
|
||||||
merged.byteLength === 0 ||
|
|
||||||
(merged.byteLength === 2 && merged[0] === 0 && merged[1] === 0)
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
await this.storage.push(id, merged);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.state.pushingUpdate = false;
|
|
||||||
this.reportSyncStatus();
|
|
||||||
}
|
|
||||||
})(),
|
|
||||||
]);
|
|
||||||
} finally {
|
|
||||||
dispose?.();
|
|
||||||
for (const docs of this.state.connectedDocs.values()) {
|
|
||||||
this.disconnectDoc(docs);
|
|
||||||
}
|
|
||||||
this.rootDoc.off('subdocs', this.handleSubdocsUpdate);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async connectDoc(doc: Doc, abort: AbortSignal) {
|
|
||||||
const { data: docData, state: inStorageState } =
|
|
||||||
(await this.storage.pull(doc.guid, encodeStateVector(doc))) ?? {};
|
|
||||||
throwIfAborted(abort);
|
|
||||||
|
|
||||||
if (docData) {
|
|
||||||
applyUpdate(doc, docData, 'load');
|
|
||||||
}
|
|
||||||
|
|
||||||
// diff root doc and in-storage, save updates to pendingUpdates
|
|
||||||
this.state.pushUpdatesQueue.push({
|
|
||||||
id: doc.guid,
|
|
||||||
data: [encodeStateAsUpdate(doc, inStorageState)],
|
|
||||||
});
|
|
||||||
|
|
||||||
this.state.connectedDocs.set(doc.guid, doc);
|
|
||||||
|
|
||||||
// start listen root doc changes
|
|
||||||
doc.on('update', this.handleYDocUpdates);
|
|
||||||
|
|
||||||
// mark rootDoc as loaded
|
|
||||||
doc.emit('sync', [true]);
|
|
||||||
|
|
||||||
this.reportSyncStatus();
|
|
||||||
}
|
|
||||||
|
|
||||||
disconnectDoc(doc: Doc) {
|
|
||||||
doc.off('update', this.handleYDocUpdates);
|
|
||||||
this.state.connectedDocs.delete(doc.guid);
|
|
||||||
this.reportSyncStatus();
|
|
||||||
}
|
|
||||||
|
|
||||||
// handle updates from ydoc
|
|
||||||
handleYDocUpdates = (update: Uint8Array, origin: string, doc: Doc) => {
|
|
||||||
// don't push updates from storage
|
|
||||||
if (origin === this.name) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const exist = this.state.pushUpdatesQueue.find(({ id }) => id === doc.guid);
|
|
||||||
if (exist) {
|
|
||||||
exist.data.push(update);
|
|
||||||
} else {
|
|
||||||
this.state.pushUpdatesQueue.push({
|
|
||||||
id: doc.guid,
|
|
||||||
data: [update],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.reportSyncStatus();
|
|
||||||
};
|
|
||||||
|
|
||||||
// handle subdocs changes, append new subdocs to queue, remove subdocs from queue
|
|
||||||
handleSubdocsUpdate = ({
|
|
||||||
added,
|
|
||||||
removed,
|
|
||||||
}: {
|
|
||||||
added: Set<Doc>;
|
|
||||||
removed: Set<Doc>;
|
|
||||||
}) => {
|
|
||||||
for (const subdoc of added) {
|
|
||||||
this.state.subdocsLoadQueue.push({ id: subdoc.guid, doc: subdoc });
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const subdoc of removed) {
|
|
||||||
this.disconnectDoc(subdoc);
|
|
||||||
this.state.subdocsLoadQueue.remove(doc => doc.doc === subdoc);
|
|
||||||
}
|
|
||||||
this.reportSyncStatus();
|
|
||||||
};
|
|
||||||
|
|
||||||
// handle updates from storage
|
|
||||||
handleStorageUpdates = (id: string, data: Uint8Array) => {
|
|
||||||
this.state.pullUpdatesQueue.push({
|
|
||||||
id,
|
|
||||||
data,
|
|
||||||
});
|
|
||||||
this.reportSyncStatus();
|
|
||||||
};
|
|
||||||
|
|
||||||
reportSyncStatus() {
|
|
||||||
let step;
|
|
||||||
if (this.state.connectedDocs.size === 0) {
|
|
||||||
step = SyncPeerStep.LoadingRootDoc;
|
|
||||||
} else if (this.state.subdocsLoadQueue.length || this.state.subdocLoading) {
|
|
||||||
step = SyncPeerStep.LoadingSubDoc;
|
|
||||||
} else if (
|
|
||||||
this.state.pullUpdatesQueue.length ||
|
|
||||||
this.state.pushUpdatesQueue.length ||
|
|
||||||
this.state.pushingUpdate
|
|
||||||
) {
|
|
||||||
step = SyncPeerStep.Syncing;
|
|
||||||
} else {
|
|
||||||
step = SyncPeerStep.Synced;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.status = {
|
|
||||||
step: step,
|
|
||||||
totalDocs:
|
|
||||||
this.state.connectedDocs.size + this.state.subdocsLoadQueue.length,
|
|
||||||
loadedDocs: this.state.connectedDocs.size,
|
|
||||||
pendingPullUpdates:
|
|
||||||
this.state.pullUpdatesQueue.length + (this.state.subdocLoading ? 1 : 0),
|
|
||||||
pendingPushUpdates:
|
|
||||||
this.state.pushUpdatesQueue.length + (this.state.pushingUpdate ? 1 : 0),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async waitForSynced(abort?: AbortSignal) {
|
|
||||||
if (this.status.step >= SyncPeerStep.Synced) {
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
return Promise.race([
|
|
||||||
new Promise<void>(resolve => {
|
|
||||||
this.onStatusChange.on(status => {
|
|
||||||
if (status.step >= SyncPeerStep.Synced) {
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
new Promise((_, reject) => {
|
|
||||||
if (abort?.aborted) {
|
|
||||||
reject(abort?.reason);
|
|
||||||
}
|
|
||||||
abort?.addEventListener('abort', () => {
|
|
||||||
reject(abort.reason);
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async waitForLoaded(abort?: AbortSignal) {
|
|
||||||
if (this.status.step > SyncPeerStep.Loaded) {
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
return Promise.race([
|
|
||||||
new Promise<void>(resolve => {
|
|
||||||
this.onStatusChange.on(status => {
|
|
||||||
if (status.step > SyncPeerStep.Loaded) {
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
new Promise((_, reject) => {
|
|
||||||
if (abort?.aborted) {
|
|
||||||
reject(abort?.reason);
|
|
||||||
}
|
|
||||||
abort?.addEventListener('abort', () => {
|
|
||||||
reject(abort.reason);
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,25 +0,0 @@
|
|||||||
export interface SyncStorage {
|
|
||||||
/**
|
|
||||||
* for debug
|
|
||||||
*/
|
|
||||||
name: string;
|
|
||||||
|
|
||||||
pull(
|
|
||||||
docId: string,
|
|
||||||
state: Uint8Array
|
|
||||||
): Promise<{ data: Uint8Array; state?: Uint8Array } | null>;
|
|
||||||
push(docId: string, data: Uint8Array): Promise<void>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Subscribe to updates from peer
|
|
||||||
*
|
|
||||||
* @param cb callback to handle updates
|
|
||||||
* @param disconnect callback to handle disconnect, reason can be something like 'network-error'
|
|
||||||
*
|
|
||||||
* @returns unsubscribe function
|
|
||||||
*/
|
|
||||||
subscribe(
|
|
||||||
cb: (docId: string, data: Uint8Array) => void,
|
|
||||||
disconnect: (reason: string) => void
|
|
||||||
): Promise<() => void>;
|
|
||||||
}
|
|
@ -1,13 +0,0 @@
|
|||||||
import type { WorkspaceMetadata } from './metadata';
|
|
||||||
import type { Workspace } from './workspace';
|
|
||||||
|
|
||||||
export interface WorkspaceFactory {
|
|
||||||
name: string;
|
|
||||||
|
|
||||||
openWorkspace(metadata: WorkspaceMetadata): Workspace;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* get blob without open workspace
|
|
||||||
*/
|
|
||||||
getWorkspaceBlob(id: string, blobKey: string): Promise<Blob | null>;
|
|
||||||
}
|
|
@ -1,6 +0,0 @@
|
|||||||
import { __unstableSchemas, AffineSchemas } from '@blocksuite/blocks/models';
|
|
||||||
import { Schema } from '@blocksuite/store';
|
|
||||||
|
|
||||||
export const globalBlockSuiteSchema = new Schema();
|
|
||||||
|
|
||||||
globalBlockSuiteSchema.register(AffineSchemas).register(__unstableSchemas);
|
|
@ -1,7 +0,0 @@
|
|||||||
export * from './engine';
|
|
||||||
export * from './factory';
|
|
||||||
export * from './global-schema';
|
|
||||||
export * from './list';
|
|
||||||
export * from './manager';
|
|
||||||
export * from './metadata';
|
|
||||||
export * from './workspace';
|
|
@ -1,21 +0,0 @@
|
|||||||
import { type WorkspaceMetadata } from '../metadata';
|
|
||||||
|
|
||||||
const CACHE_STORAGE_KEY = 'jotai-workspaces';
|
|
||||||
|
|
||||||
export function readWorkspaceListCache() {
|
|
||||||
const metadata = localStorage.getItem(CACHE_STORAGE_KEY);
|
|
||||||
if (metadata) {
|
|
||||||
try {
|
|
||||||
const items = JSON.parse(metadata) as WorkspaceMetadata[];
|
|
||||||
return [...items];
|
|
||||||
} catch (e) {
|
|
||||||
console.error('cannot parse worksapce', e);
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function writeWorkspaceListCache(metadata: WorkspaceMetadata[]) {
|
|
||||||
localStorage.setItem(CACHE_STORAGE_KEY, JSON.stringify(metadata));
|
|
||||||
}
|
|
@ -1,300 +0,0 @@
|
|||||||
import { DebugLogger } from '@affine/debug';
|
|
||||||
import type { WorkspaceFlavour } from '@affine/env/workspace';
|
|
||||||
import { Slot } from '@blocksuite/global/utils';
|
|
||||||
import type { Workspace as BlockSuiteWorkspace } from '@blocksuite/store';
|
|
||||||
import { differenceWith } from 'lodash-es';
|
|
||||||
|
|
||||||
import type { BlobStorage } from '../engine';
|
|
||||||
import type { WorkspaceMetadata } from '../metadata';
|
|
||||||
import { readWorkspaceListCache, writeWorkspaceListCache } from './cache';
|
|
||||||
import { type WorkspaceInfo, WorkspaceInformation } from './information';
|
|
||||||
|
|
||||||
export * from './information';
|
|
||||||
|
|
||||||
const logger = new DebugLogger('affine:workspace:list');
|
|
||||||
|
|
||||||
export interface WorkspaceListProvider {
|
|
||||||
name: WorkspaceFlavour;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* get workspaces list
|
|
||||||
*/
|
|
||||||
getList(): Promise<WorkspaceMetadata[]>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* delete workspace by id
|
|
||||||
*/
|
|
||||||
delete(workspaceId: string): Promise<void>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* create workspace
|
|
||||||
* @param initial callback to put initial data to workspace
|
|
||||||
*/
|
|
||||||
create(
|
|
||||||
initial: (
|
|
||||||
workspace: BlockSuiteWorkspace,
|
|
||||||
blobStorage: BlobStorage
|
|
||||||
) => Promise<void>
|
|
||||||
): Promise<string>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Start subscribe workspaces list
|
|
||||||
*
|
|
||||||
* @returns unsubscribe function
|
|
||||||
*/
|
|
||||||
subscribe(
|
|
||||||
callback: (changed: {
|
|
||||||
added?: WorkspaceMetadata[];
|
|
||||||
deleted?: WorkspaceMetadata[];
|
|
||||||
}) => void
|
|
||||||
): () => void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* get workspace avatar and name by id
|
|
||||||
*
|
|
||||||
* @param id workspace id
|
|
||||||
*/
|
|
||||||
getInformation(id: string): Promise<WorkspaceInfo | undefined>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface WorkspaceListStatus {
|
|
||||||
/**
|
|
||||||
* is workspace list doing first loading.
|
|
||||||
* if false, UI can display workspace not found page.
|
|
||||||
*/
|
|
||||||
loading: boolean;
|
|
||||||
workspaceList: WorkspaceMetadata[];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* # WorkspaceList
|
|
||||||
*
|
|
||||||
* manage multiple workspace metadata list providers.
|
|
||||||
* provide a __cache-first__ and __offline useable__ workspace list.
|
|
||||||
*/
|
|
||||||
export class WorkspaceList {
|
|
||||||
private readonly abortController = new AbortController();
|
|
||||||
|
|
||||||
private readonly workspaceInformationList = new Map<
|
|
||||||
string,
|
|
||||||
WorkspaceInformation
|
|
||||||
>();
|
|
||||||
|
|
||||||
onStatusChanged = new Slot<WorkspaceListStatus>();
|
|
||||||
private _status: Readonly<WorkspaceListStatus> = {
|
|
||||||
loading: true,
|
|
||||||
workspaceList: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
get status() {
|
|
||||||
return this._status;
|
|
||||||
}
|
|
||||||
|
|
||||||
set status(status) {
|
|
||||||
this._status = status;
|
|
||||||
// update cache
|
|
||||||
writeWorkspaceListCache(status.workspaceList);
|
|
||||||
this.onStatusChanged.emit(this._status);
|
|
||||||
}
|
|
||||||
|
|
||||||
get workspaceList() {
|
|
||||||
return this.status.workspaceList;
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(private readonly providers: WorkspaceListProvider[]) {
|
|
||||||
// initialize workspace list from cache
|
|
||||||
const cache = readWorkspaceListCache();
|
|
||||||
const workspaceList = cache;
|
|
||||||
this.status = {
|
|
||||||
...this.status,
|
|
||||||
workspaceList,
|
|
||||||
};
|
|
||||||
|
|
||||||
// start first load
|
|
||||||
this.startLoad();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* create workspace
|
|
||||||
* @param flavour workspace flavour
|
|
||||||
* @param initial callback to put initial data to workspace
|
|
||||||
* @returns workspace id
|
|
||||||
*/
|
|
||||||
async create(
|
|
||||||
flavour: WorkspaceFlavour,
|
|
||||||
initial: (
|
|
||||||
workspace: BlockSuiteWorkspace,
|
|
||||||
blobStorage: BlobStorage
|
|
||||||
) => Promise<void>
|
|
||||||
) {
|
|
||||||
const provider = this.providers.find(x => x.name === flavour);
|
|
||||||
if (!provider) {
|
|
||||||
throw new Error(`Unknown workspace flavour: ${flavour}`);
|
|
||||||
}
|
|
||||||
const id = await provider.create(initial);
|
|
||||||
const metadata = {
|
|
||||||
id,
|
|
||||||
flavour,
|
|
||||||
};
|
|
||||||
// update workspace list
|
|
||||||
this.status = this.addWorkspace(this.status, metadata);
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* delete workspace
|
|
||||||
* @param workspaceMetadata
|
|
||||||
*/
|
|
||||||
async delete(workspaceMetadata: WorkspaceMetadata) {
|
|
||||||
logger.info(
|
|
||||||
`delete workspace [${workspaceMetadata.flavour}] ${workspaceMetadata.id}`
|
|
||||||
);
|
|
||||||
const provider = this.providers.find(
|
|
||||||
x => x.name === workspaceMetadata.flavour
|
|
||||||
);
|
|
||||||
if (!provider) {
|
|
||||||
throw new Error(
|
|
||||||
`Unknown workspace flavour: ${workspaceMetadata.flavour}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
await provider.delete(workspaceMetadata.id);
|
|
||||||
|
|
||||||
// delete workspace from list
|
|
||||||
this.status = this.deleteWorkspace(this.status, workspaceMetadata);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* add workspace to list
|
|
||||||
*/
|
|
||||||
private addWorkspace(
|
|
||||||
status: WorkspaceListStatus,
|
|
||||||
workspaceMetadata: WorkspaceMetadata
|
|
||||||
) {
|
|
||||||
if (status.workspaceList.some(x => x.id === workspaceMetadata.id)) {
|
|
||||||
return status;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
...status,
|
|
||||||
workspaceList: status.workspaceList.concat(workspaceMetadata),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* delete workspace from list
|
|
||||||
*/
|
|
||||||
private deleteWorkspace(
|
|
||||||
status: WorkspaceListStatus,
|
|
||||||
workspaceMetadata: WorkspaceMetadata
|
|
||||||
) {
|
|
||||||
if (!status.workspaceList.some(x => x.id === workspaceMetadata.id)) {
|
|
||||||
return status;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
...status,
|
|
||||||
workspaceList: status.workspaceList.filter(
|
|
||||||
x => x.id !== workspaceMetadata.id
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* callback for subscribe workspaces list
|
|
||||||
*/
|
|
||||||
private handleWorkspaceChange(changed: {
|
|
||||||
added?: WorkspaceMetadata[];
|
|
||||||
deleted?: WorkspaceMetadata[];
|
|
||||||
}) {
|
|
||||||
let status = this.status;
|
|
||||||
|
|
||||||
for (const added of changed.added ?? []) {
|
|
||||||
status = this.addWorkspace(status, added);
|
|
||||||
}
|
|
||||||
for (const deleted of changed.deleted ?? []) {
|
|
||||||
status = this.deleteWorkspace(status, deleted);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.status = status;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* start first load workspace list
|
|
||||||
*/
|
|
||||||
private startLoad() {
|
|
||||||
for (const provider of this.providers) {
|
|
||||||
// subscribe workspace list change
|
|
||||||
const unsubscribe = provider.subscribe(changed => {
|
|
||||||
this.handleWorkspaceChange(changed);
|
|
||||||
});
|
|
||||||
|
|
||||||
// unsubscribe when abort
|
|
||||||
if (this.abortController.signal.aborted) {
|
|
||||||
unsubscribe();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.abortController.signal.addEventListener('abort', () => {
|
|
||||||
unsubscribe();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.revalidate()
|
|
||||||
.catch(error => {
|
|
||||||
logger.error('load workspace list error: ' + error);
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
this.status = {
|
|
||||||
...this.status,
|
|
||||||
loading: false,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async revalidate() {
|
|
||||||
await Promise.allSettled(
|
|
||||||
this.providers.map(async provider => {
|
|
||||||
try {
|
|
||||||
const list = await provider.getList();
|
|
||||||
const oldList = this.workspaceList.filter(
|
|
||||||
w => w.flavour === provider.name
|
|
||||||
);
|
|
||||||
this.handleWorkspaceChange({
|
|
||||||
added: differenceWith(list, oldList, (a, b) => a.id === b.id),
|
|
||||||
deleted: differenceWith(oldList, list, (a, b) => a.id === b.id),
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('load workspace list error: ' + error);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* get workspace information, if not exists, create it.
|
|
||||||
*/
|
|
||||||
getInformation(meta: WorkspaceMetadata) {
|
|
||||||
const exists = this.workspaceInformationList.get(meta.id);
|
|
||||||
if (exists) {
|
|
||||||
return exists;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.createInformation(meta);
|
|
||||||
}
|
|
||||||
|
|
||||||
private createInformation(workspaceMetadata: WorkspaceMetadata) {
|
|
||||||
const provider = this.providers.find(
|
|
||||||
x => x.name === workspaceMetadata.flavour
|
|
||||||
);
|
|
||||||
if (!provider) {
|
|
||||||
throw new Error(
|
|
||||||
`Unknown workspace flavour: ${workspaceMetadata.flavour}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const information = new WorkspaceInformation(workspaceMetadata, provider);
|
|
||||||
information.fetch();
|
|
||||||
this.workspaceInformationList.set(workspaceMetadata.id, information);
|
|
||||||
return information;
|
|
||||||
}
|
|
||||||
|
|
||||||
dispose() {
|
|
||||||
this.abortController.abort();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,97 +0,0 @@
|
|||||||
import { DebugLogger } from '@affine/debug';
|
|
||||||
import { Slot } from '@blocksuite/global/utils';
|
|
||||||
|
|
||||||
import type { WorkspaceMetadata } from '../metadata';
|
|
||||||
import type { Workspace } from '../workspace';
|
|
||||||
import type { WorkspaceListProvider } from './index';
|
|
||||||
|
|
||||||
const logger = new DebugLogger('affine:workspace:list:information');
|
|
||||||
|
|
||||||
const WORKSPACE_INFORMATION_CACHE_KEY = 'workspace-information:';
|
|
||||||
|
|
||||||
export interface WorkspaceInfo {
|
|
||||||
avatar?: string;
|
|
||||||
name?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* # WorkspaceInformation
|
|
||||||
*
|
|
||||||
* This class take care of workspace avatar and name
|
|
||||||
*
|
|
||||||
* The class will try to get from 3 places:
|
|
||||||
* - local cache
|
|
||||||
* - fetch from `WorkspaceListProvider`, which will fetch from database or server
|
|
||||||
* - sync with active workspace
|
|
||||||
*/
|
|
||||||
export class WorkspaceInformation {
|
|
||||||
private _info: WorkspaceInfo = {};
|
|
||||||
|
|
||||||
public set info(info: WorkspaceInfo) {
|
|
||||||
if (info.avatar !== this._info.avatar || info.name !== this._info.name) {
|
|
||||||
localStorage.setItem(
|
|
||||||
WORKSPACE_INFORMATION_CACHE_KEY + this.meta.id,
|
|
||||||
JSON.stringify(info)
|
|
||||||
);
|
|
||||||
this._info = info;
|
|
||||||
this.onUpdated.emit(info);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public get info() {
|
|
||||||
return this._info;
|
|
||||||
}
|
|
||||||
|
|
||||||
public onUpdated = new Slot<WorkspaceInfo>();
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
public meta: WorkspaceMetadata,
|
|
||||||
public provider: WorkspaceListProvider
|
|
||||||
) {
|
|
||||||
const cached = this.getCachedInformation();
|
|
||||||
// init with cached information
|
|
||||||
this.info = { ...cached };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* sync information with workspace
|
|
||||||
*/
|
|
||||||
syncWithWorkspace(workspace: Workspace) {
|
|
||||||
this.info = {
|
|
||||||
avatar: workspace.blockSuiteWorkspace.meta.avatar ?? this.info.avatar,
|
|
||||||
name: workspace.blockSuiteWorkspace.meta.name ?? this.info.name,
|
|
||||||
};
|
|
||||||
workspace.blockSuiteWorkspace.meta.commonFieldsUpdated.on(() => {
|
|
||||||
this.info = {
|
|
||||||
avatar: workspace.blockSuiteWorkspace.meta.avatar ?? this.info.avatar,
|
|
||||||
name: workspace.blockSuiteWorkspace.meta.name ?? this.info.name,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
getCachedInformation() {
|
|
||||||
const cache = localStorage.getItem(
|
|
||||||
WORKSPACE_INFORMATION_CACHE_KEY + this.meta.id
|
|
||||||
);
|
|
||||||
if (cache) {
|
|
||||||
return JSON.parse(cache) as WorkspaceInfo;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* fetch information from provider
|
|
||||||
*/
|
|
||||||
fetch() {
|
|
||||||
this.provider
|
|
||||||
.getInformation(this.meta.id)
|
|
||||||
.then(info => {
|
|
||||||
if (info) {
|
|
||||||
this.info = info;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
logger.warn('get workspace information error: ' + err);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,169 +0,0 @@
|
|||||||
import { DebugLogger } from '@affine/debug';
|
|
||||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
|
||||||
import { assertEquals } from '@blocksuite/global/utils';
|
|
||||||
import type { Workspace as BlockSuiteWorkspace } from '@blocksuite/store';
|
|
||||||
import { fixWorkspaceVersion } from '@toeverything/infra/blocksuite';
|
|
||||||
import { applyUpdate, encodeStateAsUpdate } from 'yjs';
|
|
||||||
|
|
||||||
import type { WorkspaceFactory } from './factory';
|
|
||||||
import type { BlobStorage } from './index';
|
|
||||||
import type { WorkspaceList } from './list';
|
|
||||||
import type { WorkspaceMetadata } from './metadata';
|
|
||||||
import { WorkspacePool } from './pool';
|
|
||||||
import type { Workspace } from './workspace';
|
|
||||||
|
|
||||||
const logger = new DebugLogger('affine:workspace-manager');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* # `WorkspaceManager`
|
|
||||||
*
|
|
||||||
* This class acts as the central hub for managing various aspects of workspaces.
|
|
||||||
* It is structured as follows:
|
|
||||||
*
|
|
||||||
* ```
|
|
||||||
* ┌───────────┐
|
|
||||||
* │ Workspace │
|
|
||||||
* │ Manager │
|
|
||||||
* └─────┬─────┘
|
|
||||||
* ┌─────────────┼─────────────┐
|
|
||||||
* ┌───┴───┐ ┌───┴───┐ ┌─────┴─────┐
|
|
||||||
* │ List │ │ Pool │ │ Factories │
|
|
||||||
* └───────┘ └───────┘ └───────────┘
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* Manage every about workspace
|
|
||||||
*
|
|
||||||
* # List
|
|
||||||
*
|
|
||||||
* The `WorkspaceList` component stores metadata for all workspaces, also include workspace avatar and custom name.
|
|
||||||
*
|
|
||||||
* # Factories
|
|
||||||
*
|
|
||||||
* This class contains a collection of `WorkspaceFactory`,
|
|
||||||
* We utilize `metadata.flavour` to identify the appropriate factory for opening a workspace.
|
|
||||||
* Once opened, workspaces are stored in the `WorkspacePool`.
|
|
||||||
*
|
|
||||||
* # Pool
|
|
||||||
*
|
|
||||||
* The `WorkspacePool` use reference counting to manage active workspaces.
|
|
||||||
* Calling `use()` to create a reference to the workspace. Calling `release()` to release the reference.
|
|
||||||
* When the reference count is 0, it will close the workspace.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
export class WorkspaceManager {
|
|
||||||
pool: WorkspacePool = new WorkspacePool();
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
public list: WorkspaceList,
|
|
||||||
public factories: WorkspaceFactory[]
|
|
||||||
) {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* get workspace reference by metadata.
|
|
||||||
*
|
|
||||||
* You basically don't need to call this function directly, use the react hook `useWorkspace(metadata)` instead.
|
|
||||||
*
|
|
||||||
* @returns the workspace reference and a release function, don't forget to call release function when you don't
|
|
||||||
* need the workspace anymore.
|
|
||||||
*/
|
|
||||||
use(metadata: WorkspaceMetadata): {
|
|
||||||
workspace: Workspace;
|
|
||||||
release: () => void;
|
|
||||||
} {
|
|
||||||
const exist = this.pool.get(metadata.id);
|
|
||||||
if (exist) {
|
|
||||||
return exist;
|
|
||||||
}
|
|
||||||
|
|
||||||
const workspace = this.open(metadata);
|
|
||||||
const ref = this.pool.put(workspace);
|
|
||||||
|
|
||||||
return ref;
|
|
||||||
}
|
|
||||||
|
|
||||||
createWorkspace(
|
|
||||||
flavour: WorkspaceFlavour,
|
|
||||||
initial: (
|
|
||||||
workspace: BlockSuiteWorkspace,
|
|
||||||
blobStorage: BlobStorage
|
|
||||||
) => Promise<void>
|
|
||||||
): Promise<string> {
|
|
||||||
logger.info(`create workspace [${flavour}]`);
|
|
||||||
return this.list.create(flavour, initial);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* delete workspace by metadata, same as `WorkspaceList.deleteWorkspace`
|
|
||||||
*/
|
|
||||||
async deleteWorkspace(metadata: WorkspaceMetadata) {
|
|
||||||
await this.list.delete(metadata);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* helper function to transform local workspace to cloud workspace
|
|
||||||
*/
|
|
||||||
async transformLocalToCloud(local: Workspace): Promise<WorkspaceMetadata> {
|
|
||||||
assertEquals(local.flavour, WorkspaceFlavour.LOCAL);
|
|
||||||
|
|
||||||
await local.engine.sync.waitForSynced();
|
|
||||||
|
|
||||||
const newId = await this.list.create(
|
|
||||||
WorkspaceFlavour.AFFINE_CLOUD,
|
|
||||||
async (ws, bs) => {
|
|
||||||
applyUpdate(ws.doc, encodeStateAsUpdate(local.blockSuiteWorkspace.doc));
|
|
||||||
|
|
||||||
for (const subdoc of local.blockSuiteWorkspace.doc.getSubdocs()) {
|
|
||||||
for (const newSubdoc of ws.doc.getSubdocs()) {
|
|
||||||
if (newSubdoc.guid === subdoc.guid) {
|
|
||||||
applyUpdate(newSubdoc, encodeStateAsUpdate(subdoc));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const blobList = await local.engine.blob.list();
|
|
||||||
|
|
||||||
for (const blobKey of blobList) {
|
|
||||||
const blob = await local.engine.blob.get(blobKey);
|
|
||||||
if (blob) {
|
|
||||||
await bs.set(blobKey, blob);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
await this.list.delete(local.meta);
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: newId,
|
|
||||||
flavour: WorkspaceFlavour.AFFINE_CLOUD,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* helper function to get blob without open workspace, its be used for download workspace avatars.
|
|
||||||
*/
|
|
||||||
getWorkspaceBlob(metadata: WorkspaceMetadata, blobKey: string) {
|
|
||||||
const factory = this.factories.find(x => x.name === metadata.flavour);
|
|
||||||
if (!factory) {
|
|
||||||
throw new Error(`Unknown workspace flavour: ${metadata.flavour}`);
|
|
||||||
}
|
|
||||||
return factory.getWorkspaceBlob(metadata.id, blobKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
private open(metadata: WorkspaceMetadata) {
|
|
||||||
logger.info(`open workspace [${metadata.flavour}] ${metadata.id} `);
|
|
||||||
const factory = this.factories.find(x => x.name === metadata.flavour);
|
|
||||||
if (!factory) {
|
|
||||||
throw new Error(`Unknown workspace flavour: ${metadata.flavour}`);
|
|
||||||
}
|
|
||||||
const workspace = factory.openWorkspace(metadata);
|
|
||||||
|
|
||||||
// sync information with workspace list, when workspace's avatar and name changed, information will be updated
|
|
||||||
this.list.getInformation(metadata).syncWithWorkspace(workspace);
|
|
||||||
|
|
||||||
// apply compatibility fix
|
|
||||||
fixWorkspaceVersion(workspace.blockSuiteWorkspace.doc);
|
|
||||||
|
|
||||||
return workspace;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,3 +0,0 @@
|
|||||||
import type { WorkspaceFlavour } from '@affine/env/workspace';
|
|
||||||
|
|
||||||
export type WorkspaceMetadata = { id: string; flavour: WorkspaceFlavour };
|
|
@ -1,86 +0,0 @@
|
|||||||
import { DebugLogger } from '@affine/debug';
|
|
||||||
import { Unreachable } from '@affine/env/constant';
|
|
||||||
|
|
||||||
import type { Workspace } from './workspace';
|
|
||||||
|
|
||||||
const logger = new DebugLogger('affine:workspace-manager:pool');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Collection of opened workspaces. use reference counting to manage active workspaces.
|
|
||||||
*/
|
|
||||||
export class WorkspacePool {
|
|
||||||
openedWorkspaces = new Map<string, { workspace: Workspace; rc: number }>();
|
|
||||||
timeoutToGc: NodeJS.Timeout | null = null;
|
|
||||||
|
|
||||||
get(workspaceId: string): {
|
|
||||||
workspace: Workspace;
|
|
||||||
release: () => void;
|
|
||||||
} | null {
|
|
||||||
const exist = this.openedWorkspaces.get(workspaceId);
|
|
||||||
if (exist) {
|
|
||||||
exist.rc++;
|
|
||||||
let released = false;
|
|
||||||
return {
|
|
||||||
workspace: exist.workspace,
|
|
||||||
release: () => {
|
|
||||||
// avoid double release
|
|
||||||
if (released) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
released = true;
|
|
||||||
exist.rc--;
|
|
||||||
this.requestGc();
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
put(workspace: Workspace) {
|
|
||||||
const ref = { workspace, rc: 0 };
|
|
||||||
this.openedWorkspaces.set(workspace.meta.id, ref);
|
|
||||||
|
|
||||||
const r = this.get(workspace.meta.id);
|
|
||||||
if (!r) {
|
|
||||||
throw new Unreachable();
|
|
||||||
}
|
|
||||||
|
|
||||||
return r;
|
|
||||||
}
|
|
||||||
|
|
||||||
private requestGc() {
|
|
||||||
if (this.timeoutToGc) {
|
|
||||||
clearInterval(this.timeoutToGc);
|
|
||||||
}
|
|
||||||
|
|
||||||
// do gc every 1s
|
|
||||||
this.timeoutToGc = setInterval(() => {
|
|
||||||
this.gc();
|
|
||||||
}, 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
private gc() {
|
|
||||||
for (const [id, { workspace, rc }] of new Map(
|
|
||||||
this.openedWorkspaces /* clone the map, because the origin will be modified during iteration */
|
|
||||||
)) {
|
|
||||||
if (rc === 0 && workspace.canGracefulStop()) {
|
|
||||||
// we can safely close the workspace
|
|
||||||
logger.info(`close workspace [${workspace.flavour}] ${workspace.id}`);
|
|
||||||
workspace.forceStop();
|
|
||||||
|
|
||||||
this.openedWorkspaces.delete(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const [_, { rc }] of this.openedWorkspaces) {
|
|
||||||
if (rc === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// if all workspaces has referrer, stop gc
|
|
||||||
if (this.timeoutToGc) {
|
|
||||||
clearInterval(this.timeoutToGc);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,148 +0,0 @@
|
|||||||
import { Unreachable } from '@affine/env/constant';
|
|
||||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
|
||||||
import { Slot } from '@blocksuite/global/utils';
|
|
||||||
import {
|
|
||||||
checkWorkspaceCompatibility,
|
|
||||||
MigrationPoint,
|
|
||||||
} from '@toeverything/infra/blocksuite';
|
|
||||||
import {
|
|
||||||
forceUpgradePages,
|
|
||||||
upgradeV1ToV2,
|
|
||||||
} from '@toeverything/infra/blocksuite';
|
|
||||||
import { migrateGuidCompatibility } from '@toeverything/infra/blocksuite';
|
|
||||||
import { applyUpdate, Doc as YDoc, encodeStateAsUpdate } from 'yjs';
|
|
||||||
|
|
||||||
import type { WorkspaceManager } from '..';
|
|
||||||
import type { Workspace } from '../workspace';
|
|
||||||
|
|
||||||
export interface WorkspaceUpgradeStatus {
|
|
||||||
needUpgrade: boolean;
|
|
||||||
upgrading: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class WorkspaceUpgradeController {
|
|
||||||
_status: Readonly<WorkspaceUpgradeStatus> = {
|
|
||||||
needUpgrade: false,
|
|
||||||
upgrading: false,
|
|
||||||
};
|
|
||||||
readonly onStatusChange = new Slot<WorkspaceUpgradeStatus>();
|
|
||||||
|
|
||||||
get status() {
|
|
||||||
return this._status;
|
|
||||||
}
|
|
||||||
|
|
||||||
set status(value) {
|
|
||||||
if (
|
|
||||||
value.needUpgrade !== this._status.needUpgrade ||
|
|
||||||
value.upgrading !== this._status.upgrading
|
|
||||||
) {
|
|
||||||
this._status = value;
|
|
||||||
this.onStatusChange.emit(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(private readonly workspace: Workspace) {
|
|
||||||
workspace.blockSuiteWorkspace.doc.on('update', () => {
|
|
||||||
this.checkIfNeedUpgrade();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
checkIfNeedUpgrade() {
|
|
||||||
const needUpgrade = !!checkWorkspaceCompatibility(
|
|
||||||
this.workspace.blockSuiteWorkspace,
|
|
||||||
this.workspace.flavour === WorkspaceFlavour.AFFINE_CLOUD
|
|
||||||
);
|
|
||||||
this.status = {
|
|
||||||
...this.status,
|
|
||||||
needUpgrade,
|
|
||||||
};
|
|
||||||
return needUpgrade;
|
|
||||||
}
|
|
||||||
|
|
||||||
async upgrade(workspaceManager: WorkspaceManager): Promise<string | null> {
|
|
||||||
if (this.status.upgrading) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.status = { ...this.status, upgrading: true };
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.workspace.engine.sync.waitForSynced();
|
|
||||||
|
|
||||||
const step = checkWorkspaceCompatibility(
|
|
||||||
this.workspace.blockSuiteWorkspace,
|
|
||||||
this.workspace.flavour === WorkspaceFlavour.AFFINE_CLOUD
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!step) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clone a new doc to prevent change events.
|
|
||||||
const clonedDoc = new YDoc({
|
|
||||||
guid: this.workspace.blockSuiteWorkspace.doc.guid,
|
|
||||||
});
|
|
||||||
applyDoc(clonedDoc, this.workspace.blockSuiteWorkspace.doc);
|
|
||||||
|
|
||||||
if (step === MigrationPoint.SubDoc) {
|
|
||||||
const newWorkspace = await workspaceManager.createWorkspace(
|
|
||||||
WorkspaceFlavour.LOCAL,
|
|
||||||
async (workspace, blobStorage) => {
|
|
||||||
await upgradeV1ToV2(clonedDoc, workspace.doc);
|
|
||||||
migrateGuidCompatibility(clonedDoc);
|
|
||||||
await forceUpgradePages(
|
|
||||||
workspace.doc,
|
|
||||||
this.workspace.blockSuiteWorkspace.schema
|
|
||||||
);
|
|
||||||
const blobList =
|
|
||||||
await this.workspace.blockSuiteWorkspace.blob.list();
|
|
||||||
|
|
||||||
for (const blobKey of blobList) {
|
|
||||||
const blob =
|
|
||||||
await this.workspace.blockSuiteWorkspace.blob.get(blobKey);
|
|
||||||
if (blob) {
|
|
||||||
await blobStorage.set(blobKey, blob);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
await workspaceManager.deleteWorkspace(this.workspace.meta);
|
|
||||||
return newWorkspace;
|
|
||||||
} else if (step === MigrationPoint.GuidFix) {
|
|
||||||
migrateGuidCompatibility(clonedDoc);
|
|
||||||
await forceUpgradePages(
|
|
||||||
clonedDoc,
|
|
||||||
this.workspace.blockSuiteWorkspace.schema
|
|
||||||
);
|
|
||||||
applyDoc(this.workspace.blockSuiteWorkspace.doc, clonedDoc);
|
|
||||||
await this.workspace.engine.sync.waitForSynced();
|
|
||||||
return null;
|
|
||||||
} else if (step === MigrationPoint.BlockVersion) {
|
|
||||||
await forceUpgradePages(
|
|
||||||
clonedDoc,
|
|
||||||
this.workspace.blockSuiteWorkspace.schema
|
|
||||||
);
|
|
||||||
applyDoc(this.workspace.blockSuiteWorkspace.doc, clonedDoc);
|
|
||||||
await this.workspace.engine.sync.waitForSynced();
|
|
||||||
return null;
|
|
||||||
} else {
|
|
||||||
throw new Unreachable();
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
this.status = { ...this.status, upgrading: false };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyDoc(target: YDoc, result: YDoc) {
|
|
||||||
applyUpdate(target, encodeStateAsUpdate(result));
|
|
||||||
for (const targetSubDoc of target.subdocs.values()) {
|
|
||||||
const resultSubDocs = Array.from(result.subdocs.values());
|
|
||||||
const resultSubDoc = resultSubDocs.find(
|
|
||||||
item => item.guid === targetSubDoc.guid
|
|
||||||
);
|
|
||||||
if (resultSubDoc) {
|
|
||||||
applyDoc(targetSubDoc, resultSubDoc);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,45 +0,0 @@
|
|||||||
import { describe, expect, test, vi } from 'vitest';
|
|
||||||
|
|
||||||
import { AsyncQueue } from '../async-queue';
|
|
||||||
|
|
||||||
describe('async-queue', () => {
|
|
||||||
test('push & pop', async () => {
|
|
||||||
const queue = new AsyncQueue();
|
|
||||||
queue.push(1, 2, 3);
|
|
||||||
expect(queue.length).toBe(3);
|
|
||||||
expect(await queue.next()).toBe(1);
|
|
||||||
expect(await queue.next()).toBe(2);
|
|
||||||
expect(await queue.next()).toBe(3);
|
|
||||||
expect(queue.length).toBe(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('await', async () => {
|
|
||||||
const queue = new AsyncQueue<number>();
|
|
||||||
queue.push(1, 2);
|
|
||||||
expect(await queue.next()).toBe(1);
|
|
||||||
expect(await queue.next()).toBe(2);
|
|
||||||
|
|
||||||
let v = -1;
|
|
||||||
|
|
||||||
// setup 2 pop tasks
|
|
||||||
queue.next().then(next => {
|
|
||||||
v = next;
|
|
||||||
});
|
|
||||||
queue.next().then(next => {
|
|
||||||
v = next;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Wait for 100ms
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 100));
|
|
||||||
// v should not be changed
|
|
||||||
expect(v).toBe(-1);
|
|
||||||
|
|
||||||
// push 3, should trigger the first pop task
|
|
||||||
queue.push(3);
|
|
||||||
await vi.waitFor(() => v === 3);
|
|
||||||
|
|
||||||
// push 4, should trigger the second pop task
|
|
||||||
queue.push(4);
|
|
||||||
await vi.waitFor(() => v === 4);
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,13 +0,0 @@
|
|||||||
import { describe, expect, test } from 'vitest';
|
|
||||||
|
|
||||||
import { throwIfAborted } from '../throw-if-aborted';
|
|
||||||
|
|
||||||
describe('throw-if-aborted', () => {
|
|
||||||
test('basic', async () => {
|
|
||||||
const abortController = new AbortController();
|
|
||||||
const abortSignal = abortController.signal;
|
|
||||||
expect(throwIfAborted(abortSignal)).toBe(true);
|
|
||||||
abortController.abort('TEST_ABORT');
|
|
||||||
expect(() => throwIfAborted(abortSignal)).toThrowError('TEST_ABORT');
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,101 +0,0 @@
|
|||||||
export class AsyncQueue<T> {
|
|
||||||
private _queue: T[];
|
|
||||||
|
|
||||||
private _resolveUpdate: (() => void) | null = null;
|
|
||||||
private _waitForUpdate: Promise<void> | null = null;
|
|
||||||
|
|
||||||
constructor(init: T[] = []) {
|
|
||||||
this._queue = init;
|
|
||||||
}
|
|
||||||
|
|
||||||
get length() {
|
|
||||||
return this._queue.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
async next(
|
|
||||||
abort?: AbortSignal,
|
|
||||||
dequeue: (arr: T[]) => T | undefined = a => a.shift()
|
|
||||||
): Promise<T> {
|
|
||||||
const update = dequeue(this._queue);
|
|
||||||
if (update) {
|
|
||||||
return update;
|
|
||||||
} else {
|
|
||||||
if (!this._waitForUpdate) {
|
|
||||||
this._waitForUpdate = new Promise(resolve => {
|
|
||||||
this._resolveUpdate = resolve;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await Promise.race([
|
|
||||||
this._waitForUpdate,
|
|
||||||
new Promise((_, reject) => {
|
|
||||||
if (abort?.aborted) {
|
|
||||||
reject(abort?.reason);
|
|
||||||
}
|
|
||||||
abort?.addEventListener('abort', () => {
|
|
||||||
reject(abort.reason);
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return this.next(abort, dequeue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
push(...updates: T[]) {
|
|
||||||
this._queue.push(...updates);
|
|
||||||
if (this._resolveUpdate) {
|
|
||||||
const resolve = this._resolveUpdate;
|
|
||||||
this._resolveUpdate = null;
|
|
||||||
this._waitForUpdate = null;
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
remove(predicate: (update: T) => boolean) {
|
|
||||||
const index = this._queue.findIndex(predicate);
|
|
||||||
if (index !== -1) {
|
|
||||||
this._queue.splice(index, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
find(predicate: (update: T) => boolean) {
|
|
||||||
return this._queue.find(predicate);
|
|
||||||
}
|
|
||||||
|
|
||||||
clear() {
|
|
||||||
this._queue = [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class PriorityAsyncQueue<
|
|
||||||
T extends { id: string },
|
|
||||||
> extends AsyncQueue<T> {
|
|
||||||
constructor(
|
|
||||||
init: T[] = [],
|
|
||||||
public readonly priorityTarget: SharedPriorityTarget = new SharedPriorityTarget()
|
|
||||||
) {
|
|
||||||
super(init);
|
|
||||||
}
|
|
||||||
|
|
||||||
override next(abort?: AbortSignal | undefined): Promise<T> {
|
|
||||||
return super.next(abort, arr => {
|
|
||||||
if (this.priorityTarget.priorityRule !== null) {
|
|
||||||
const index = arr.findIndex(
|
|
||||||
update => this.priorityTarget.priorityRule?.(update.id)
|
|
||||||
);
|
|
||||||
if (index !== -1) {
|
|
||||||
return arr.splice(index, 1)[0];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return arr.shift();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shared priority target can be shared by multiple queues.
|
|
||||||
*/
|
|
||||||
export class SharedPriorityTarget {
|
|
||||||
public priorityRule: ((id: string) => boolean) | null = null;
|
|
||||||
}
|
|
@ -1,17 +0,0 @@
|
|||||||
import { applyUpdate, Doc, encodeStateAsUpdate } from 'yjs';
|
|
||||||
|
|
||||||
export function mergeUpdates(updates: Uint8Array[]) {
|
|
||||||
if (updates.length === 0) {
|
|
||||||
return new Uint8Array();
|
|
||||||
}
|
|
||||||
if (updates.length === 1) {
|
|
||||||
return updates[0];
|
|
||||||
}
|
|
||||||
const doc = new Doc();
|
|
||||||
doc.transact(() => {
|
|
||||||
updates.forEach(update => {
|
|
||||||
applyUpdate(doc, update);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return encodeStateAsUpdate(doc);
|
|
||||||
}
|
|
@ -1,9 +0,0 @@
|
|||||||
// because AbortSignal.throwIfAborted is not available in abortcontroller-polyfill
|
|
||||||
export function throwIfAborted(abort?: AbortSignal) {
|
|
||||||
if (abort?.aborted) {
|
|
||||||
throw new Error(abort.reason);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const MANUALLY_STOP = 'manually-stop';
|
|
@ -1,137 +0,0 @@
|
|||||||
import { DebugLogger } from '@affine/debug';
|
|
||||||
import { Slot } from '@blocksuite/global/utils';
|
|
||||||
import type { Workspace as BlockSuiteWorkspace } from '@blocksuite/store';
|
|
||||||
|
|
||||||
import type { WorkspaceEngine, WorkspaceEngineStatus } from './engine';
|
|
||||||
import type { WorkspaceMetadata } from './metadata';
|
|
||||||
import {
|
|
||||||
WorkspaceUpgradeController,
|
|
||||||
type WorkspaceUpgradeStatus,
|
|
||||||
} from './upgrade';
|
|
||||||
|
|
||||||
const logger = new DebugLogger('affine:workspace');
|
|
||||||
|
|
||||||
export type WorkspaceStatus = {
|
|
||||||
mode: 'ready' | 'closed';
|
|
||||||
engine: WorkspaceEngineStatus;
|
|
||||||
upgrade: WorkspaceUpgradeStatus;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* # Workspace
|
|
||||||
*
|
|
||||||
* ```
|
|
||||||
* ┌───────────┐
|
|
||||||
* │ Workspace │
|
|
||||||
* └─────┬─────┘
|
|
||||||
* │
|
|
||||||
* │
|
|
||||||
* ┌──────────────┼─────────────┐
|
|
||||||
* │ │ │
|
|
||||||
* ┌───┴─────┐ ┌──────┴─────┐ ┌───┴────┐
|
|
||||||
* │ Upgrade │ │ blocksuite │ │ Engine │
|
|
||||||
* └─────────┘ └────────────┘ └───┬────┘
|
|
||||||
* │
|
|
||||||
* ┌──────┼─────────┐
|
|
||||||
* │ │ │
|
|
||||||
* ┌──┴─┐ ┌──┴─┐ ┌─────┴───┐
|
|
||||||
* │sync│ │blob│ │awareness│
|
|
||||||
* └────┘ └────┘ └─────────┘
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* This class contains all the components needed to run a workspace.
|
|
||||||
*/
|
|
||||||
export class Workspace {
|
|
||||||
get id() {
|
|
||||||
return this.meta.id;
|
|
||||||
}
|
|
||||||
get flavour() {
|
|
||||||
return this.meta.flavour;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _status: WorkspaceStatus;
|
|
||||||
|
|
||||||
upgrade: WorkspaceUpgradeController;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* event on workspace stop, workspace is one-time use, so it will be triggered only once
|
|
||||||
*/
|
|
||||||
onStop = new Slot();
|
|
||||||
|
|
||||||
onStatusChange = new Slot<WorkspaceStatus>();
|
|
||||||
get status() {
|
|
||||||
return this._status;
|
|
||||||
}
|
|
||||||
|
|
||||||
set status(status: WorkspaceStatus) {
|
|
||||||
this._status = status;
|
|
||||||
this.onStatusChange.emit(status);
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
public meta: WorkspaceMetadata,
|
|
||||||
public engine: WorkspaceEngine,
|
|
||||||
public blockSuiteWorkspace: BlockSuiteWorkspace
|
|
||||||
) {
|
|
||||||
this.upgrade = new WorkspaceUpgradeController(this);
|
|
||||||
|
|
||||||
this._status = {
|
|
||||||
mode: 'closed',
|
|
||||||
engine: engine.status,
|
|
||||||
upgrade: this.upgrade.status,
|
|
||||||
};
|
|
||||||
this.engine.onStatusChange.on(status => {
|
|
||||||
this.status = {
|
|
||||||
...this.status,
|
|
||||||
engine: status,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
this.upgrade.onStatusChange.on(status => {
|
|
||||||
this.status = {
|
|
||||||
...this.status,
|
|
||||||
upgrade: status,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
this.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* workspace start when create and workspace is one-time use
|
|
||||||
*/
|
|
||||||
private start() {
|
|
||||||
if (this.status.mode === 'ready') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
logger.info('start workspace', this.id);
|
|
||||||
this.engine.start();
|
|
||||||
this.status = {
|
|
||||||
...this.status,
|
|
||||||
mode: 'ready',
|
|
||||||
engine: this.engine.status,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
canGracefulStop() {
|
|
||||||
return this.engine.canGracefulStop() && !this.status.upgrade.upgrading;
|
|
||||||
}
|
|
||||||
|
|
||||||
forceStop() {
|
|
||||||
if (this.status.mode === 'closed') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
logger.info('stop workspace', this.id);
|
|
||||||
this.engine.forceStop();
|
|
||||||
this.status = {
|
|
||||||
...this.status,
|
|
||||||
mode: 'closed',
|
|
||||||
engine: this.engine.status,
|
|
||||||
};
|
|
||||||
this.onStop.emit();
|
|
||||||
}
|
|
||||||
|
|
||||||
// same as `WorkspaceEngine.sync.setPriorityRule`
|
|
||||||
setPriorityRule(target: ((id: string) => boolean) | null) {
|
|
||||||
this.engine.sync.setPriorityRule(target);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,16 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "../../../tsconfig.json",
|
|
||||||
"include": ["./src"],
|
|
||||||
"compilerOptions": {
|
|
||||||
"noEmit": false,
|
|
||||||
"outDir": "lib"
|
|
||||||
},
|
|
||||||
"references": [
|
|
||||||
{ "path": "../../../tests/fixtures" },
|
|
||||||
{ "path": "../../common/env" },
|
|
||||||
{ "path": "../../common/debug" },
|
|
||||||
{ "path": "../../common/infra" },
|
|
||||||
{ "path": "../../frontend/graphql" },
|
|
||||||
{ "path": "../../frontend/electron-api" }
|
|
||||||
]
|
|
||||||
}
|
|
@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": ["../../../typedoc.base.json"],
|
|
||||||
"entryPoints": ["src/index.ts"]
|
|
||||||
}
|
|
@ -24,7 +24,6 @@
|
|||||||
"@affine/electron-api": "workspace:*",
|
"@affine/electron-api": "workspace:*",
|
||||||
"@affine/graphql": "workspace:*",
|
"@affine/graphql": "workspace:*",
|
||||||
"@affine/i18n": "workspace:*",
|
"@affine/i18n": "workspace:*",
|
||||||
"@affine/workspace": "workspace:*",
|
|
||||||
"@dnd-kit/core": "^6.0.8",
|
"@dnd-kit/core": "^6.0.8",
|
||||||
"@dnd-kit/modifiers": "^7.0.0",
|
"@dnd-kit/modifiers": "^7.0.0",
|
||||||
"@dnd-kit/sortable": "^8.0.0",
|
"@dnd-kit/sortable": "^8.0.0",
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { UNTITLED_WORKSPACE_NAME } from '@affine/env/constant';
|
import { UNTITLED_WORKSPACE_NAME } from '@affine/env/constant';
|
||||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import type { WorkspaceMetadata } from '@affine/workspace';
|
|
||||||
import { CollaborationIcon, SettingsIcon } from '@blocksuite/icons';
|
import { CollaborationIcon, SettingsIcon } from '@blocksuite/icons';
|
||||||
|
import type { WorkspaceMetadata } from '@toeverything/infra';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
import { Avatar } from '../../../ui/avatar';
|
import { Avatar } from '../../../ui/avatar';
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import type { WorkspaceMetadata } from '@affine/workspace';
|
|
||||||
import type { DragEndEvent } from '@dnd-kit/core';
|
import type { DragEndEvent } from '@dnd-kit/core';
|
||||||
import {
|
import {
|
||||||
DndContext,
|
DndContext,
|
||||||
@ -11,6 +10,7 @@ import {
|
|||||||
restrictToVerticalAxis,
|
restrictToVerticalAxis,
|
||||||
} from '@dnd-kit/modifiers';
|
} from '@dnd-kit/modifiers';
|
||||||
import { arrayMove, SortableContext, useSortable } from '@dnd-kit/sortable';
|
import { arrayMove, SortableContext, useSortable } from '@dnd-kit/sortable';
|
||||||
|
import type { WorkspaceMetadata } from '@toeverything/infra';
|
||||||
import type { CSSProperties } from 'react';
|
import type { CSSProperties } from 'react';
|
||||||
import { Suspense, useCallback, useEffect, useMemo, useState } from 'react';
|
import { Suspense, useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
|
@ -14,7 +14,9 @@
|
|||||||
{
|
{
|
||||||
"path": "../../frontend/electron-api"
|
"path": "../../frontend/electron-api"
|
||||||
},
|
},
|
||||||
{ "path": "../../common/workspace" },
|
{
|
||||||
|
"path": "../../frontend/graphql"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "../../common/debug"
|
"path": "../../common/debug"
|
||||||
},
|
},
|
||||||
|
@ -24,7 +24,6 @@
|
|||||||
"@affine/graphql": "workspace:*",
|
"@affine/graphql": "workspace:*",
|
||||||
"@affine/i18n": "workspace:*",
|
"@affine/i18n": "workspace:*",
|
||||||
"@affine/templates": "workspace:*",
|
"@affine/templates": "workspace:*",
|
||||||
"@affine/workspace": "workspace:*",
|
|
||||||
"@affine/workspace-impl": "workspace:*",
|
"@affine/workspace-impl": "workspace:*",
|
||||||
"@blocksuite/block-std": "0.12.0-nightly-202401290223-b6302df",
|
"@blocksuite/block-std": "0.12.0-nightly-202401290223-b6302df",
|
||||||
"@blocksuite/blocks": "0.12.0-nightly-202401290223-b6302df",
|
"@blocksuite/blocks": "0.12.0-nightly-202401290223-b6302df",
|
||||||
|
@ -8,14 +8,17 @@ import { WorkspaceFallback } from '@affine/component/workspace';
|
|||||||
import { createI18n, setUpLanguage } from '@affine/i18n';
|
import { createI18n, setUpLanguage } from '@affine/i18n';
|
||||||
import { CacheProvider } from '@emotion/react';
|
import { CacheProvider } from '@emotion/react';
|
||||||
import { getCurrentStore } from '@toeverything/infra/atom';
|
import { getCurrentStore } from '@toeverything/infra/atom';
|
||||||
|
import { ServiceCollection } from '@toeverything/infra/di';
|
||||||
import type { PropsWithChildren, ReactElement } from 'react';
|
import type { PropsWithChildren, ReactElement } from 'react';
|
||||||
import { lazy, memo, Suspense } from 'react';
|
import { lazy, memo, Suspense } from 'react';
|
||||||
import { RouterProvider } from 'react-router-dom';
|
import { RouterProvider } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { GlobalScopeProvider } from './modules/infra-web/global-scope';
|
||||||
import { CloudSessionProvider } from './providers/session-provider';
|
import { CloudSessionProvider } from './providers/session-provider';
|
||||||
import { router } from './router';
|
import { router } from './router';
|
||||||
import { performanceLogger, performanceRenderLogger } from './shared';
|
import { performanceLogger, performanceRenderLogger } from './shared';
|
||||||
import createEmotionCache from './utils/create-emotion-cache';
|
import createEmotionCache from './utils/create-emotion-cache';
|
||||||
|
import { configureWebServices } from './web';
|
||||||
|
|
||||||
const performanceI18nLogger = performanceLogger.namespace('i18n');
|
const performanceI18nLogger = performanceLogger.namespace('i18n');
|
||||||
const cache = createEmotionCache();
|
const cache = createEmotionCache();
|
||||||
@ -52,6 +55,10 @@ async function loadLanguage() {
|
|||||||
|
|
||||||
let languageLoadingPromise: Promise<void> | null = null;
|
let languageLoadingPromise: Promise<void> | null = null;
|
||||||
|
|
||||||
|
const services = new ServiceCollection();
|
||||||
|
configureWebServices(services);
|
||||||
|
const serviceProvider = services.provider();
|
||||||
|
|
||||||
export const App = memo(function App() {
|
export const App = memo(function App() {
|
||||||
performanceRenderLogger.info('App');
|
performanceRenderLogger.info('App');
|
||||||
|
|
||||||
@ -60,20 +67,26 @@ export const App = memo(function App() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CacheProvider value={cache}>
|
<Suspense>
|
||||||
<AffineContext store={getCurrentStore()}>
|
<GlobalScopeProvider provider={serviceProvider}>
|
||||||
<CloudSessionProvider>
|
<CacheProvider value={cache}>
|
||||||
<DebugProvider>
|
<AffineContext store={getCurrentStore()}>
|
||||||
<GlobalLoading />
|
<CloudSessionProvider>
|
||||||
{runtimeConfig.enableNotificationCenter && <NotificationCenter />}
|
<DebugProvider>
|
||||||
<RouterProvider
|
<GlobalLoading />
|
||||||
fallbackElement={<WorkspaceFallback key="RouterFallback" />}
|
{runtimeConfig.enableNotificationCenter && (
|
||||||
router={router}
|
<NotificationCenter />
|
||||||
future={future}
|
)}
|
||||||
/>
|
<RouterProvider
|
||||||
</DebugProvider>
|
fallbackElement={<WorkspaceFallback key="RouterFallback" />}
|
||||||
</CloudSessionProvider>
|
router={router}
|
||||||
</AffineContext>
|
future={future}
|
||||||
</CacheProvider>
|
/>
|
||||||
|
</DebugProvider>
|
||||||
|
</CloudSessionProvider>
|
||||||
|
</AffineContext>
|
||||||
|
</CacheProvider>
|
||||||
|
</GlobalScopeProvider>
|
||||||
|
</Suspense>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -1,231 +0,0 @@
|
|||||||
import {
|
|
||||||
currentWorkspaceAtom,
|
|
||||||
waitForCurrentWorkspaceAtom,
|
|
||||||
} from '@affine/core/modules/workspace';
|
|
||||||
import type { Collection, DeprecatedCollection } from '@affine/env/filter';
|
|
||||||
import { DisposableGroup } from '@blocksuite/global/utils';
|
|
||||||
import type { Workspace } from '@blocksuite/store';
|
|
||||||
import { type DBSchema, openDB } from 'idb';
|
|
||||||
import { atom } from 'jotai';
|
|
||||||
import { atomWithObservable } from 'jotai/utils';
|
|
||||||
import { Observable, of } from 'rxjs';
|
|
||||||
|
|
||||||
import type {
|
|
||||||
CollectionsCRUD,
|
|
||||||
CollectionsCRUDAtom,
|
|
||||||
} from '../components/page-list';
|
|
||||||
import { getUserSetting } from '../utils/user-setting';
|
|
||||||
import { getWorkspaceSetting } from '../utils/workspace-setting';
|
|
||||||
import { sessionAtom } from './cloud-user';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated
|
|
||||||
*/
|
|
||||||
export interface PageCollectionDBV1 extends DBSchema {
|
|
||||||
view: {
|
|
||||||
key: DeprecatedCollection['id'];
|
|
||||||
value: DeprecatedCollection;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated
|
|
||||||
*/
|
|
||||||
export interface StorageCRUD<Value> {
|
|
||||||
get: (key: string) => Promise<Value | null>;
|
|
||||||
set: (key: string, value: Value) => Promise<string>;
|
|
||||||
delete: (key: string) => Promise<void>;
|
|
||||||
list: () => Promise<string[]>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated
|
|
||||||
*/
|
|
||||||
const collectionDBAtom = atom(
|
|
||||||
openDB<PageCollectionDBV1>('page-view', 1, {
|
|
||||||
upgrade(database) {
|
|
||||||
database.createObjectStore('view', {
|
|
||||||
keyPath: 'id',
|
|
||||||
});
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
/**
|
|
||||||
* @deprecated
|
|
||||||
*/
|
|
||||||
const localCollectionCRUDAtom = atom(get => ({
|
|
||||||
get: async (key: string) => {
|
|
||||||
const db = await get(collectionDBAtom);
|
|
||||||
const t = db.transaction('view').objectStore('view');
|
|
||||||
return (await t.get(key)) ?? null;
|
|
||||||
},
|
|
||||||
set: async (key: string, value: DeprecatedCollection) => {
|
|
||||||
const db = await get(collectionDBAtom);
|
|
||||||
const t = db.transaction('view', 'readwrite').objectStore('view');
|
|
||||||
await t.put(value);
|
|
||||||
return key;
|
|
||||||
},
|
|
||||||
delete: async (key: string) => {
|
|
||||||
const db = await get(collectionDBAtom);
|
|
||||||
const t = db.transaction('view', 'readwrite').objectStore('view');
|
|
||||||
await t.delete(key);
|
|
||||||
},
|
|
||||||
list: async () => {
|
|
||||||
const db = await get(collectionDBAtom);
|
|
||||||
const t = db.transaction('view').objectStore('view');
|
|
||||||
return t.getAllKeys();
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
/**
|
|
||||||
* @deprecated
|
|
||||||
*/
|
|
||||||
const getCollections = async (
|
|
||||||
storage: StorageCRUD<DeprecatedCollection>
|
|
||||||
): Promise<DeprecatedCollection[]> => {
|
|
||||||
return storage
|
|
||||||
.list()
|
|
||||||
.then(async keys => {
|
|
||||||
return await Promise.all(keys.map(key => storage.get(key))).then(v =>
|
|
||||||
v.filter((v): v is DeprecatedCollection => v !== null)
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error('Failed to load collections', error);
|
|
||||||
return [];
|
|
||||||
});
|
|
||||||
};
|
|
||||||
type BaseCollectionsDataType = {
|
|
||||||
loading: boolean;
|
|
||||||
collections: Collection[];
|
|
||||||
};
|
|
||||||
export const pageCollectionBaseAtom =
|
|
||||||
atomWithObservable<BaseCollectionsDataType>(
|
|
||||||
get => {
|
|
||||||
const currentWorkspace = get(currentWorkspaceAtom);
|
|
||||||
if (!currentWorkspace) {
|
|
||||||
return of({ loading: true, collections: [] });
|
|
||||||
}
|
|
||||||
|
|
||||||
const session = get(sessionAtom);
|
|
||||||
const userId = session?.data?.user.id ?? null;
|
|
||||||
const migrateCollectionsFromIdbData = async (
|
|
||||||
workspace: Workspace
|
|
||||||
): Promise<Collection[]> => {
|
|
||||||
const localCRUD = get(localCollectionCRUDAtom);
|
|
||||||
const collections = await getCollections(localCRUD);
|
|
||||||
const result = collections.filter(v => v.workspaceId === workspace.id);
|
|
||||||
Promise.all(
|
|
||||||
result.map(collection => {
|
|
||||||
return localCRUD.delete(collection.id);
|
|
||||||
})
|
|
||||||
).catch(error => {
|
|
||||||
console.error('Failed to delete collections from indexeddb', error);
|
|
||||||
});
|
|
||||||
return result.map(v => {
|
|
||||||
return {
|
|
||||||
id: v.id,
|
|
||||||
name: v.name,
|
|
||||||
filterList: v.filterList,
|
|
||||||
allowList: v.allowList ?? [],
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
const migrateCollectionsFromUserData = async (
|
|
||||||
workspace: Workspace
|
|
||||||
): Promise<Collection[]> => {
|
|
||||||
if (userId == null) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
const userSetting = getUserSetting(workspace, userId);
|
|
||||||
await userSetting.loaded;
|
|
||||||
const view = userSetting.view;
|
|
||||||
if (view) {
|
|
||||||
const collections: Omit<DeprecatedCollection, 'workspaceId'>[] = [
|
|
||||||
...view.values(),
|
|
||||||
];
|
|
||||||
//delete collections
|
|
||||||
view.clear();
|
|
||||||
return collections.map(v => {
|
|
||||||
return {
|
|
||||||
id: v.id,
|
|
||||||
name: v.name,
|
|
||||||
filterList: v.filterList,
|
|
||||||
allowList: v.allowList ?? [],
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
};
|
|
||||||
|
|
||||||
return new Observable<BaseCollectionsDataType>(subscriber => {
|
|
||||||
const group = new DisposableGroup();
|
|
||||||
const workspaceSetting = getWorkspaceSetting(
|
|
||||||
currentWorkspace.blockSuiteWorkspace
|
|
||||||
);
|
|
||||||
migrateCollectionsFromIdbData(currentWorkspace.blockSuiteWorkspace)
|
|
||||||
.then(collections => {
|
|
||||||
if (collections.length) {
|
|
||||||
workspaceSetting.addCollection(...collections);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error(error);
|
|
||||||
});
|
|
||||||
migrateCollectionsFromUserData(currentWorkspace.blockSuiteWorkspace)
|
|
||||||
.then(collections => {
|
|
||||||
if (collections.length) {
|
|
||||||
workspaceSetting.addCollection(...collections);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error(error);
|
|
||||||
});
|
|
||||||
subscriber.next({
|
|
||||||
loading: false,
|
|
||||||
collections: workspaceSetting.collections,
|
|
||||||
});
|
|
||||||
if (group.disposed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const fn = () => {
|
|
||||||
subscriber.next({
|
|
||||||
loading: false,
|
|
||||||
collections: workspaceSetting.collections,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
workspaceSetting.setting.observeDeep(fn);
|
|
||||||
group.add(() => {
|
|
||||||
workspaceSetting.setting.unobserveDeep(fn);
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
group.dispose();
|
|
||||||
};
|
|
||||||
});
|
|
||||||
},
|
|
||||||
{ initialValue: { loading: true, collections: [] } }
|
|
||||||
);
|
|
||||||
|
|
||||||
export const collectionsCRUDAtom: CollectionsCRUDAtom = atom(async get => {
|
|
||||||
const workspace = await get(waitForCurrentWorkspaceAtom);
|
|
||||||
return {
|
|
||||||
addCollection: (...collections) => {
|
|
||||||
getWorkspaceSetting(workspace.blockSuiteWorkspace).addCollection(
|
|
||||||
...collections
|
|
||||||
);
|
|
||||||
},
|
|
||||||
collections: get(pageCollectionBaseAtom).collections,
|
|
||||||
updateCollection: (id, updater) => {
|
|
||||||
getWorkspaceSetting(workspace.blockSuiteWorkspace).updateCollection(
|
|
||||||
id,
|
|
||||||
updater
|
|
||||||
);
|
|
||||||
},
|
|
||||||
deleteCollection: (info, ...ids) => {
|
|
||||||
getWorkspaceSetting(workspace.blockSuiteWorkspace).deleteCollection(
|
|
||||||
info,
|
|
||||||
...ids
|
|
||||||
);
|
|
||||||
},
|
|
||||||
} satisfies CollectionsCRUD;
|
|
||||||
});
|
|
@ -1,7 +1,7 @@
|
|||||||
import { DebugLogger } from '@affine/debug';
|
import { DebugLogger } from '@affine/debug';
|
||||||
import { DEFAULT_WORKSPACE_NAME } from '@affine/env/constant';
|
import { DEFAULT_WORKSPACE_NAME } from '@affine/env/constant';
|
||||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||||
import { workspaceManager } from '@affine/workspace-impl';
|
import type { WorkspaceManager } from '@toeverything/infra';
|
||||||
import { getCurrentStore } from '@toeverything/infra/atom';
|
import { getCurrentStore } from '@toeverything/infra/atom';
|
||||||
import {
|
import {
|
||||||
buildShowcaseWorkspace,
|
buildShowcaseWorkspace,
|
||||||
@ -12,12 +12,12 @@ import { setPageModeAtom } from '../atoms';
|
|||||||
|
|
||||||
const logger = new DebugLogger('affine:first-app-data');
|
const logger = new DebugLogger('affine:first-app-data');
|
||||||
|
|
||||||
export async function createFirstAppData() {
|
export async function createFirstAppData(workspaceManager: WorkspaceManager) {
|
||||||
if (localStorage.getItem('is-first-open') !== null) {
|
if (localStorage.getItem('is-first-open') !== null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
localStorage.setItem('is-first-open', 'false');
|
localStorage.setItem('is-first-open', 'false');
|
||||||
const workspaceId = await workspaceManager.createWorkspace(
|
const workspaceMetadata = await workspaceManager.createWorkspace(
|
||||||
WorkspaceFlavour.LOCAL,
|
WorkspaceFlavour.LOCAL,
|
||||||
async workspace => {
|
async workspace => {
|
||||||
workspace.meta.setName(DEFAULT_WORKSPACE_NAME);
|
workspace.meta.setName(DEFAULT_WORKSPACE_NAME);
|
||||||
@ -38,6 +38,6 @@ export async function createFirstAppData() {
|
|||||||
logger.debug('create first workspace');
|
logger.debug('create first workspace');
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
console.info('create first workspace', workspaceId);
|
console.info('create first workspace', workspaceMetadata);
|
||||||
return workspaceId;
|
return workspaceMetadata;
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import {
|
import { WorkspaceListService } from '@toeverything/infra';
|
||||||
currentWorkspaceAtom,
|
import { useService } from '@toeverything/infra/di';
|
||||||
workspaceListAtom,
|
import { useLiveData } from '@toeverything/infra/livedata';
|
||||||
} from '@affine/core/modules/workspace';
|
|
||||||
import { useAtomValue } from 'jotai/react';
|
import { useAtomValue } from 'jotai/react';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useLocation, useParams } from 'react-router-dom';
|
import { useLocation, useParams } from 'react-router-dom';
|
||||||
|
|
||||||
import { currentPageIdAtom } from '../../../../atoms/mode';
|
import { currentPageIdAtom } from '../../../../atoms/mode';
|
||||||
|
import { CurrentWorkspaceService } from '../../../../modules/workspace/current-workspace';
|
||||||
|
|
||||||
export interface DumpInfoProps {
|
export interface DumpInfoProps {
|
||||||
error: any;
|
error: any;
|
||||||
@ -14,8 +14,10 @@ export interface DumpInfoProps {
|
|||||||
|
|
||||||
export const DumpInfo = (_props: DumpInfoProps) => {
|
export const DumpInfo = (_props: DumpInfoProps) => {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const workspaceList = useAtomValue(workspaceListAtom);
|
const workspaceList = useService(WorkspaceListService);
|
||||||
const currentWorkspace = useAtomValue(currentWorkspaceAtom);
|
const currentWorkspace = useLiveData(
|
||||||
|
useService(CurrentWorkspaceService).currentWorkspace
|
||||||
|
);
|
||||||
const currentPageId = useAtomValue(currentPageIdAtom);
|
const currentPageId = useAtomValue(currentPageIdAtom);
|
||||||
const path = location.pathname;
|
const path = location.pathname;
|
||||||
const query = useParams();
|
const query = useParams();
|
||||||
|
@ -1,13 +1,16 @@
|
|||||||
import { waitForCurrentWorkspaceAtom } from '@affine/core/modules/workspace';
|
import { useService } from '@toeverything/infra/di';
|
||||||
import { useAtomValue } from 'jotai';
|
import { useLiveData } from '@toeverything/infra/livedata';
|
||||||
import { Suspense, useEffect } from 'react';
|
import { Suspense, useEffect } from 'react';
|
||||||
|
|
||||||
import { useCurrentLoginStatus } from '../../../hooks/affine/use-current-login-status';
|
import { useCurrentLoginStatus } from '../../../hooks/affine/use-current-login-status';
|
||||||
import { useCurrentUser } from '../../../hooks/affine/use-current-user';
|
import { useCurrentUser } from '../../../hooks/affine/use-current-user';
|
||||||
|
import { CurrentWorkspaceService } from '../../../modules/workspace/current-workspace';
|
||||||
|
|
||||||
const SyncAwarenessInnerLoggedIn = () => {
|
const SyncAwarenessInnerLoggedIn = () => {
|
||||||
const currentUser = useCurrentUser();
|
const currentUser = useCurrentUser();
|
||||||
const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom);
|
const currentWorkspace = useLiveData(
|
||||||
|
useService(CurrentWorkspaceService).currentWorkspace
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (currentUser && currentWorkspace) {
|
if (currentUser && currentWorkspace) {
|
||||||
|
@ -5,18 +5,18 @@ import {
|
|||||||
Modal,
|
Modal,
|
||||||
} from '@affine/component/ui/modal';
|
} from '@affine/component/ui/modal';
|
||||||
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||||
import { workspaceManagerAtom } from '@affine/core/modules/workspace';
|
|
||||||
import { DebugLogger } from '@affine/debug';
|
import { DebugLogger } from '@affine/debug';
|
||||||
import { apis } from '@affine/electron-api';
|
import { apis } from '@affine/electron-api';
|
||||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { _addLocalWorkspace } from '@affine/workspace-impl';
|
import { _addLocalWorkspace } from '@affine/workspace-impl';
|
||||||
|
import { WorkspaceManager } from '@toeverything/infra';
|
||||||
import { getCurrentStore } from '@toeverything/infra/atom';
|
import { getCurrentStore } from '@toeverything/infra/atom';
|
||||||
import {
|
import {
|
||||||
buildShowcaseWorkspace,
|
buildShowcaseWorkspace,
|
||||||
initEmptyPage,
|
initEmptyPage,
|
||||||
} from '@toeverything/infra/blocksuite';
|
} from '@toeverything/infra/blocksuite';
|
||||||
import { useAtomValue } from 'jotai';
|
import { useService } from '@toeverything/infra/di';
|
||||||
import type { KeyboardEvent } from 'react';
|
import type { KeyboardEvent } from 'react';
|
||||||
import { useLayoutEffect } from 'react';
|
import { useLayoutEffect } from 'react';
|
||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
@ -101,7 +101,7 @@ export const CreateWorkspaceModal = ({
|
|||||||
}: ModalProps) => {
|
}: ModalProps) => {
|
||||||
const [step, setStep] = useState<CreateWorkspaceStep>();
|
const [step, setStep] = useState<CreateWorkspaceStep>();
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
const workspaceManager = useAtomValue(workspaceManagerAtom);
|
const workspaceManager = useService(WorkspaceManager);
|
||||||
|
|
||||||
// todo: maybe refactor using xstate?
|
// todo: maybe refactor using xstate?
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
@ -148,7 +148,7 @@ export const CreateWorkspaceModal = ({
|
|||||||
async (name: string) => {
|
async (name: string) => {
|
||||||
// this will be the last step for web for now
|
// this will be the last step for web for now
|
||||||
// fix me later
|
// fix me later
|
||||||
const id = await workspaceManager.createWorkspace(
|
const { id } = await workspaceManager.createWorkspace(
|
||||||
WorkspaceFlavour.LOCAL,
|
WorkspaceFlavour.LOCAL,
|
||||||
async workspace => {
|
async workspace => {
|
||||||
workspace.meta.setName(name);
|
workspace.meta.setName(name);
|
||||||
|
@ -8,10 +8,10 @@ import {
|
|||||||
listHistoryQuery,
|
listHistoryQuery,
|
||||||
recoverDocMutation,
|
recoverDocMutation,
|
||||||
} from '@affine/graphql';
|
} from '@affine/graphql';
|
||||||
import { globalBlockSuiteSchema } from '@affine/workspace';
|
import { AffineCloudBlobStorage } from '@affine/workspace-impl';
|
||||||
import { createAffineCloudBlobStorage } from '@affine/workspace-impl';
|
|
||||||
import { assertEquals } from '@blocksuite/global/utils';
|
import { assertEquals } from '@blocksuite/global/utils';
|
||||||
import { Workspace } from '@blocksuite/store';
|
import { Workspace } from '@blocksuite/store';
|
||||||
|
import { globalBlockSuiteSchema } from '@toeverything/infra';
|
||||||
import { revertUpdate } from '@toeverything/y-indexeddb';
|
import { revertUpdate } from '@toeverything/y-indexeddb';
|
||||||
import { useEffect, useMemo } from 'react';
|
import { useEffect, useMemo } from 'react';
|
||||||
import useSWRImmutable from 'swr/immutable';
|
import useSWRImmutable from 'swr/immutable';
|
||||||
@ -108,7 +108,7 @@ const workspaceMap = new Map<string, Workspace>();
|
|||||||
const getOrCreateShellWorkspace = (workspaceId: string) => {
|
const getOrCreateShellWorkspace = (workspaceId: string) => {
|
||||||
let workspace = workspaceMap.get(workspaceId);
|
let workspace = workspaceMap.get(workspaceId);
|
||||||
if (!workspace) {
|
if (!workspace) {
|
||||||
const blobStorage = createAffineCloudBlobStorage(workspaceId);
|
const blobStorage = new AffineCloudBlobStorage(workspaceId);
|
||||||
workspace = new Workspace({
|
workspace = new Workspace({
|
||||||
id: workspaceId,
|
id: workspaceId,
|
||||||
providerCreators: [],
|
providerCreators: [],
|
||||||
|
@ -7,14 +7,17 @@ import { useIsWorkspaceOwner } from '@affine/core/hooks/affine/use-is-workspace-
|
|||||||
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||||
import { useBlockSuiteWorkspacePageTitle } from '@affine/core/hooks/use-block-suite-workspace-page-title';
|
import { useBlockSuiteWorkspacePageTitle } from '@affine/core/hooks/use-block-suite-workspace-page-title';
|
||||||
import { useWorkspaceQuota } from '@affine/core/hooks/use-workspace-quota';
|
import { useWorkspaceQuota } from '@affine/core/hooks/use-workspace-quota';
|
||||||
import { waitForCurrentWorkspaceAtom } from '@affine/core/modules/workspace';
|
|
||||||
import { timestampToLocalTime } from '@affine/core/utils';
|
|
||||||
import { Trans } from '@affine/i18n';
|
import { Trans } from '@affine/i18n';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { CloseIcon, ToggleCollapseIcon } from '@blocksuite/icons';
|
import { CloseIcon, ToggleCollapseIcon } from '@blocksuite/icons';
|
||||||
import type { Page, Workspace } from '@blocksuite/store';
|
import {
|
||||||
|
type Page,
|
||||||
|
type Workspace as BlockSuiteWorkspace,
|
||||||
|
} from '@blocksuite/store';
|
||||||
import * as Collapsible from '@radix-ui/react-collapsible';
|
import * as Collapsible from '@radix-ui/react-collapsible';
|
||||||
import type { DialogContentProps } from '@radix-ui/react-dialog';
|
import type { DialogContentProps } from '@radix-ui/react-dialog';
|
||||||
|
import { Workspace } from '@toeverything/infra';
|
||||||
|
import { useService } from '@toeverything/infra/di';
|
||||||
import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai';
|
import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai';
|
||||||
import {
|
import {
|
||||||
Fragment,
|
Fragment,
|
||||||
@ -29,6 +32,7 @@ import { encodeStateAsUpdate } from 'yjs';
|
|||||||
|
|
||||||
import { currentModeAtom } from '../../../atoms/mode';
|
import { currentModeAtom } from '../../../atoms/mode';
|
||||||
import { pageHistoryModalAtom } from '../../../atoms/page-history';
|
import { pageHistoryModalAtom } from '../../../atoms/page-history';
|
||||||
|
import { timestampToLocalTime } from '../../../utils';
|
||||||
import { BlockSuiteEditor } from '../../blocksuite/block-suite-editor';
|
import { BlockSuiteEditor } from '../../blocksuite/block-suite-editor';
|
||||||
import { StyledEditorModeSwitch } from '../../blocksuite/block-suite-mode-switch/style';
|
import { StyledEditorModeSwitch } from '../../blocksuite/block-suite-mode-switch/style';
|
||||||
import {
|
import {
|
||||||
@ -48,7 +52,7 @@ import * as styles from './styles.css';
|
|||||||
export interface PageHistoryModalProps {
|
export interface PageHistoryModalProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
workspace: Workspace;
|
workspace: BlockSuiteWorkspace;
|
||||||
pageId: string;
|
pageId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -153,13 +157,12 @@ const HistoryEditorPreview = ({
|
|||||||
const planPromptClosedAtom = atom(false);
|
const planPromptClosedAtom = atom(false);
|
||||||
|
|
||||||
const PlanPrompt = () => {
|
const PlanPrompt = () => {
|
||||||
const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom);
|
const workspace = useService(Workspace);
|
||||||
|
const workspaceQuota = useWorkspaceQuota(workspace.id);
|
||||||
const isOwner = useIsWorkspaceOwner(currentWorkspace.meta);
|
|
||||||
const workspaceQuota = useWorkspaceQuota(currentWorkspace.id);
|
|
||||||
const isProWorkspace = useMemo(() => {
|
const isProWorkspace = useMemo(() => {
|
||||||
return workspaceQuota?.humanReadable.name.toLowerCase() !== 'free';
|
return workspaceQuota?.humanReadable.name.toLowerCase() !== 'free';
|
||||||
}, [workspaceQuota]);
|
}, [workspaceQuota]);
|
||||||
|
const isOwner = useIsWorkspaceOwner(workspace.meta);
|
||||||
|
|
||||||
const setSettingModalAtom = useSetAtom(openSettingModalAtom);
|
const setSettingModalAtom = useSetAtom(openSettingModalAtom);
|
||||||
const [planPromptClosed, setPlanPromptClosed] = useAtom(planPromptClosedAtom);
|
const [planPromptClosed, setPlanPromptClosed] = useAtom(planPromptClosedAtom);
|
||||||
@ -412,7 +415,7 @@ const PageHistoryManager = ({
|
|||||||
pageId,
|
pageId,
|
||||||
onClose,
|
onClose,
|
||||||
}: {
|
}: {
|
||||||
workspace: Workspace;
|
workspace: BlockSuiteWorkspace;
|
||||||
pageId: string;
|
pageId: string;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
@ -536,8 +539,7 @@ export const PageHistoryModal = ({
|
|||||||
|
|
||||||
export const GlobalPageHistoryModal = () => {
|
export const GlobalPageHistoryModal = () => {
|
||||||
const [{ open, pageId }, setState] = useAtom(pageHistoryModalAtom);
|
const [{ open, pageId }, setState] = useAtom(pageHistoryModalAtom);
|
||||||
const workspace = useAtomValue(waitForCurrentWorkspaceAtom);
|
const workspace = useService(Workspace);
|
||||||
|
|
||||||
const handleOpenChange = useCallback(
|
const handleOpenChange = useCallback(
|
||||||
(open: boolean) => {
|
(open: boolean) => {
|
||||||
setState(prev => ({
|
setState(prev => ({
|
||||||
|
@ -3,15 +3,15 @@ import { openQuotaModalAtom, openSettingModalAtom } from '@affine/core/atoms';
|
|||||||
import { useIsWorkspaceOwner } from '@affine/core/hooks/affine/use-is-workspace-owner';
|
import { useIsWorkspaceOwner } from '@affine/core/hooks/affine/use-is-workspace-owner';
|
||||||
import { useUserQuota } from '@affine/core/hooks/use-quota';
|
import { useUserQuota } from '@affine/core/hooks/use-quota';
|
||||||
import { useWorkspaceQuota } from '@affine/core/hooks/use-workspace-quota';
|
import { useWorkspaceQuota } from '@affine/core/hooks/use-workspace-quota';
|
||||||
import { waitForCurrentWorkspaceAtom } from '@affine/core/modules/workspace';
|
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
|
import { useService, Workspace } from '@toeverything/infra';
|
||||||
import bytes from 'bytes';
|
import bytes from 'bytes';
|
||||||
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
|
import { useAtom, useSetAtom } from 'jotai';
|
||||||
import { useCallback, useEffect, useMemo } from 'react';
|
import { useCallback, useEffect, useMemo } from 'react';
|
||||||
|
|
||||||
export const CloudQuotaModal = () => {
|
export const CloudQuotaModal = () => {
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom);
|
const currentWorkspace = useService(Workspace);
|
||||||
const [open, setOpen] = useAtom(openQuotaModalAtom);
|
const [open, setOpen] = useAtom(openQuotaModalAtom);
|
||||||
const workspaceQuota = useWorkspaceQuota(currentWorkspace.id);
|
const workspaceQuota = useWorkspaceQuota(currentWorkspace.id);
|
||||||
const isOwner = useIsWorkspaceOwner(currentWorkspace.meta);
|
const isOwner = useIsWorkspaceOwner(currentWorkspace.meta);
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
import { ConfirmModal } from '@affine/component/ui/modal';
|
import { ConfirmModal } from '@affine/component/ui/modal';
|
||||||
import { openQuotaModalAtom } from '@affine/core/atoms';
|
import { openQuotaModalAtom } from '@affine/core/atoms';
|
||||||
import { waitForCurrentWorkspaceAtom } from '@affine/core/modules/workspace';
|
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { useAtom, useAtomValue } from 'jotai';
|
import { useService, Workspace } from '@toeverything/infra';
|
||||||
|
import { useAtom } from 'jotai';
|
||||||
import { useCallback, useEffect } from 'react';
|
import { useCallback, useEffect } from 'react';
|
||||||
|
|
||||||
export const LocalQuotaModal = () => {
|
export const LocalQuotaModal = () => {
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom);
|
const currentWorkspace = useService(Workspace);
|
||||||
const [open, setOpen] = useAtom(openQuotaModalAtom);
|
const [open, setOpen] = useAtom(openQuotaModalAtom);
|
||||||
|
|
||||||
const onConfirm = useCallback(() => {
|
const onConfirm = useCallback(() => {
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { WorkspaceDetailSkeleton } from '@affine/component/setting-components';
|
import { WorkspaceDetailSkeleton } from '@affine/component/setting-components';
|
||||||
import { Modal, type ModalProps } from '@affine/component/ui/modal';
|
import { Modal, type ModalProps } from '@affine/component/ui/modal';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import type { WorkspaceMetadata } from '@affine/workspace/metadata';
|
|
||||||
import { ContactWithUsIcon } from '@blocksuite/icons';
|
import { ContactWithUsIcon } from '@blocksuite/icons';
|
||||||
|
import type { WorkspaceMetadata } from '@toeverything/infra';
|
||||||
import { debounce } from 'lodash-es';
|
import { debounce } from 'lodash-es';
|
||||||
import { Suspense, useCallback, useLayoutEffect, useRef } from 'react';
|
import { Suspense, useCallback, useLayoutEffect, useRef } from 'react';
|
||||||
|
|
||||||
|
@ -8,17 +8,19 @@ import { useIsWorkspaceOwner } from '@affine/core/hooks/affine/use-is-workspace-
|
|||||||
import { useWorkspaceBlobObjectUrl } from '@affine/core/hooks/use-workspace-blob';
|
import { useWorkspaceBlobObjectUrl } from '@affine/core/hooks/use-workspace-blob';
|
||||||
import { useWorkspaceAvailableFeatures } from '@affine/core/hooks/use-workspace-features';
|
import { useWorkspaceAvailableFeatures } from '@affine/core/hooks/use-workspace-features';
|
||||||
import { useWorkspaceInfo } from '@affine/core/hooks/use-workspace-info';
|
import { useWorkspaceInfo } from '@affine/core/hooks/use-workspace-info';
|
||||||
import {
|
|
||||||
waitForCurrentWorkspaceAtom,
|
|
||||||
workspaceListAtom,
|
|
||||||
} from '@affine/core/modules/workspace';
|
|
||||||
import { UNTITLED_WORKSPACE_NAME } from '@affine/env/constant';
|
import { UNTITLED_WORKSPACE_NAME } from '@affine/env/constant';
|
||||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import type { WorkspaceMetadata } from '@affine/workspace';
|
|
||||||
import { Logo1Icon } from '@blocksuite/icons';
|
import { Logo1Icon } from '@blocksuite/icons';
|
||||||
|
import {
|
||||||
|
Workspace,
|
||||||
|
WorkspaceManager,
|
||||||
|
type WorkspaceMetadata,
|
||||||
|
} from '@toeverything/infra';
|
||||||
|
import { useService } from '@toeverything/infra/di';
|
||||||
|
import { useLiveData } from '@toeverything/infra/livedata';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { useAtom, useAtomValue } from 'jotai/react';
|
import { useAtom } from 'jotai/react';
|
||||||
import { type ReactElement, Suspense, useCallback, useMemo } from 'react';
|
import { type ReactElement, Suspense, useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
import { authAtom } from '../../../../atoms';
|
import { authAtom } from '../../../../atoms';
|
||||||
@ -188,7 +190,9 @@ export const WorkspaceList = ({
|
|||||||
selectedWorkspaceId: string | null;
|
selectedWorkspaceId: string | null;
|
||||||
activeSubTab: WorkspaceSubTab;
|
activeSubTab: WorkspaceSubTab;
|
||||||
}) => {
|
}) => {
|
||||||
const workspaces = useAtomValue(workspaceListAtom);
|
const workspaces = useLiveData(
|
||||||
|
useService(WorkspaceManager).list.workspaceList
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{workspaces.map(workspace => {
|
{workspaces.map(workspace => {
|
||||||
@ -236,7 +240,7 @@ const WorkspaceListItem = ({
|
|||||||
const information = useWorkspaceInfo(meta);
|
const information = useWorkspaceInfo(meta);
|
||||||
const avatarUrl = useWorkspaceBlobObjectUrl(meta, information?.avatar);
|
const avatarUrl = useWorkspaceBlobObjectUrl(meta, information?.avatar);
|
||||||
const name = information?.name ?? UNTITLED_WORKSPACE_NAME;
|
const name = information?.name ?? UNTITLED_WORKSPACE_NAME;
|
||||||
const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom);
|
const currentWorkspace = useService(Workspace);
|
||||||
const isCurrent = currentWorkspace.id === meta.id;
|
const isCurrent = currentWorkspace.id === meta.id;
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
const isOwner = useIsWorkspaceOwner(meta);
|
const isOwner = useIsWorkspaceOwner(meta);
|
||||||
|
@ -8,7 +8,7 @@ import {
|
|||||||
} from '@affine/core/hooks/use-workspace-features';
|
} from '@affine/core/hooks/use-workspace-features';
|
||||||
import { FeatureType } from '@affine/graphql';
|
import { FeatureType } from '@affine/graphql';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import type { WorkspaceMetadata } from '@affine/workspace/metadata';
|
import type { WorkspaceMetadata } from '@toeverything/infra';
|
||||||
import { useAtom } from 'jotai';
|
import { useAtom } from 'jotai';
|
||||||
import { atomWithStorage } from 'jotai/utils';
|
import { atomWithStorage } from 'jotai/utils';
|
||||||
import { Suspense, useCallback, useState } from 'react';
|
import { Suspense, useCallback, useState } from 'react';
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import type { WorkspaceMetadata } from '@affine/workspace/metadata';
|
import type { WorkspaceMetadata } from '@toeverything/infra';
|
||||||
|
|
||||||
import { useIsWorkspaceOwner } from '../../../../hooks/affine/use-is-workspace-owner';
|
import { useIsWorkspaceOwner } from '../../../../hooks/affine/use-is-workspace-owner';
|
||||||
import { ExperimentalFeatures } from './experimental-features';
|
import { ExperimentalFeatures } from './experimental-features';
|
||||||
|
@ -8,7 +8,7 @@ import { UNTITLED_WORKSPACE_NAME } from '@affine/env/constant';
|
|||||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||||
import { Trans } from '@affine/i18n';
|
import { Trans } from '@affine/i18n';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import type { WorkspaceMetadata } from '@affine/workspace/metadata';
|
import type { WorkspaceMetadata } from '@toeverything/infra';
|
||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
|
|
||||||
import * as styles from './style.css';
|
import * as styles from './style.css';
|
||||||
|
@ -2,15 +2,12 @@ import { pushNotificationAtom } from '@affine/component/notification-center';
|
|||||||
import { SettingRow } from '@affine/component/setting-components';
|
import { SettingRow } from '@affine/component/setting-components';
|
||||||
import { ConfirmModal } from '@affine/component/ui/modal';
|
import { ConfirmModal } from '@affine/component/ui/modal';
|
||||||
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||||
import {
|
|
||||||
currentWorkspaceAtom,
|
|
||||||
workspaceListAtom,
|
|
||||||
workspaceManagerAtom,
|
|
||||||
} from '@affine/core/modules/workspace';
|
|
||||||
import { WorkspaceSubPath } from '@affine/core/shared';
|
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { ArrowRightSmallIcon } from '@blocksuite/icons';
|
import { ArrowRightSmallIcon } from '@blocksuite/icons';
|
||||||
import { useAtomValue, useSetAtom } from 'jotai';
|
import { Workspace, WorkspaceManager } from '@toeverything/infra';
|
||||||
|
import { useService } from '@toeverything/infra/di';
|
||||||
|
import { useLiveData } from '@toeverything/infra/livedata';
|
||||||
|
import { useSetAtom } from 'jotai';
|
||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
|
|
||||||
import { openSettingModalAtom } from '../../../../../../atoms';
|
import { openSettingModalAtom } from '../../../../../../atoms';
|
||||||
@ -18,6 +15,7 @@ import {
|
|||||||
RouteLogic,
|
RouteLogic,
|
||||||
useNavigateHelper,
|
useNavigateHelper,
|
||||||
} from '../../../../../../hooks/use-navigate-helper';
|
} from '../../../../../../hooks/use-navigate-helper';
|
||||||
|
import { WorkspaceSubPath } from '../../../../../../shared';
|
||||||
import type { WorkspaceSettingDetailProps } from '../types';
|
import type { WorkspaceSettingDetailProps } from '../types';
|
||||||
import { WorkspaceDeleteModal } from './delete';
|
import { WorkspaceDeleteModal } from './delete';
|
||||||
|
|
||||||
@ -35,9 +33,9 @@ export const DeleteLeaveWorkspace = ({
|
|||||||
const [showLeave, setShowLeave] = useState(false);
|
const [showLeave, setShowLeave] = useState(false);
|
||||||
const setSettingModal = useSetAtom(openSettingModalAtom);
|
const setSettingModal = useSetAtom(openSettingModalAtom);
|
||||||
|
|
||||||
const workspaceManager = useAtomValue(workspaceManagerAtom);
|
const workspaceManager = useService(WorkspaceManager);
|
||||||
const workspaceList = useAtomValue(workspaceListAtom);
|
const workspaceList = useLiveData(workspaceManager.list.workspaceList);
|
||||||
const currentWorkspace = useAtomValue(currentWorkspaceAtom);
|
const currentWorkspace = useService(Workspace);
|
||||||
const pushNotification = useSetAtom(pushNotificationAtom);
|
const pushNotification = useSetAtom(pushNotificationAtom);
|
||||||
|
|
||||||
const onLeaveOrDelete = useCallback(() => {
|
const onLeaveOrDelete = useCallback(() => {
|
||||||
|
@ -2,17 +2,17 @@ import { SettingRow } from '@affine/component/setting-components';
|
|||||||
import { Button } from '@affine/component/ui/button';
|
import { Button } from '@affine/component/ui/button';
|
||||||
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||||
import { useWorkspaceInfo } from '@affine/core/hooks/use-workspace-info';
|
import { useWorkspaceInfo } from '@affine/core/hooks/use-workspace-info';
|
||||||
import { workspaceManagerAtom } from '@affine/core/modules/workspace';
|
|
||||||
import { WorkspaceSubPath } from '@affine/core/shared';
|
|
||||||
import { UNTITLED_WORKSPACE_NAME } from '@affine/env/constant';
|
import { UNTITLED_WORKSPACE_NAME } from '@affine/env/constant';
|
||||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import type { Workspace } from '@affine/workspace';
|
import { type Workspace, WorkspaceManager } from '@toeverything/infra';
|
||||||
import { useAtomValue, useSetAtom } from 'jotai';
|
import { useService } from '@toeverything/infra/di';
|
||||||
|
import { useSetAtom } from 'jotai';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { openSettingModalAtom } from '../../../../../atoms';
|
import { openSettingModalAtom } from '../../../../../atoms';
|
||||||
import { useNavigateHelper } from '../../../../../hooks/use-navigate-helper';
|
import { useNavigateHelper } from '../../../../../hooks/use-navigate-helper';
|
||||||
|
import { WorkspaceSubPath } from '../../../../../shared';
|
||||||
import { EnableAffineCloudModal } from '../../../enable-affine-cloud-modal';
|
import { EnableAffineCloudModal } from '../../../enable-affine-cloud-modal';
|
||||||
import { TmpDisableAffineCloudModal } from '../../../tmp-disable-affine-cloud-modal';
|
import { TmpDisableAffineCloudModal } from '../../../tmp-disable-affine-cloud-modal';
|
||||||
import type { WorkspaceSettingDetailProps } from './types';
|
import type { WorkspaceSettingDetailProps } from './types';
|
||||||
@ -29,7 +29,7 @@ export const EnableCloudPanel = ({
|
|||||||
|
|
||||||
const { openPage } = useNavigateHelper();
|
const { openPage } = useNavigateHelper();
|
||||||
|
|
||||||
const workspaceManager = useAtomValue(workspaceManagerAtom);
|
const workspaceManager = useService(WorkspaceManager);
|
||||||
const workspaceInfo = useWorkspaceInfo(workspaceMetadata);
|
const workspaceInfo = useWorkspaceInfo(workspaceMetadata);
|
||||||
const setSettingModal = useSetAtom(openSettingModalAtom);
|
const setSettingModal = useSetAtom(openSettingModalAtom);
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@ import { Button } from '@affine/component/ui/button';
|
|||||||
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||||
import { apis } from '@affine/electron-api';
|
import { apis } from '@affine/electron-api';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import type { Workspace, WorkspaceMetadata } from '@affine/workspace';
|
import type { Workspace, WorkspaceMetadata } from '@toeverything/infra';
|
||||||
import { useSetAtom } from 'jotai';
|
import { useSetAtom } from 'jotai';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
@ -9,9 +9,9 @@ import { useWorkspaceStatus } from '@affine/core/hooks/use-workspace-status';
|
|||||||
import { validateAndReduceImage } from '@affine/core/utils/reduce-image';
|
import { validateAndReduceImage } from '@affine/core/utils/reduce-image';
|
||||||
import { UNTITLED_WORKSPACE_NAME } from '@affine/env/constant';
|
import { UNTITLED_WORKSPACE_NAME } from '@affine/env/constant';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import type { Workspace } from '@affine/workspace';
|
|
||||||
import { SyncPeerStep } from '@affine/workspace';
|
|
||||||
import { CameraIcon } from '@blocksuite/icons';
|
import { CameraIcon } from '@blocksuite/icons';
|
||||||
|
import type { Workspace } from '@toeverything/infra';
|
||||||
|
import { SyncPeerStep } from '@toeverything/infra';
|
||||||
import { useSetAtom } from 'jotai';
|
import { useSetAtom } from 'jotai';
|
||||||
import {
|
import {
|
||||||
type KeyboardEvent,
|
type KeyboardEvent,
|
||||||
|
@ -4,7 +4,7 @@ import { Button } from '@affine/component/ui/button';
|
|||||||
import { Tooltip } from '@affine/component/ui/tooltip';
|
import { Tooltip } from '@affine/component/ui/tooltip';
|
||||||
import { apis, events } from '@affine/electron-api';
|
import { apis, events } from '@affine/electron-api';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import type { WorkspaceMetadata } from '@affine/workspace/metadata';
|
import type { WorkspaceMetadata } from '@toeverything/infra';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import type { WorkspaceMetadata } from '@affine/workspace/metadata';
|
import type { WorkspaceMetadata } from '@toeverything/infra';
|
||||||
|
|
||||||
export interface WorkspaceSettingDetailProps {
|
export interface WorkspaceSettingDetailProps {
|
||||||
isOwner: boolean;
|
isOwner: boolean;
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||||
import { workspaceManagerAtom } from '@affine/core/modules/workspace';
|
|
||||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||||
import type { Workspace } from '@affine/workspace';
|
|
||||||
import type { Page } from '@blocksuite/store';
|
import type { Page } from '@blocksuite/store';
|
||||||
import { useAtomValue } from 'jotai';
|
import { type Workspace, WorkspaceManager } from '@toeverything/infra';
|
||||||
|
import { useService } from '@toeverything/infra';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { useNavigateHelper } from '../../../hooks/use-navigate-helper';
|
import { useNavigateHelper } from '../../../hooks/use-navigate-helper';
|
||||||
@ -25,7 +24,7 @@ export const SharePageButton = ({
|
|||||||
|
|
||||||
const { openPage } = useNavigateHelper();
|
const { openPage } = useNavigateHelper();
|
||||||
|
|
||||||
const workspaceManager = useAtomValue(workspaceManagerAtom);
|
const workspaceManager = useService(WorkspaceManager);
|
||||||
|
|
||||||
const handleConfirm = useAsyncCallback(async () => {
|
const handleConfirm = useAsyncCallback(async () => {
|
||||||
if (workspace.flavour !== WorkspaceFlavour.LOCAL) {
|
if (workspace.flavour !== WorkspaceFlavour.LOCAL) {
|
||||||
|
@ -3,9 +3,9 @@ import { Divider } from '@affine/component/ui/divider';
|
|||||||
import { Menu } from '@affine/component/ui/menu';
|
import { Menu } from '@affine/component/ui/menu';
|
||||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import type { WorkspaceMetadata } from '@affine/workspace';
|
|
||||||
import { WebIcon } from '@blocksuite/icons';
|
import { WebIcon } from '@blocksuite/icons';
|
||||||
import type { Page } from '@blocksuite/store';
|
import type { Page } from '@blocksuite/store';
|
||||||
|
import type { WorkspaceMetadata } from '@toeverything/infra';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
|
||||||
import { useIsSharedPage } from '../../../../hooks/affine/use-is-shared-page';
|
import { useIsSharedPage } from '../../../../hooks/affine/use-is-shared-page';
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import { FavoriteTag } from '@affine/core/components/page-list';
|
import { FavoriteTag } from '@affine/core/components/page-list';
|
||||||
import { useBlockSuiteMetaHelper } from '@affine/core/hooks/affine/use-block-suite-meta-helper';
|
import { useBlockSuiteMetaHelper } from '@affine/core/hooks/affine/use-block-suite-meta-helper';
|
||||||
import { useBlockSuitePageMeta } from '@affine/core/hooks/use-block-suite-page-meta';
|
import { useBlockSuitePageMeta } from '@affine/core/hooks/use-block-suite-page-meta';
|
||||||
import { waitForCurrentWorkspaceAtom } from '@affine/core/modules/workspace';
|
|
||||||
import { toast } from '@affine/core/utils';
|
import { toast } from '@affine/core/utils';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { assertExists } from '@blocksuite/global/utils';
|
import { assertExists } from '@blocksuite/global/utils';
|
||||||
import { useAtomValue } from 'jotai';
|
import { Workspace } from '@toeverything/infra';
|
||||||
|
import { useService } from '@toeverything/infra/di';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
export interface FavoriteButtonProps {
|
export interface FavoriteButtonProps {
|
||||||
@ -14,7 +14,7 @@ export interface FavoriteButtonProps {
|
|||||||
|
|
||||||
export const useFavorite = (pageId: string) => {
|
export const useFavorite = (pageId: string) => {
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
const workspace = useAtomValue(waitForCurrentWorkspaceAtom);
|
const workspace = useService(Workspace);
|
||||||
const blockSuiteWorkspace = workspace.blockSuiteWorkspace;
|
const blockSuiteWorkspace = workspace.blockSuiteWorkspace;
|
||||||
const currentPage = blockSuiteWorkspace.getPage(pageId);
|
const currentPage = blockSuiteWorkspace.getPage(pageId);
|
||||||
assertExists(currentPage);
|
assertExists(currentPage);
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { toast } from '@affine/component';
|
||||||
import {
|
import {
|
||||||
Menu,
|
Menu,
|
||||||
MenuIcon,
|
MenuIcon,
|
||||||
@ -11,8 +12,6 @@ import { useBlockSuiteMetaHelper } from '@affine/core/hooks/affine/use-block-sui
|
|||||||
import { useExportPage } from '@affine/core/hooks/affine/use-export-page';
|
import { useExportPage } from '@affine/core/hooks/affine/use-export-page';
|
||||||
import { useTrashModalHelper } from '@affine/core/hooks/affine/use-trash-modal-helper';
|
import { useTrashModalHelper } from '@affine/core/hooks/affine/use-trash-modal-helper';
|
||||||
import { useBlockSuitePageMeta } from '@affine/core/hooks/use-block-suite-page-meta';
|
import { useBlockSuitePageMeta } from '@affine/core/hooks/use-block-suite-page-meta';
|
||||||
import { waitForCurrentWorkspaceAtom } from '@affine/core/modules/workspace';
|
|
||||||
import { toast } from '@affine/core/utils';
|
|
||||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { assertExists } from '@blocksuite/global/utils';
|
import { assertExists } from '@blocksuite/global/utils';
|
||||||
@ -26,6 +25,7 @@ import {
|
|||||||
ImportIcon,
|
ImportIcon,
|
||||||
PageIcon,
|
PageIcon,
|
||||||
} from '@blocksuite/icons';
|
} from '@blocksuite/icons';
|
||||||
|
import { useService, Workspace } from '@toeverything/infra';
|
||||||
import { useAtomValue } from 'jotai';
|
import { useAtomValue } from 'jotai';
|
||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
|
|
||||||
@ -46,8 +46,7 @@ export const PageHeaderMenuButton = ({
|
|||||||
}: PageMenuProps) => {
|
}: PageMenuProps) => {
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
|
|
||||||
// fixme(himself65): remove these hooks ASAP
|
const workspace = useService(Workspace);
|
||||||
const workspace = useAtomValue(waitForCurrentWorkspaceAtom);
|
|
||||||
const blockSuiteWorkspace = workspace.blockSuiteWorkspace;
|
const blockSuiteWorkspace = workspace.blockSuiteWorkspace;
|
||||||
const currentPage = blockSuiteWorkspace.getPage(pageId);
|
const currentPage = blockSuiteWorkspace.getPage(pageId);
|
||||||
assertExists(currentPage);
|
assertExists(currentPage);
|
||||||
|
@ -3,57 +3,46 @@
|
|||||||
*/
|
*/
|
||||||
import 'fake-indexeddb/auto';
|
import 'fake-indexeddb/auto';
|
||||||
|
|
||||||
|
import type { CollectionService } from '@affine/core/modules/collection';
|
||||||
import type { Collection } from '@affine/env/filter';
|
import type { Collection } from '@affine/env/filter';
|
||||||
import { renderHook } from '@testing-library/react';
|
import { renderHook } from '@testing-library/react';
|
||||||
import { atom } from 'jotai';
|
import { LiveData } from '@toeverything/infra';
|
||||||
import { atomWithObservable } from 'jotai/utils';
|
|
||||||
import { BehaviorSubject } from 'rxjs';
|
import { BehaviorSubject } from 'rxjs';
|
||||||
import { expect, test } from 'vitest';
|
import { expect, test } from 'vitest';
|
||||||
|
|
||||||
import { createDefaultFilter, vars } from '../filter/vars';
|
import { createDefaultFilter, vars } from '../filter/vars';
|
||||||
import {
|
import { useCollectionManager } from '../use-collection-manager';
|
||||||
type CollectionsCRUD,
|
|
||||||
useCollectionManager,
|
|
||||||
} from '../use-collection-manager';
|
|
||||||
|
|
||||||
const defaultMeta = { tags: { options: [] } };
|
const defaultMeta = { tags: { options: [] } };
|
||||||
const collectionsSubject = new BehaviorSubject<Collection[]>([]);
|
const collectionsSubject = new BehaviorSubject<Collection[]>([]);
|
||||||
const baseAtom = atomWithObservable<Collection[]>(
|
|
||||||
() => {
|
|
||||||
return collectionsSubject;
|
|
||||||
},
|
|
||||||
{
|
|
||||||
initialValue: [],
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const mockAtom = atom(get => {
|
const mockWorkspaceCollectionService = {
|
||||||
return {
|
collections: LiveData.from(collectionsSubject, []),
|
||||||
collections: get(baseAtom),
|
addCollection: (...collections) => {
|
||||||
addCollection: (...collections) => {
|
const prev = collectionsSubject.value;
|
||||||
const prev = collectionsSubject.value;
|
collectionsSubject.next([...collections, ...prev]);
|
||||||
collectionsSubject.next([...collections, ...prev]);
|
},
|
||||||
},
|
deleteCollection: (...ids) => {
|
||||||
deleteCollection: (...ids) => {
|
const prev = collectionsSubject.value;
|
||||||
const prev = collectionsSubject.value;
|
collectionsSubject.next(prev.filter(v => !ids.includes(v.id)));
|
||||||
collectionsSubject.next(prev.filter(v => !ids.includes(v.id)));
|
},
|
||||||
},
|
updateCollection: (id, updater) => {
|
||||||
updateCollection: (id, updater) => {
|
const prev = collectionsSubject.value;
|
||||||
const prev = collectionsSubject.value;
|
collectionsSubject.next(
|
||||||
collectionsSubject.next(
|
prev.map(v => {
|
||||||
prev.map(v => {
|
if (v.id === id) {
|
||||||
if (v.id === id) {
|
return updater(v);
|
||||||
return updater(v);
|
}
|
||||||
}
|
return v;
|
||||||
return v;
|
})
|
||||||
})
|
);
|
||||||
);
|
},
|
||||||
},
|
} as CollectionService;
|
||||||
} satisfies CollectionsCRUD;
|
|
||||||
});
|
|
||||||
|
|
||||||
test('useAllPageSetting', async () => {
|
test('useAllPageSetting', async () => {
|
||||||
const settingHook = renderHook(() => useCollectionManager(mockAtom));
|
const settingHook = renderHook(() =>
|
||||||
|
useCollectionManager(mockWorkspaceCollectionService)
|
||||||
|
);
|
||||||
const prevCollection = settingHook.result.current.currentCollection;
|
const prevCollection = settingHook.result.current.currentCollection;
|
||||||
expect(settingHook.result.current.savedCollections).toEqual([]);
|
expect(settingHook.result.current.savedCollections).toEqual([]);
|
||||||
settingHook.result.current.updateCollection({
|
settingHook.result.current.updateCollection({
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
import { collectionsCRUDAtom } from '@affine/core/atoms/collections';
|
|
||||||
import { useDeleteCollectionInfo } from '@affine/core/hooks/affine/use-delete-collection-info';
|
import { useDeleteCollectionInfo } from '@affine/core/hooks/affine/use-delete-collection-info';
|
||||||
import { waitForCurrentWorkspaceAtom } from '@affine/core/modules/workspace';
|
|
||||||
import type { Collection, DeleteCollectionInfo } from '@affine/env/filter';
|
import type { Collection, DeleteCollectionInfo } from '@affine/env/filter';
|
||||||
import { Trans } from '@affine/i18n';
|
import { Trans } from '@affine/i18n';
|
||||||
import { useAtomValue } from 'jotai';
|
import { useService } from '@toeverything/infra';
|
||||||
|
import { Workspace } from '@toeverything/infra';
|
||||||
import {
|
import {
|
||||||
type ReactElement,
|
type ReactElement,
|
||||||
useCallback,
|
useCallback,
|
||||||
@ -12,6 +11,7 @@ import {
|
|||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
|
||||||
|
import { CollectionService } from '../../../modules/collection';
|
||||||
import { ListFloatingToolbar } from '../components/list-floating-toolbar';
|
import { ListFloatingToolbar } from '../components/list-floating-toolbar';
|
||||||
import { collectionHeaderColsDef } from '../header-col-def';
|
import { collectionHeaderColsDef } from '../header-col-def';
|
||||||
import { CollectionOperationCell } from '../operation-cell';
|
import { CollectionOperationCell } from '../operation-cell';
|
||||||
@ -69,8 +69,8 @@ export const VirtualizedCollectionList = ({
|
|||||||
const [selectedCollectionIds, setSelectedCollectionIds] = useState<string[]>(
|
const [selectedCollectionIds, setSelectedCollectionIds] = useState<string[]>(
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
const setting = useCollectionManager(collectionsCRUDAtom);
|
const setting = useCollectionManager(useService(CollectionService));
|
||||||
const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom);
|
const currentWorkspace = useService(Workspace);
|
||||||
const info = useDeleteCollectionInfo();
|
const info = useDeleteCollectionInfo();
|
||||||
|
|
||||||
const collectionOperations = useCollectionOperationsRenderer({
|
const collectionOperations = useCollectionOperationsRenderer({
|
||||||
|
@ -1,13 +1,14 @@
|
|||||||
import { Button } from '@affine/component';
|
import { Button } from '@affine/component';
|
||||||
import { collectionsCRUDAtom } from '@affine/core/atoms/collections';
|
|
||||||
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||||
import { useNavigateHelper } from '@affine/core/hooks/use-navigate-helper';
|
import { useNavigateHelper } from '@affine/core/hooks/use-navigate-helper';
|
||||||
import type { Collection, Tag } from '@affine/env/filter';
|
import type { Collection, Tag } from '@affine/env/filter';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { ViewLayersIcon } from '@blocksuite/icons';
|
import { ViewLayersIcon } from '@blocksuite/icons';
|
||||||
|
import { useService } from '@toeverything/infra/di';
|
||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
|
import { CollectionService } from '../../../modules/collection';
|
||||||
import { createTagFilter } from '../filter/utils';
|
import { createTagFilter } from '../filter/utils';
|
||||||
import {
|
import {
|
||||||
createEmptyCollection,
|
createEmptyCollection,
|
||||||
@ -24,7 +25,7 @@ import { PageListNewPageButton } from './page-list-new-page-button';
|
|||||||
|
|
||||||
export const PageListHeader = ({ workspaceId }: { workspaceId: string }) => {
|
export const PageListHeader = ({ workspaceId }: { workspaceId: string }) => {
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
const setting = useCollectionManager(collectionsCRUDAtom);
|
const setting = useCollectionManager(useService(CollectionService));
|
||||||
const { jumpToCollections } = useNavigateHelper();
|
const { jumpToCollections } = useNavigateHelper();
|
||||||
|
|
||||||
const handleJumpToCollections = useCallback(() => {
|
const handleJumpToCollections = useCallback(() => {
|
||||||
@ -74,14 +75,16 @@ export const CollectionPageListHeader = ({
|
|||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
}) => {
|
}) => {
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
const setting = useCollectionManager(collectionsCRUDAtom);
|
const setting = useCollectionManager(useService(CollectionService));
|
||||||
const { jumpToCollections } = useNavigateHelper();
|
const { jumpToCollections } = useNavigateHelper();
|
||||||
|
|
||||||
const handleJumpToCollections = useCallback(() => {
|
const handleJumpToCollections = useCallback(() => {
|
||||||
jumpToCollections(workspaceId);
|
jumpToCollections(workspaceId);
|
||||||
}, [jumpToCollections, workspaceId]);
|
}, [jumpToCollections, workspaceId]);
|
||||||
|
|
||||||
const { updateCollection } = useCollectionManager(collectionsCRUDAtom);
|
const { updateCollection } = useCollectionManager(
|
||||||
|
useService(CollectionService)
|
||||||
|
);
|
||||||
const { node, open } = useEditCollection(config);
|
const { node, open } = useEditCollection(config);
|
||||||
|
|
||||||
const handleAddPage = useAsyncCallback(async () => {
|
const handleAddPage = useAsyncCallback(async () => {
|
||||||
@ -121,7 +124,7 @@ export const TagPageListHeader = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
const { jumpToTags, jumpToCollection } = useNavigateHelper();
|
const { jumpToTags, jumpToCollection } = useNavigateHelper();
|
||||||
const setting = useCollectionManager(collectionsCRUDAtom);
|
const setting = useCollectionManager(useService(CollectionService));
|
||||||
const { open, node } = useEditCollectionName({
|
const { open, node } = useEditCollectionName({
|
||||||
title: t['com.affine.editCollection.saveCollection'](),
|
title: t['com.affine.editCollection.saveCollection'](),
|
||||||
showTips: true,
|
showTips: true,
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { waitForCurrentWorkspaceAtom } from '@affine/core/modules/workspace';
|
import { Workspace } from '@toeverything/infra';
|
||||||
import { useAtomValue } from 'jotai';
|
import { useService } from '@toeverything/infra/di';
|
||||||
import type { PropsWithChildren } from 'react';
|
import type { PropsWithChildren } from 'react';
|
||||||
|
|
||||||
import { usePageHelper } from '../../blocksuite/block-suite-page-list/utils';
|
import { usePageHelper } from '../../blocksuite/block-suite-page-list/utils';
|
||||||
@ -16,7 +16,7 @@ export const PageListNewPageButton = ({
|
|||||||
size?: 'small' | 'default';
|
size?: 'small' | 'default';
|
||||||
testId?: string;
|
testId?: string;
|
||||||
}>) => {
|
}>) => {
|
||||||
const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom);
|
const currentWorkspace = useService(Workspace);
|
||||||
const { importFile, createEdgeless, createPage } = usePageHelper(
|
const { importFile, createEdgeless, createPage } = usePageHelper(
|
||||||
currentWorkspace.blockSuiteWorkspace
|
currentWorkspace.blockSuiteWorkspace
|
||||||
);
|
);
|
||||||
|
@ -2,12 +2,12 @@ import { toast } from '@affine/component';
|
|||||||
import { useBlockSuiteMetaHelper } from '@affine/core/hooks/affine/use-block-suite-meta-helper';
|
import { useBlockSuiteMetaHelper } from '@affine/core/hooks/affine/use-block-suite-meta-helper';
|
||||||
import { useTrashModalHelper } from '@affine/core/hooks/affine/use-trash-modal-helper';
|
import { useTrashModalHelper } from '@affine/core/hooks/affine/use-trash-modal-helper';
|
||||||
import { useBlockSuitePageMeta } from '@affine/core/hooks/use-block-suite-page-meta';
|
import { useBlockSuitePageMeta } from '@affine/core/hooks/use-block-suite-page-meta';
|
||||||
import { waitForCurrentWorkspaceAtom } from '@affine/core/modules/workspace';
|
|
||||||
import type { Collection } from '@affine/env/filter';
|
import type { Collection } from '@affine/env/filter';
|
||||||
import { Trans } from '@affine/i18n';
|
import { Trans } from '@affine/i18n';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import type { PageMeta, Tag } from '@blocksuite/store';
|
import type { PageMeta, Tag } from '@blocksuite/store';
|
||||||
import { useAtomValue } from 'jotai';
|
import { useService } from '@toeverything/infra';
|
||||||
|
import { Workspace } from '@toeverything/infra';
|
||||||
import { useCallback, useMemo, useRef, useState } from 'react';
|
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||||
|
|
||||||
import { usePageHelper } from '../../blocksuite/block-suite-page-list/utils';
|
import { usePageHelper } from '../../blocksuite/block-suite-page-list/utils';
|
||||||
@ -27,7 +27,7 @@ import {
|
|||||||
} from './page-list-header';
|
} from './page-list-header';
|
||||||
|
|
||||||
const usePageOperationsRenderer = () => {
|
const usePageOperationsRenderer = () => {
|
||||||
const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom);
|
const currentWorkspace = useService(Workspace);
|
||||||
const { setTrashModal } = useTrashModalHelper(
|
const { setTrashModal } = useTrashModalHelper(
|
||||||
currentWorkspace.blockSuiteWorkspace
|
currentWorkspace.blockSuiteWorkspace
|
||||||
);
|
);
|
||||||
@ -89,7 +89,7 @@ export const VirtualizedPageList = ({
|
|||||||
const listRef = useRef<ItemListHandle>(null);
|
const listRef = useRef<ItemListHandle>(null);
|
||||||
const [showFloatingToolbar, setShowFloatingToolbar] = useState(false);
|
const [showFloatingToolbar, setShowFloatingToolbar] = useState(false);
|
||||||
const [selectedPageIds, setSelectedPageIds] = useState<string[]>([]);
|
const [selectedPageIds, setSelectedPageIds] = useState<string[]>([]);
|
||||||
const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom);
|
const currentWorkspace = useService(Workspace);
|
||||||
const pageMetas = useBlockSuitePageMeta(currentWorkspace.blockSuiteWorkspace);
|
const pageMetas = useBlockSuitePageMeta(currentWorkspace.blockSuiteWorkspace);
|
||||||
const pageOperations = usePageOperationsRenderer();
|
const pageOperations = usePageOperationsRenderer();
|
||||||
const { isPreferredEdgeless } = usePageHelper(
|
const { isPreferredEdgeless } = usePageHelper(
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { waitForCurrentWorkspaceAtom } from '@affine/core/modules/workspace';
|
|
||||||
import { Trans } from '@affine/i18n';
|
import { Trans } from '@affine/i18n';
|
||||||
import type { Tag } from '@blocksuite/store';
|
import type { Tag } from '@blocksuite/store';
|
||||||
import { useAtomValue } from 'jotai';
|
import { useService } from '@toeverything/infra';
|
||||||
|
import { Workspace } from '@toeverything/infra';
|
||||||
import { useCallback, useMemo, useRef, useState } from 'react';
|
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||||
|
|
||||||
import { ListFloatingToolbar } from '../components/list-floating-toolbar';
|
import { ListFloatingToolbar } from '../components/list-floating-toolbar';
|
||||||
@ -26,7 +26,7 @@ export const VirtualizedTagList = ({
|
|||||||
const listRef = useRef<ItemListHandle>(null);
|
const listRef = useRef<ItemListHandle>(null);
|
||||||
const [showFloatingToolbar, setShowFloatingToolbar] = useState(false);
|
const [showFloatingToolbar, setShowFloatingToolbar] = useState(false);
|
||||||
const [selectedTagIds, setSelectedTagIds] = useState<string[]>([]);
|
const [selectedTagIds, setSelectedTagIds] = useState<string[]>([]);
|
||||||
const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom);
|
const currentWorkspace = useService(Workspace);
|
||||||
|
|
||||||
const filteredSelectedTagIds = useMemo(() => {
|
const filteredSelectedTagIds = useMemo(() => {
|
||||||
const ids = tags.map(tag => tag.id);
|
const ids = tags.map(tag => tag.id);
|
||||||
|
@ -1,11 +1,8 @@
|
|||||||
import type {
|
import type { CollectionService } from '@affine/core/modules/collection';
|
||||||
Collection,
|
import type { Collection, Filter, VariableMap } from '@affine/env/filter';
|
||||||
DeleteCollectionInfo,
|
|
||||||
Filter,
|
|
||||||
VariableMap,
|
|
||||||
} from '@affine/env/filter';
|
|
||||||
import type { PageMeta } from '@blocksuite/store';
|
import type { PageMeta } from '@blocksuite/store';
|
||||||
import { type Atom, useAtom, useAtomValue } from 'jotai';
|
import { useLiveData } from '@toeverything/infra/livedata';
|
||||||
|
import { useAtom, useAtomValue } from 'jotai';
|
||||||
import { atomWithReset } from 'jotai/utils';
|
import { atomWithReset } from 'jotai/utils';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { NIL } from 'uuid';
|
import { NIL } from 'uuid';
|
||||||
@ -32,47 +29,28 @@ export const currentCollectionAtom = atomWithReset<string>(NIL);
|
|||||||
|
|
||||||
export type Updater<T> = (value: T) => T;
|
export type Updater<T> = (value: T) => T;
|
||||||
export type CollectionUpdater = Updater<Collection>;
|
export type CollectionUpdater = Updater<Collection>;
|
||||||
export type CollectionsCRUD = {
|
|
||||||
addCollection: (...collections: Collection[]) => void;
|
|
||||||
collections: Collection[];
|
|
||||||
updateCollection: (id: string, updater: CollectionUpdater) => void;
|
|
||||||
deleteCollection: (info: DeleteCollectionInfo, ...ids: string[]) => void;
|
|
||||||
};
|
|
||||||
export type CollectionsCRUDAtom = Atom<
|
|
||||||
Promise<CollectionsCRUD> | CollectionsCRUD
|
|
||||||
>;
|
|
||||||
|
|
||||||
export const useSavedCollections = (collectionAtom: CollectionsCRUDAtom) => {
|
export const useSavedCollections = (collectionService: CollectionService) => {
|
||||||
const [{ collections, addCollection, deleteCollection, updateCollection }] =
|
|
||||||
useAtom(collectionAtom);
|
|
||||||
const addPage = useCallback(
|
const addPage = useCallback(
|
||||||
(collectionId: string, pageId: string) => {
|
(collectionId: string, pageId: string) => {
|
||||||
updateCollection(collectionId, old => {
|
collectionService.updateCollection(collectionId, old => {
|
||||||
return {
|
return {
|
||||||
...old,
|
...old,
|
||||||
allowList: [pageId, ...(old.allowList ?? [])],
|
allowList: [pageId, ...(old.allowList ?? [])],
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[updateCollection]
|
[collectionService]
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
collections,
|
collectionService,
|
||||||
addCollection,
|
|
||||||
updateCollection,
|
|
||||||
deleteCollection,
|
|
||||||
addPage,
|
addPage,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useCollectionManager = (collectionsAtom: CollectionsCRUDAtom) => {
|
export const useCollectionManager = (collectionService: CollectionService) => {
|
||||||
const {
|
const collections = useLiveData(collectionService.collections);
|
||||||
collections,
|
const { addPage } = useSavedCollections(collectionService);
|
||||||
updateCollection,
|
|
||||||
addCollection,
|
|
||||||
deleteCollection,
|
|
||||||
addPage,
|
|
||||||
} = useSavedCollections(collectionsAtom);
|
|
||||||
const currentCollectionId = useAtomValue(currentCollectionAtom);
|
const currentCollectionId = useAtomValue(currentCollectionAtom);
|
||||||
const [defaultCollection, updateDefaultCollection] = useAtom(
|
const [defaultCollection, updateDefaultCollection] = useAtom(
|
||||||
defaultCollectionAtom
|
defaultCollectionAtom
|
||||||
@ -82,10 +60,10 @@ export const useCollectionManager = (collectionsAtom: CollectionsCRUDAtom) => {
|
|||||||
if (collection.id === NIL) {
|
if (collection.id === NIL) {
|
||||||
updateDefaultCollection(collection);
|
updateDefaultCollection(collection);
|
||||||
} else {
|
} else {
|
||||||
updateCollection(collection.id, () => collection);
|
collectionService.updateCollection(collection.id, () => collection);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[updateDefaultCollection, updateCollection]
|
[updateDefaultCollection, collectionService]
|
||||||
);
|
);
|
||||||
const setTemporaryFilter = useCallback(
|
const setTemporaryFilter = useCallback(
|
||||||
(filterList: Filter[]) => {
|
(filterList: Filter[]) => {
|
||||||
@ -108,9 +86,10 @@ export const useCollectionManager = (collectionsAtom: CollectionsCRUDAtom) => {
|
|||||||
isDefault: currentCollectionId === NIL,
|
isDefault: currentCollectionId === NIL,
|
||||||
|
|
||||||
// actions
|
// actions
|
||||||
createCollection: addCollection,
|
createCollection: collectionService.addCollection.bind(collectionService),
|
||||||
updateCollection: update,
|
updateCollection: update,
|
||||||
deleteCollection,
|
deleteCollection:
|
||||||
|
collectionService.deleteCollection.bind(collectionService),
|
||||||
addPage,
|
addPage,
|
||||||
setTemporaryFilter,
|
setTemporaryFilter,
|
||||||
};
|
};
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import { allPageModeSelectAtom } from '@affine/core/atoms';
|
import { allPageModeSelectAtom } from '@affine/core/atoms';
|
||||||
import { collectionsCRUDAtom } from '@affine/core/atoms/collections';
|
|
||||||
import { usePageHelper } from '@affine/core/components/blocksuite/block-suite-page-list/utils';
|
import { usePageHelper } from '@affine/core/components/blocksuite/block-suite-page-list/utils';
|
||||||
|
import { CollectionService } from '@affine/core/modules/collection';
|
||||||
import type { BlockSuiteWorkspace } from '@affine/core/shared';
|
import type { BlockSuiteWorkspace } from '@affine/core/shared';
|
||||||
import type { PageMeta } from '@blocksuite/store';
|
import type { PageMeta } from '@blocksuite/store';
|
||||||
|
import { useService } from '@toeverything/infra/di';
|
||||||
import { useAtomValue } from 'jotai';
|
import { useAtomValue } from 'jotai';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
@ -19,8 +20,9 @@ export const useFilteredPageMetas = (
|
|||||||
) => {
|
) => {
|
||||||
const { isPreferredEdgeless } = usePageHelper(workspace);
|
const { isPreferredEdgeless } = usePageHelper(workspace);
|
||||||
const pageMode = useAtomValue(allPageModeSelectAtom);
|
const pageMode = useAtomValue(allPageModeSelectAtom);
|
||||||
const { currentCollection, isDefault } =
|
const { currentCollection, isDefault } = useCollectionManager(
|
||||||
useCollectionManager(collectionsCRUDAtom);
|
useService(CollectionService)
|
||||||
|
);
|
||||||
|
|
||||||
const filteredPageMetas = useMemo(
|
const filteredPageMetas = useMemo(
|
||||||
() =>
|
() =>
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { Button, Tooltip } from '@affine/component';
|
import { Button, Tooltip } from '@affine/component';
|
||||||
|
import type { CollectionService } from '@affine/core/modules/collection';
|
||||||
import type { DeleteCollectionInfo, PropertiesMeta } from '@affine/env/filter';
|
import type { DeleteCollectionInfo, PropertiesMeta } from '@affine/env/filter';
|
||||||
import type { GetPageInfoById } from '@affine/env/page-info';
|
import type { GetPageInfoById } from '@affine/env/page-info';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
@ -6,10 +7,7 @@ import { ViewLayersIcon } from '@blocksuite/icons';
|
|||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import {
|
import { useCollectionManager } from '../use-collection-manager';
|
||||||
type CollectionsCRUDAtom,
|
|
||||||
useCollectionManager,
|
|
||||||
} from '../use-collection-manager';
|
|
||||||
import * as styles from './collection-bar.css';
|
import * as styles from './collection-bar.css';
|
||||||
import {
|
import {
|
||||||
type AllPageListConfig,
|
type AllPageListConfig,
|
||||||
@ -20,16 +18,16 @@ import { useActions } from './use-action';
|
|||||||
interface CollectionBarProps {
|
interface CollectionBarProps {
|
||||||
getPageInfo: GetPageInfoById;
|
getPageInfo: GetPageInfoById;
|
||||||
propertiesMeta: PropertiesMeta;
|
propertiesMeta: PropertiesMeta;
|
||||||
collectionsAtom: CollectionsCRUDAtom;
|
collectionService: CollectionService;
|
||||||
backToAll: () => void;
|
backToAll: () => void;
|
||||||
allPageListConfig: AllPageListConfig;
|
allPageListConfig: AllPageListConfig;
|
||||||
info: DeleteCollectionInfo;
|
info: DeleteCollectionInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CollectionBar = (props: CollectionBarProps) => {
|
export const CollectionBar = (props: CollectionBarProps) => {
|
||||||
const { collectionsAtom } = props;
|
const { collectionService } = props;
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
const setting = useCollectionManager(collectionsAtom);
|
const setting = useCollectionManager(collectionService);
|
||||||
const collection = setting.currentCollection;
|
const collection = setting.currentCollection;
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const actions = useActions({
|
const actions = useActions({
|
||||||
|
@ -3,15 +3,11 @@ import {
|
|||||||
useBlockSuitePageMeta,
|
useBlockSuitePageMeta,
|
||||||
usePageMetaHelper,
|
usePageMetaHelper,
|
||||||
} from '@affine/core/hooks/use-block-suite-page-meta';
|
} from '@affine/core/hooks/use-block-suite-page-meta';
|
||||||
import {
|
|
||||||
currentWorkspaceAtom,
|
|
||||||
waitForCurrentWorkspaceAtom,
|
|
||||||
} from '@affine/core/modules/workspace';
|
|
||||||
import { WorkspaceSubPath } from '@affine/core/shared';
|
|
||||||
import type { Collection } from '@affine/env/filter';
|
import type { Collection } from '@affine/env/filter';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { EdgelessIcon, PageIcon, ViewLayersIcon } from '@blocksuite/icons';
|
import { EdgelessIcon, PageIcon, ViewLayersIcon } from '@blocksuite/icons';
|
||||||
import type { Page, PageMeta } from '@blocksuite/store';
|
import type { PageMeta } from '@blocksuite/store';
|
||||||
|
import { Workspace } from '@toeverything/infra';
|
||||||
import { getCurrentStore } from '@toeverything/infra/atom';
|
import { getCurrentStore } from '@toeverything/infra/atom';
|
||||||
import {
|
import {
|
||||||
type AffineCommand,
|
type AffineCommand,
|
||||||
@ -19,19 +15,17 @@ import {
|
|||||||
type CommandCategory,
|
type CommandCategory,
|
||||||
PreconditionStrategy,
|
PreconditionStrategy,
|
||||||
} from '@toeverything/infra/command';
|
} from '@toeverything/infra/command';
|
||||||
|
import { useService } from '@toeverything/infra/di';
|
||||||
import { commandScore } from 'cmdk';
|
import { commandScore } from 'cmdk';
|
||||||
import { atom, useAtomValue } from 'jotai';
|
import { atom, useAtomValue } from 'jotai';
|
||||||
import { groupBy } from 'lodash-es';
|
import { groupBy } from 'lodash-es';
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import {
|
import { pageSettingsAtom, recentPageIdsBaseAtom } from '../../../atoms';
|
||||||
openQuickSearchModalAtom,
|
|
||||||
pageSettingsAtom,
|
|
||||||
recentPageIdsBaseAtom,
|
|
||||||
} from '../../../atoms';
|
|
||||||
import { collectionsCRUDAtom } from '../../../atoms/collections';
|
|
||||||
import { currentPageIdAtom } from '../../../atoms/mode';
|
import { currentPageIdAtom } from '../../../atoms/mode';
|
||||||
import { useNavigateHelper } from '../../../hooks/use-navigate-helper';
|
import { useNavigateHelper } from '../../../hooks/use-navigate-helper';
|
||||||
|
import { CollectionService } from '../../../modules/collection';
|
||||||
|
import { WorkspaceSubPath } from '../../../shared';
|
||||||
import { usePageHelper } from '../../blocksuite/block-suite-page-list/utils';
|
import { usePageHelper } from '../../blocksuite/block-suite-page-list/utils';
|
||||||
import type { CMDKCommand, CommandContext } from './types';
|
import type { CMDKCommand, CommandContext } from './types';
|
||||||
|
|
||||||
@ -47,41 +41,6 @@ export function removeDoubleQuotes(str?: string): string | undefined {
|
|||||||
export const cmdkQueryAtom = atom('');
|
export const cmdkQueryAtom = atom('');
|
||||||
export const cmdkValueAtom = atom('');
|
export const cmdkValueAtom = atom('');
|
||||||
|
|
||||||
// like currentWorkspaceAtom, but not throw error
|
|
||||||
const safeCurrentPageAtom = atom<Promise<Page | undefined>>(async get => {
|
|
||||||
const currentWorkspace = get(currentWorkspaceAtom);
|
|
||||||
if (!currentWorkspace) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentPageId = get(currentPageIdAtom);
|
|
||||||
|
|
||||||
if (!currentPageId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const page = currentWorkspace.blockSuiteWorkspace.getPage(currentPageId);
|
|
||||||
|
|
||||||
if (!page) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!page.loaded) {
|
|
||||||
await page.waitForLoaded();
|
|
||||||
}
|
|
||||||
return page;
|
|
||||||
});
|
|
||||||
|
|
||||||
export const commandContextAtom = atom<Promise<CommandContext>>(async get => {
|
|
||||||
const currentPage = await get(safeCurrentPageAtom);
|
|
||||||
const pageSettings = get(pageSettingsAtom);
|
|
||||||
|
|
||||||
return {
|
|
||||||
currentPage,
|
|
||||||
pageMode: currentPage ? pageSettings[currentPage.id]?.mode : undefined,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
function filterCommandByContext(
|
function filterCommandByContext(
|
||||||
command: AffineCommand,
|
command: AffineCommand,
|
||||||
context: CommandContext
|
context: CommandContext
|
||||||
@ -96,7 +55,7 @@ function filterCommandByContext(
|
|||||||
return context.pageMode === 'page';
|
return context.pageMode === 'page';
|
||||||
}
|
}
|
||||||
if (command.preconditionStrategy === PreconditionStrategy.InPaperOrEdgeless) {
|
if (command.preconditionStrategy === PreconditionStrategy.InPaperOrEdgeless) {
|
||||||
return !!context.currentPage;
|
return !!context.pageMode;
|
||||||
}
|
}
|
||||||
if (command.preconditionStrategy === PreconditionStrategy.Never) {
|
if (command.preconditionStrategy === PreconditionStrategy.Never) {
|
||||||
return false;
|
return false;
|
||||||
@ -107,27 +66,16 @@ function filterCommandByContext(
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
let quickSearchOpenCounter = 0;
|
function getAllCommand(context: CommandContext) {
|
||||||
const openCountAtom = atom(get => {
|
|
||||||
if (get(openQuickSearchModalAtom)) {
|
|
||||||
quickSearchOpenCounter++;
|
|
||||||
}
|
|
||||||
return quickSearchOpenCounter;
|
|
||||||
});
|
|
||||||
|
|
||||||
export const filteredAffineCommands = atom(async get => {
|
|
||||||
const context = await get(commandContextAtom);
|
|
||||||
// reset when modal open
|
|
||||||
get(openCountAtom);
|
|
||||||
const commands = AffineCommandRegistry.getAll();
|
const commands = AffineCommandRegistry.getAll();
|
||||||
return commands.filter(command => {
|
return commands.filter(command => {
|
||||||
return filterCommandByContext(command, context);
|
return filterCommandByContext(command, context);
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
const useWorkspacePages = () => {
|
const useWorkspacePages = () => {
|
||||||
const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom);
|
const workspace = useService(Workspace);
|
||||||
const pages = useBlockSuitePageMeta(currentWorkspace.blockSuiteWorkspace);
|
const pages = useBlockSuitePageMeta(workspace.blockSuiteWorkspace);
|
||||||
return pages;
|
return pages;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -153,6 +101,7 @@ export const pageToCommand = (
|
|||||||
store: ReturnType<typeof getCurrentStore>,
|
store: ReturnType<typeof getCurrentStore>,
|
||||||
navigationHelper: ReturnType<typeof useNavigateHelper>,
|
navigationHelper: ReturnType<typeof useNavigateHelper>,
|
||||||
t: ReturnType<typeof useAFFiNEI18N>,
|
t: ReturnType<typeof useAFFiNEI18N>,
|
||||||
|
workspace: Workspace,
|
||||||
label?: {
|
label?: {
|
||||||
title: string;
|
title: string;
|
||||||
subTitle?: string;
|
subTitle?: string;
|
||||||
@ -160,7 +109,6 @@ export const pageToCommand = (
|
|||||||
blockId?: string
|
blockId?: string
|
||||||
): CMDKCommand => {
|
): CMDKCommand => {
|
||||||
const pageMode = store.get(pageSettingsAtom)?.[page.id]?.mode;
|
const pageMode = store.get(pageSettingsAtom)?.[page.id]?.mode;
|
||||||
const currentWorkspace = store.get(currentWorkspaceAtom);
|
|
||||||
|
|
||||||
const title = page.title || t['Untitled']();
|
const title = page.title || t['Untitled']();
|
||||||
const commandLabel = label || {
|
const commandLabel = label || {
|
||||||
@ -186,18 +134,14 @@ export const pageToCommand = (
|
|||||||
originalValue: title,
|
originalValue: title,
|
||||||
category: category,
|
category: category,
|
||||||
run: () => {
|
run: () => {
|
||||||
if (!currentWorkspace) {
|
if (!workspace) {
|
||||||
console.error('current workspace not found');
|
console.error('current workspace not found');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (blockId) {
|
if (blockId) {
|
||||||
return navigationHelper.jumpToPageBlock(
|
return navigationHelper.jumpToPageBlock(workspace.id, page.id, blockId);
|
||||||
currentWorkspace.id,
|
|
||||||
page.id,
|
|
||||||
blockId
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return navigationHelper.jumpToPage(currentWorkspace.id, page.id);
|
return navigationHelper.jumpToPage(workspace.id, page.id);
|
||||||
},
|
},
|
||||||
icon: pageMode === 'edgeless' ? <EdgelessIcon /> : <PageIcon />,
|
icon: pageMode === 'edgeless' ? <EdgelessIcon /> : <PageIcon />,
|
||||||
timestamp: page.updatedDate,
|
timestamp: page.updatedDate,
|
||||||
@ -212,7 +156,7 @@ export const usePageCommands = () => {
|
|||||||
const recentPages = useRecentPages();
|
const recentPages = useRecentPages();
|
||||||
const pages = useWorkspacePages();
|
const pages = useWorkspacePages();
|
||||||
const store = getCurrentStore();
|
const store = getCurrentStore();
|
||||||
const workspace = useAtomValue(waitForCurrentWorkspaceAtom);
|
const workspace = useService(Workspace);
|
||||||
const pageHelper = usePageHelper(workspace.blockSuiteWorkspace);
|
const pageHelper = usePageHelper(workspace.blockSuiteWorkspace);
|
||||||
const pageMetaHelper = usePageMetaHelper(workspace.blockSuiteWorkspace);
|
const pageMetaHelper = usePageMetaHelper(workspace.blockSuiteWorkspace);
|
||||||
const query = useAtomValue(cmdkQueryAtom);
|
const query = useAtomValue(cmdkQueryAtom);
|
||||||
@ -241,7 +185,14 @@ export const usePageCommands = () => {
|
|||||||
let results: CMDKCommand[] = [];
|
let results: CMDKCommand[] = [];
|
||||||
if (query.trim() === '') {
|
if (query.trim() === '') {
|
||||||
results = recentPages.map(page => {
|
results = recentPages.map(page => {
|
||||||
return pageToCommand('affine:recent', page, store, navigationHelper, t);
|
return pageToCommand(
|
||||||
|
'affine:recent',
|
||||||
|
page,
|
||||||
|
store,
|
||||||
|
navigationHelper,
|
||||||
|
t,
|
||||||
|
workspace
|
||||||
|
);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// queried pages that has matched contents
|
// queried pages that has matched contents
|
||||||
@ -283,6 +234,7 @@ export const usePageCommands = () => {
|
|||||||
store,
|
store,
|
||||||
navigationHelper,
|
navigationHelper,
|
||||||
t,
|
t,
|
||||||
|
workspace,
|
||||||
label,
|
label,
|
||||||
blockId
|
blockId
|
||||||
);
|
);
|
||||||
@ -334,27 +286,26 @@ export const usePageCommands = () => {
|
|||||||
}
|
}
|
||||||
return results;
|
return results;
|
||||||
}, [
|
}, [
|
||||||
pageHelper,
|
searchTime,
|
||||||
pageMetaHelper,
|
|
||||||
navigationHelper,
|
|
||||||
pages,
|
|
||||||
query,
|
query,
|
||||||
recentPages,
|
recentPages,
|
||||||
store,
|
store,
|
||||||
|
navigationHelper,
|
||||||
t,
|
t,
|
||||||
workspace.blockSuiteWorkspace,
|
workspace,
|
||||||
searchTime,
|
pages,
|
||||||
|
pageHelper,
|
||||||
|
pageMetaHelper,
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const collectionToCommand = (
|
export const collectionToCommand = (
|
||||||
collection: Collection,
|
collection: Collection,
|
||||||
store: ReturnType<typeof getCurrentStore>,
|
|
||||||
navigationHelper: ReturnType<typeof useNavigateHelper>,
|
navigationHelper: ReturnType<typeof useNavigateHelper>,
|
||||||
selectCollection: (id: string) => void,
|
selectCollection: (id: string) => void,
|
||||||
t: ReturnType<typeof useAFFiNEI18N>
|
t: ReturnType<typeof useAFFiNEI18N>,
|
||||||
|
workspace: Workspace
|
||||||
): CMDKCommand => {
|
): CMDKCommand => {
|
||||||
const currentWorkspace = store.get(currentWorkspaceAtom);
|
|
||||||
const label = collection.name || t['Untitled']();
|
const label = collection.name || t['Untitled']();
|
||||||
const category = 'affine:collections';
|
const category = 'affine:collections';
|
||||||
return {
|
return {
|
||||||
@ -372,11 +323,7 @@ export const collectionToCommand = (
|
|||||||
originalValue: label,
|
originalValue: label,
|
||||||
category: category,
|
category: category,
|
||||||
run: () => {
|
run: () => {
|
||||||
if (!currentWorkspace) {
|
navigationHelper.jumpToSubPath(workspace.id, WorkspaceSubPath.ALL);
|
||||||
console.error('current workspace not found');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
navigationHelper.jumpToSubPath(currentWorkspace.id, WorkspaceSubPath.ALL);
|
|
||||||
selectCollection(collection.id);
|
selectCollection(collection.id);
|
||||||
},
|
},
|
||||||
icon: <ViewLayersIcon />,
|
icon: <ViewLayersIcon />,
|
||||||
@ -385,12 +332,13 @@ export const collectionToCommand = (
|
|||||||
|
|
||||||
export const useCollectionsCommands = () => {
|
export const useCollectionsCommands = () => {
|
||||||
// todo: considering collections for searching pages
|
// todo: considering collections for searching pages
|
||||||
const { savedCollections } = useCollectionManager(collectionsCRUDAtom);
|
const { savedCollections } = useCollectionManager(
|
||||||
const store = getCurrentStore();
|
useService(CollectionService)
|
||||||
|
);
|
||||||
const query = useAtomValue(cmdkQueryAtom);
|
const query = useAtomValue(cmdkQueryAtom);
|
||||||
const navigationHelper = useNavigateHelper();
|
const navigationHelper = useNavigateHelper();
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
const workspace = useAtomValue(waitForCurrentWorkspaceAtom);
|
const workspace = useService(Workspace);
|
||||||
const selectCollection = useCallback(
|
const selectCollection = useCallback(
|
||||||
(id: string) => {
|
(id: string) => {
|
||||||
navigationHelper.jumpToCollection(workspace.id, id);
|
navigationHelper.jumpToCollection(workspace.id, id);
|
||||||
@ -405,22 +353,39 @@ export const useCollectionsCommands = () => {
|
|||||||
results = savedCollections.map(collection => {
|
results = savedCollections.map(collection => {
|
||||||
const command = collectionToCommand(
|
const command = collectionToCommand(
|
||||||
collection,
|
collection,
|
||||||
store,
|
|
||||||
navigationHelper,
|
navigationHelper,
|
||||||
selectCollection,
|
selectCollection,
|
||||||
t
|
t,
|
||||||
|
workspace
|
||||||
);
|
);
|
||||||
return command;
|
return command;
|
||||||
});
|
});
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
}, [query, savedCollections, store, navigationHelper, selectCollection, t]);
|
}, [
|
||||||
|
query,
|
||||||
|
savedCollections,
|
||||||
|
navigationHelper,
|
||||||
|
selectCollection,
|
||||||
|
t,
|
||||||
|
workspace,
|
||||||
|
]);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useCMDKCommandGroups = () => {
|
export const useCMDKCommandGroups = () => {
|
||||||
const pageCommands = usePageCommands();
|
const pageCommands = usePageCommands();
|
||||||
const collectionCommands = useCollectionsCommands();
|
const collectionCommands = useCollectionsCommands();
|
||||||
const affineCommands = useAtomValue(filteredAffineCommands);
|
|
||||||
|
const currentPageId = useAtomValue(currentPageIdAtom);
|
||||||
|
const pageSettings = useAtomValue(pageSettingsAtom);
|
||||||
|
const currentPageMode = currentPageId
|
||||||
|
? pageSettings[currentPageId]?.mode
|
||||||
|
: undefined;
|
||||||
|
const affineCommands = useMemo(() => {
|
||||||
|
return getAllCommand({
|
||||||
|
pageMode: currentPageMode,
|
||||||
|
});
|
||||||
|
}, [currentPageMode]);
|
||||||
|
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
const commands = [
|
const commands = [
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
import type { Page } from '@blocksuite/store';
|
|
||||||
import type { CommandCategory } from '@toeverything/infra/command';
|
import type { CommandCategory } from '@toeverything/infra/command';
|
||||||
|
|
||||||
export interface CommandContext {
|
export interface CommandContext {
|
||||||
currentPage: Page | undefined;
|
|
||||||
pageMode: 'page' | 'edgeless' | undefined;
|
pageMode: 'page' | 'edgeless' | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,22 +2,25 @@ import { Button } from '@affine/component/ui/button';
|
|||||||
import { ConfirmModal } from '@affine/component/ui/modal';
|
import { ConfirmModal } from '@affine/component/ui/modal';
|
||||||
import { Tooltip } from '@affine/component/ui/tooltip';
|
import { Tooltip } from '@affine/component/ui/tooltip';
|
||||||
import { useBlockSuitePageMeta } from '@affine/core/hooks/use-block-suite-page-meta';
|
import { useBlockSuitePageMeta } from '@affine/core/hooks/use-block-suite-page-meta';
|
||||||
import { waitForCurrentWorkspaceAtom } from '@affine/core/modules/workspace';
|
|
||||||
import { WorkspaceSubPath } from '@affine/core/shared';
|
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { assertExists } from '@blocksuite/global/utils';
|
import { assertExists } from '@blocksuite/global/utils';
|
||||||
import { DeleteIcon, ResetIcon } from '@blocksuite/icons';
|
import { DeleteIcon, ResetIcon } from '@blocksuite/icons';
|
||||||
import { useAtomValue } from 'jotai';
|
import { useService } from '@toeverything/infra/di';
|
||||||
|
import { useLiveData } from '@toeverything/infra/livedata';
|
||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
|
|
||||||
import { useAppSettingHelper } from '../../../hooks/affine/use-app-setting-helper';
|
import { useAppSettingHelper } from '../../../hooks/affine/use-app-setting-helper';
|
||||||
import { useBlockSuiteMetaHelper } from '../../../hooks/affine/use-block-suite-meta-helper';
|
import { useBlockSuiteMetaHelper } from '../../../hooks/affine/use-block-suite-meta-helper';
|
||||||
import { useNavigateHelper } from '../../../hooks/use-navigate-helper';
|
import { useNavigateHelper } from '../../../hooks/use-navigate-helper';
|
||||||
|
import { CurrentWorkspaceService } from '../../../modules/workspace/current-workspace';
|
||||||
|
import { WorkspaceSubPath } from '../../../shared';
|
||||||
import { toast } from '../../../utils';
|
import { toast } from '../../../utils';
|
||||||
import * as styles from './styles.css';
|
import * as styles from './styles.css';
|
||||||
|
|
||||||
export const TrashPageFooter = ({ pageId }: { pageId: string }) => {
|
export const TrashPageFooter = ({ pageId }: { pageId: string }) => {
|
||||||
const workspace = useAtomValue(waitForCurrentWorkspaceAtom);
|
const workspace = useLiveData(
|
||||||
|
useService(CurrentWorkspaceService).currentWorkspace
|
||||||
|
);
|
||||||
assertExists(workspace);
|
assertExists(workspace);
|
||||||
const blockSuiteWorkspace = workspace.blockSuiteWorkspace;
|
const blockSuiteWorkspace = workspace.blockSuiteWorkspace;
|
||||||
const pageMeta = useBlockSuitePageMeta(blockSuiteWorkspace).find(
|
const pageMeta = useBlockSuitePageMeta(blockSuiteWorkspace).find(
|
||||||
|
@ -7,21 +7,22 @@ import {
|
|||||||
filterPage,
|
filterPage,
|
||||||
stopPropagation,
|
stopPropagation,
|
||||||
useCollectionManager,
|
useCollectionManager,
|
||||||
useSavedCollections,
|
|
||||||
} from '@affine/core/components/page-list';
|
} from '@affine/core/components/page-list';
|
||||||
import { useBlockSuitePageMeta } from '@affine/core/hooks/use-block-suite-page-meta';
|
import { CollectionService } from '@affine/core/modules/collection';
|
||||||
import type { Collection, DeleteCollectionInfo } from '@affine/env/filter';
|
import type { Collection, DeleteCollectionInfo } from '@affine/env/filter';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { MoreHorizontalIcon, ViewLayersIcon } from '@blocksuite/icons';
|
import { MoreHorizontalIcon, ViewLayersIcon } from '@blocksuite/icons';
|
||||||
import type { PageMeta, Workspace } from '@blocksuite/store';
|
import type { PageMeta, Workspace } from '@blocksuite/store';
|
||||||
import { useDroppable } from '@dnd-kit/core';
|
import { useDroppable } from '@dnd-kit/core';
|
||||||
import * as Collapsible from '@radix-ui/react-collapsible';
|
import * as Collapsible from '@radix-ui/react-collapsible';
|
||||||
|
import { useService } from '@toeverything/infra';
|
||||||
|
import { useLiveData } from '@toeverything/infra/livedata';
|
||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
import { collectionsCRUDAtom } from '../../../../atoms/collections';
|
|
||||||
import { useAllPageListConfig } from '../../../../hooks/affine/use-all-page-list-config';
|
import { useAllPageListConfig } from '../../../../hooks/affine/use-all-page-list-config';
|
||||||
import { getDropItemId } from '../../../../hooks/affine/use-sidebar-drag';
|
import { getDropItemId } from '../../../../hooks/affine/use-sidebar-drag';
|
||||||
|
import { useBlockSuitePageMeta } from '../../../../hooks/use-block-suite-page-meta';
|
||||||
import type { CollectionsListProps } from '../index';
|
import type { CollectionsListProps } from '../index';
|
||||||
import { Page } from './page';
|
import { Page } from './page';
|
||||||
import * as styles from './styles.css';
|
import * as styles from './styles.css';
|
||||||
@ -39,7 +40,7 @@ const CollectionRenderer = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const [collapsed, setCollapsed] = useState(true);
|
const [collapsed, setCollapsed] = useState(true);
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const setting = useCollectionManager(collectionsCRUDAtom);
|
const setting = useCollectionManager(useService(CollectionService));
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
const dragItemId = getDropItemId('collections', collection.id);
|
const dragItemId = getDropItemId('collections', collection.id);
|
||||||
|
|
||||||
@ -168,7 +169,7 @@ export const CollectionsList = ({
|
|||||||
onCreate,
|
onCreate,
|
||||||
}: CollectionsListProps) => {
|
}: CollectionsListProps) => {
|
||||||
const metas = useBlockSuitePageMeta(workspace);
|
const metas = useBlockSuitePageMeta(workspace);
|
||||||
const { collections } = useSavedCollections(collectionsCRUDAtom);
|
const collections = useLiveData(useService(CollectionService).collections);
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
if (collections.length === 0) {
|
if (collections.length === 0) {
|
||||||
return (
|
return (
|
||||||
|
@ -1,13 +1,12 @@
|
|||||||
import { Divider } from '@affine/component/ui/divider';
|
import { Divider } from '@affine/component/ui/divider';
|
||||||
import { MenuItem } from '@affine/component/ui/menu';
|
import { MenuItem } from '@affine/component/ui/menu';
|
||||||
import {
|
|
||||||
workspaceListAtom,
|
|
||||||
workspaceManagerAtom,
|
|
||||||
} from '@affine/core/modules/workspace';
|
|
||||||
import { Unreachable } from '@affine/env/constant';
|
import { Unreachable } from '@affine/env/constant';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { Logo1Icon } from '@blocksuite/icons';
|
import { Logo1Icon } from '@blocksuite/icons';
|
||||||
import { useAtomValue, useSetAtom } from 'jotai';
|
import { WorkspaceManager } from '@toeverything/infra';
|
||||||
|
import { useService } from '@toeverything/infra/di';
|
||||||
|
import { useLiveData } from '@toeverything/infra/livedata';
|
||||||
|
import { useSetAtom } from 'jotai';
|
||||||
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
||||||
import { useSession } from 'next-auth/react';
|
import { useSession } from 'next-auth/react';
|
||||||
import { useCallback, useEffect, useMemo } from 'react';
|
import { useCallback, useEffect, useMemo } from 'react';
|
||||||
@ -85,9 +84,8 @@ export const UserWithWorkspaceList = ({
|
|||||||
onEventEnd?.();
|
onEventEnd?.();
|
||||||
}, [onEventEnd, setOpenCreateWorkspaceModal]);
|
}, [onEventEnd, setOpenCreateWorkspaceModal]);
|
||||||
|
|
||||||
const workspaces = useAtomValue(workspaceListAtom);
|
const workspaceManager = useService(WorkspaceManager);
|
||||||
|
const workspaces = useLiveData(workspaceManager.list.workspaceList);
|
||||||
const workspaceManager = useAtomValue(workspaceManagerAtom);
|
|
||||||
|
|
||||||
// revalidate workspace list when mounted
|
// revalidate workspace list when mounted
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -5,16 +5,13 @@ import {
|
|||||||
useWorkspaceAvatar,
|
useWorkspaceAvatar,
|
||||||
useWorkspaceName,
|
useWorkspaceName,
|
||||||
} from '@affine/core/hooks/use-workspace-info';
|
} from '@affine/core/hooks/use-workspace-info';
|
||||||
import {
|
|
||||||
currentWorkspaceAtom,
|
|
||||||
workspaceListAtom,
|
|
||||||
} from '@affine/core/modules/workspace';
|
|
||||||
import { WorkspaceSubPath } from '@affine/core/shared';
|
|
||||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import type { WorkspaceMetadata } from '@affine/workspace';
|
|
||||||
import type { DragEndEvent } from '@dnd-kit/core';
|
import type { DragEndEvent } from '@dnd-kit/core';
|
||||||
import { useAtomValue, useSetAtom } from 'jotai';
|
import { WorkspaceManager, type WorkspaceMetadata } from '@toeverything/infra';
|
||||||
|
import { useService } from '@toeverything/infra/di';
|
||||||
|
import { useLiveData } from '@toeverything/infra/livedata';
|
||||||
|
import { useSetAtom } from 'jotai';
|
||||||
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
||||||
import { useSession } from 'next-auth/react';
|
import { useSession } from 'next-auth/react';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
@ -23,6 +20,8 @@ import {
|
|||||||
openCreateWorkspaceModalAtom,
|
openCreateWorkspaceModalAtom,
|
||||||
openSettingModalAtom,
|
openSettingModalAtom,
|
||||||
} from '../../../../../atoms';
|
} from '../../../../../atoms';
|
||||||
|
import { CurrentWorkspaceService } from '../../../../../modules/workspace/current-workspace';
|
||||||
|
import { WorkspaceSubPath } from '../../../../../shared';
|
||||||
import { useIsWorkspaceOwner } from '../.././../../../hooks/affine/use-is-workspace-owner';
|
import { useIsWorkspaceOwner } from '../.././../../../hooks/affine/use-is-workspace-owner';
|
||||||
import { useNavigateHelper } from '../.././../../../hooks/use-navigate-helper';
|
import { useNavigateHelper } from '../.././../../../hooks/use-navigate-helper';
|
||||||
import * as styles from './index.css';
|
import * as styles from './index.css';
|
||||||
@ -106,13 +105,17 @@ export const AFFiNEWorkspaceList = ({
|
|||||||
}: {
|
}: {
|
||||||
onEventEnd?: () => void;
|
onEventEnd?: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
const workspaces = useAtomValue(workspaceListAtom);
|
const workspaces = useLiveData(
|
||||||
|
useService(WorkspaceManager).list.workspaceList
|
||||||
|
);
|
||||||
|
|
||||||
const setOpenCreateWorkspaceModal = useSetAtom(openCreateWorkspaceModalAtom);
|
const setOpenCreateWorkspaceModal = useSetAtom(openCreateWorkspaceModalAtom);
|
||||||
|
|
||||||
const { jumpToSubPath } = useNavigateHelper();
|
const { jumpToSubPath } = useNavigateHelper();
|
||||||
|
|
||||||
const currentWorkspace = useAtomValue(currentWorkspaceAtom);
|
const currentWorkspace = useLiveData(
|
||||||
|
useService(CurrentWorkspaceService).currentWorkspace
|
||||||
|
);
|
||||||
|
|
||||||
const setOpenSettingModalAtom = useSetAtom(openSettingModalAtom);
|
const setOpenSettingModalAtom = useSetAtom(openSettingModalAtom);
|
||||||
|
|
||||||
|
@ -6,11 +6,9 @@ import { openSettingModalAtom } from '@affine/core/atoms';
|
|||||||
import { useIsWorkspaceOwner } from '@affine/core/hooks/affine/use-is-workspace-owner';
|
import { useIsWorkspaceOwner } from '@affine/core/hooks/affine/use-is-workspace-owner';
|
||||||
import { useWorkspaceBlobObjectUrl } from '@affine/core/hooks/use-workspace-blob';
|
import { useWorkspaceBlobObjectUrl } from '@affine/core/hooks/use-workspace-blob';
|
||||||
import { useWorkspaceInfo } from '@affine/core/hooks/use-workspace-info';
|
import { useWorkspaceInfo } from '@affine/core/hooks/use-workspace-info';
|
||||||
import { waitForCurrentWorkspaceAtom } from '@affine/core/modules/workspace';
|
|
||||||
import { UNTITLED_WORKSPACE_NAME } from '@affine/env/constant';
|
import { UNTITLED_WORKSPACE_NAME } from '@affine/env/constant';
|
||||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { type SyncEngineStatus, SyncEngineStep } from '@affine/workspace';
|
|
||||||
import {
|
import {
|
||||||
CloudWorkspaceIcon,
|
CloudWorkspaceIcon,
|
||||||
InformationFillDuotoneIcon,
|
InformationFillDuotoneIcon,
|
||||||
@ -18,7 +16,13 @@ import {
|
|||||||
NoNetworkIcon,
|
NoNetworkIcon,
|
||||||
UnsyncIcon,
|
UnsyncIcon,
|
||||||
} from '@blocksuite/icons';
|
} from '@blocksuite/icons';
|
||||||
import { useAtomValue, useSetAtom } from 'jotai';
|
import {
|
||||||
|
type SyncEngineStatus,
|
||||||
|
SyncEngineStep,
|
||||||
|
Workspace,
|
||||||
|
} from '@toeverything/infra';
|
||||||
|
import { useService } from '@toeverything/infra/di';
|
||||||
|
import { useSetAtom } from 'jotai';
|
||||||
import { debounce, mean } from 'lodash-es';
|
import { debounce, mean } from 'lodash-es';
|
||||||
import {
|
import {
|
||||||
forwardRef,
|
forwardRef,
|
||||||
@ -97,7 +101,7 @@ const useSyncEngineSyncProgress = () => {
|
|||||||
useState<SyncEngineStatus | null>(null);
|
useState<SyncEngineStatus | null>(null);
|
||||||
const [isOverCapacity, setIsOverCapacity] = useState(false);
|
const [isOverCapacity, setIsOverCapacity] = useState(false);
|
||||||
|
|
||||||
const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom);
|
const currentWorkspace = useService(Workspace);
|
||||||
const isOwner = useIsWorkspaceOwner(currentWorkspace.meta);
|
const isOwner = useIsWorkspaceOwner(currentWorkspace.meta);
|
||||||
|
|
||||||
const setSettingModalAtom = useSetAtom(openSettingModalAtom);
|
const setSettingModalAtom = useSetAtom(openSettingModalAtom);
|
||||||
@ -250,7 +254,7 @@ export const WorkspaceCard = forwardRef<
|
|||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
HTMLAttributes<HTMLDivElement>
|
HTMLAttributes<HTMLDivElement>
|
||||||
>(({ ...props }, ref) => {
|
>(({ ...props }, ref) => {
|
||||||
const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom);
|
const currentWorkspace = useService(Workspace);
|
||||||
|
|
||||||
const information = useWorkspaceInfo(currentWorkspace.meta);
|
const information = useWorkspaceInfo(currentWorkspace.meta);
|
||||||
|
|
||||||
|
@ -12,15 +12,14 @@ import {
|
|||||||
SidebarScrollableContainer,
|
SidebarScrollableContainer,
|
||||||
} from '@affine/component/app-sidebar';
|
} from '@affine/component/app-sidebar';
|
||||||
import { Menu } from '@affine/component/ui/menu';
|
import { Menu } from '@affine/component/ui/menu';
|
||||||
import { collectionsCRUDAtom } from '@affine/core/atoms/collections';
|
|
||||||
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||||
import { WorkspaceSubPath } from '@affine/core/shared';
|
import { CollectionService } from '@affine/core/modules/collection';
|
||||||
import { apis, events } from '@affine/electron-api';
|
import { apis, events } from '@affine/electron-api';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import type { Workspace } from '@affine/workspace';
|
|
||||||
import { FolderIcon, SettingsIcon } from '@blocksuite/icons';
|
import { FolderIcon, SettingsIcon } from '@blocksuite/icons';
|
||||||
import { type Page } from '@blocksuite/store';
|
import { type Page } from '@blocksuite/store';
|
||||||
import { useDroppable } from '@dnd-kit/core';
|
import { useDroppable } from '@dnd-kit/core';
|
||||||
|
import { useService, type Workspace } from '@toeverything/infra';
|
||||||
import { useAtom, useAtomValue } from 'jotai';
|
import { useAtom, useAtomValue } from 'jotai';
|
||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
import type { HTMLAttributes, ReactElement } from 'react';
|
import type { HTMLAttributes, ReactElement } from 'react';
|
||||||
@ -35,6 +34,7 @@ import { getDropItemId } from '../../hooks/affine/use-sidebar-drag';
|
|||||||
import { useTrashModalHelper } from '../../hooks/affine/use-trash-modal-helper';
|
import { useTrashModalHelper } from '../../hooks/affine/use-trash-modal-helper';
|
||||||
import { useRegisterBrowserHistoryCommands } from '../../hooks/use-browser-history-commands';
|
import { useRegisterBrowserHistoryCommands } from '../../hooks/use-browser-history-commands';
|
||||||
import { useNavigateHelper } from '../../hooks/use-navigate-helper';
|
import { useNavigateHelper } from '../../hooks/use-navigate-helper';
|
||||||
|
import { WorkspaceSubPath } from '../../shared';
|
||||||
import {
|
import {
|
||||||
createEmptyCollection,
|
createEmptyCollection,
|
||||||
MoveToTrash,
|
MoveToTrash,
|
||||||
@ -177,7 +177,7 @@ export const RootAppSidebar = ({
|
|||||||
useRegisterBrowserHistoryCommands(router.back, router.forward);
|
useRegisterBrowserHistoryCommands(router.back, router.forward);
|
||||||
const userInfo = useDeleteCollectionInfo();
|
const userInfo = useDeleteCollectionInfo();
|
||||||
|
|
||||||
const setting = useCollectionManager(collectionsCRUDAtom);
|
const setting = useCollectionManager(useService(CollectionService));
|
||||||
const { node, open } = useEditCollectionName({
|
const { node, open } = useEditCollectionName({
|
||||||
title: t['com.affine.editCollection.createCollection'](),
|
title: t['com.affine.editCollection.createCollection'](),
|
||||||
showTips: true,
|
showTips: true,
|
||||||
|
@ -1,18 +1,18 @@
|
|||||||
import { BrowserWarning } from '@affine/component/affine-banner';
|
import { BrowserWarning } from '@affine/component/affine-banner';
|
||||||
import { LocalDemoTips } from '@affine/component/affine-banner';
|
import { LocalDemoTips } from '@affine/component/affine-banner';
|
||||||
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||||
import { workspaceManagerAtom } from '@affine/core/modules/workspace';
|
|
||||||
import { WorkspaceSubPath } from '@affine/core/shared';
|
|
||||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||||
import { Trans } from '@affine/i18n';
|
import { Trans } from '@affine/i18n';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import type { Workspace } from '@affine/workspace';
|
import { type Workspace, WorkspaceManager } from '@toeverything/infra';
|
||||||
import { useAtomValue, useSetAtom } from 'jotai';
|
import { useService } from '@toeverything/infra';
|
||||||
|
import { useSetAtom } from 'jotai';
|
||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
|
|
||||||
import { authAtom } from '../atoms';
|
import { authAtom } from '../atoms';
|
||||||
import { useCurrentLoginStatus } from '../hooks/affine/use-current-login-status';
|
import { useCurrentLoginStatus } from '../hooks/affine/use-current-login-status';
|
||||||
import { useNavigateHelper } from '../hooks/use-navigate-helper';
|
import { useNavigateHelper } from '../hooks/use-navigate-helper';
|
||||||
|
import { WorkspaceSubPath } from '../shared';
|
||||||
import { EnableAffineCloudModal } from './affine/enable-affine-cloud-modal';
|
import { EnableAffineCloudModal } from './affine/enable-affine-cloud-modal';
|
||||||
|
|
||||||
const minimumChromeVersion = 106;
|
const minimumChromeVersion = 106;
|
||||||
@ -77,7 +77,7 @@ export const TopTip = ({
|
|||||||
}, [setAuthModal]);
|
}, [setAuthModal]);
|
||||||
|
|
||||||
const { openPage } = useNavigateHelper();
|
const { openPage } = useNavigateHelper();
|
||||||
const workspaceManager = useAtomValue(workspaceManagerAtom);
|
const workspaceManager = useService(WorkspaceManager);
|
||||||
const handleConfirm = useAsyncCallback(async () => {
|
const handleConfirm = useAsyncCallback(async () => {
|
||||||
if (workspace.flavour !== WorkspaceFlavour.LOCAL) {
|
if (workspace.flavour !== WorkspaceFlavour.LOCAL) {
|
||||||
return;
|
return;
|
||||||
|
@ -3,15 +3,12 @@ import { AffineShapeIcon } from '@affine/core/components/page-list'; // TODO: im
|
|||||||
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||||
import { useNavigateHelper } from '@affine/core/hooks/use-navigate-helper';
|
import { useNavigateHelper } from '@affine/core/hooks/use-navigate-helper';
|
||||||
import { useWorkspaceStatus } from '@affine/core/hooks/use-workspace-status';
|
import { useWorkspaceStatus } from '@affine/core/hooks/use-workspace-status';
|
||||||
import {
|
|
||||||
waitForCurrentWorkspaceAtom,
|
|
||||||
workspaceManagerAtom,
|
|
||||||
} from '@affine/core/modules/workspace';
|
|
||||||
import { WorkspaceSubPath } from '@affine/core/shared';
|
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { useAtomValue } from 'jotai';
|
import { Workspace, WorkspaceManager } from '@toeverything/infra';
|
||||||
|
import { useService } from '@toeverything/infra/di';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import { WorkspaceSubPath } from '../../shared';
|
||||||
import * as styles from './upgrade.css';
|
import * as styles from './upgrade.css';
|
||||||
import { ArrowCircleIcon, HeartBreakIcon } from './upgrade-icon';
|
import { ArrowCircleIcon, HeartBreakIcon } from './upgrade-icon';
|
||||||
|
|
||||||
@ -20,8 +17,8 @@ import { ArrowCircleIcon, HeartBreakIcon } from './upgrade-icon';
|
|||||||
*/
|
*/
|
||||||
export const WorkspaceUpgrade = function WorkspaceUpgrade() {
|
export const WorkspaceUpgrade = function WorkspaceUpgrade() {
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom);
|
const currentWorkspace = useService(Workspace);
|
||||||
const workspaceManager = useAtomValue(workspaceManagerAtom);
|
const workspaceManager = useService(WorkspaceManager);
|
||||||
const upgradeStatus = useWorkspaceStatus(currentWorkspace, s => s.upgrade);
|
const upgradeStatus = useWorkspaceStatus(currentWorkspace, s => s.upgrade);
|
||||||
const { openPage } = useNavigateHelper();
|
const { openPage } = useNavigateHelper();
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
@ -32,10 +29,10 @@ export const WorkspaceUpgrade = function WorkspaceUpgrade() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const newWorkspaceId =
|
const newWorkspace =
|
||||||
await currentWorkspace.upgrade.upgrade(workspaceManager);
|
await currentWorkspace.upgrade.upgrade(workspaceManager);
|
||||||
if (newWorkspaceId) {
|
if (newWorkspace) {
|
||||||
openPage(newWorkspaceId, WorkspaceSubPath.ALL);
|
openPage(newWorkspace.id, WorkspaceSubPath.ALL);
|
||||||
} else {
|
} else {
|
||||||
// blocksuite may enter an incorrect state, reload to reset it.
|
// blocksuite may enter an incorrect state, reload to reset it.
|
||||||
location.reload();
|
location.reload();
|
||||||
|
@ -3,101 +3,71 @@
|
|||||||
*/
|
*/
|
||||||
import 'fake-indexeddb/auto';
|
import 'fake-indexeddb/auto';
|
||||||
|
|
||||||
import {
|
import { WorkspacePropertiesAdapter } from '@affine/core/modules/workspace';
|
||||||
currentWorkspaceAtom,
|
|
||||||
WorkspacePropertiesAdapter,
|
|
||||||
} from '@affine/core/modules/workspace';
|
|
||||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
|
||||||
import type { Workspace } from '@affine/workspace/workspace';
|
|
||||||
import { __unstableSchemas, AffineSchemas } from '@blocksuite/blocks/models';
|
|
||||||
import { assertExists } from '@blocksuite/global/utils';
|
|
||||||
import { type Page, Workspace as BlockSuiteWorkspace } from '@blocksuite/store';
|
|
||||||
import { Schema } from '@blocksuite/store';
|
|
||||||
import { render } from '@testing-library/react';
|
import { render } from '@testing-library/react';
|
||||||
|
import { Workspace } from '@toeverything/infra';
|
||||||
|
import { ServiceProviderContext, useService } from '@toeverything/infra/di';
|
||||||
import { createStore, Provider } from 'jotai';
|
import { createStore, Provider } from 'jotai';
|
||||||
import { Suspense } from 'react';
|
import { Suspense } from 'react';
|
||||||
import { describe, expect, test, vi } from 'vitest';
|
import { describe, expect, test, vi } from 'vitest';
|
||||||
import { beforeEach } from 'vitest';
|
import { beforeEach } from 'vitest';
|
||||||
|
|
||||||
|
import { configureTestingEnvironment } from '../../testing';
|
||||||
import { useBlockSuiteWorkspacePageTitle } from '../use-block-suite-workspace-page-title';
|
import { useBlockSuiteWorkspacePageTitle } from '../use-block-suite-workspace-page-title';
|
||||||
|
|
||||||
let blockSuiteWorkspace: BlockSuiteWorkspace;
|
|
||||||
const store = createStore();
|
const store = createStore();
|
||||||
|
|
||||||
const schema = new Schema();
|
|
||||||
schema.register(AffineSchemas).register(__unstableSchemas);
|
|
||||||
|
|
||||||
const Component = () => {
|
const Component = () => {
|
||||||
const title = useBlockSuiteWorkspacePageTitle(blockSuiteWorkspace, 'page0');
|
const workspace = useService(Workspace);
|
||||||
|
const title = useBlockSuiteWorkspacePageTitle(
|
||||||
|
workspace.blockSuiteWorkspace,
|
||||||
|
'page0'
|
||||||
|
);
|
||||||
return <div>title: {title}</div>;
|
return <div>title: {title}</div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
// todo: this module has some side-effects that will break the tests
|
|
||||||
vi.mock('@affine/workspace-impl', () => ({
|
|
||||||
default: {},
|
|
||||||
}));
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
vi.useFakeTimers({ toFake: ['requestIdleCallback'] });
|
vi.useFakeTimers({ toFake: ['requestIdleCallback'] });
|
||||||
|
|
||||||
blockSuiteWorkspace = new BlockSuiteWorkspace({ id: 'test', schema });
|
|
||||||
|
|
||||||
const workspace = {
|
|
||||||
blockSuiteWorkspace,
|
|
||||||
flavour: WorkspaceFlavour.LOCAL,
|
|
||||||
} as Workspace;
|
|
||||||
|
|
||||||
store.set(currentWorkspaceAtom, workspace);
|
|
||||||
|
|
||||||
blockSuiteWorkspace = workspace.blockSuiteWorkspace;
|
|
||||||
|
|
||||||
blockSuiteWorkspace.doc.emit('sync', []);
|
|
||||||
|
|
||||||
const initPage = async (page: Page) => {
|
|
||||||
await page.waitForLoaded();
|
|
||||||
expect(page).not.toBeNull();
|
|
||||||
assertExists(page);
|
|
||||||
const pageBlockId = page.addBlock('affine:page', {
|
|
||||||
title: new page.Text(''),
|
|
||||||
});
|
|
||||||
const frameId = page.addBlock('affine:note', {}, pageBlockId);
|
|
||||||
page.addBlock('affine:paragraph', {}, frameId);
|
|
||||||
};
|
|
||||||
await initPage(blockSuiteWorkspace.createPage({ id: 'page0' }));
|
|
||||||
await initPage(blockSuiteWorkspace.createPage({ id: 'page1' }));
|
|
||||||
await initPage(blockSuiteWorkspace.createPage({ id: 'page2' }));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('useBlockSuiteWorkspacePageTitle', () => {
|
describe('useBlockSuiteWorkspacePageTitle', () => {
|
||||||
test('basic', async () => {
|
test('basic', async () => {
|
||||||
|
const { workspace, page } = await configureTestingEnvironment();
|
||||||
const { findByText, rerender } = render(
|
const { findByText, rerender } = render(
|
||||||
<Provider store={store}>
|
<ServiceProviderContext.Provider value={page.services}>
|
||||||
<Suspense fallback="loading">
|
<Provider store={store}>
|
||||||
<Component />
|
<Suspense fallback="loading">
|
||||||
</Suspense>
|
<Component />
|
||||||
</Provider>
|
</Suspense>
|
||||||
|
</Provider>
|
||||||
|
</ServiceProviderContext.Provider>
|
||||||
);
|
);
|
||||||
expect(await findByText('title: Untitled')).toBeDefined();
|
expect(await findByText('title: Untitled')).toBeDefined();
|
||||||
blockSuiteWorkspace.setPageMeta('page0', { title: '1' });
|
workspace.blockSuiteWorkspace.setPageMeta(page.id, { title: '1' });
|
||||||
rerender(
|
rerender(
|
||||||
<Provider store={store}>
|
<ServiceProviderContext.Provider value={page.services}>
|
||||||
<Suspense fallback="loading">
|
<Provider store={store}>
|
||||||
<Component />
|
<Suspense fallback="loading">
|
||||||
</Suspense>
|
<Component />
|
||||||
</Provider>
|
</Suspense>
|
||||||
|
</Provider>
|
||||||
|
</ServiceProviderContext.Provider>
|
||||||
);
|
);
|
||||||
expect(await findByText('title: 1')).toBeDefined();
|
expect(await findByText('title: 1')).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('journal', async () => {
|
test('journal', async () => {
|
||||||
const adapter = new WorkspacePropertiesAdapter(blockSuiteWorkspace);
|
const { workspace, page } = await configureTestingEnvironment();
|
||||||
adapter.setJournalPageDateString('page0', '2021-01-01');
|
const adapter = workspace.services.get(WorkspacePropertiesAdapter);
|
||||||
|
adapter.setJournalPageDateString(page.id, '2021-01-01');
|
||||||
const { findByText } = render(
|
const { findByText } = render(
|
||||||
<Provider store={store}>
|
<ServiceProviderContext.Provider value={page.services}>
|
||||||
<Suspense fallback="loading">
|
<Provider store={store}>
|
||||||
<Component />
|
<Suspense fallback="loading">
|
||||||
</Suspense>
|
<Component />
|
||||||
</Provider>
|
</Suspense>
|
||||||
|
</Provider>
|
||||||
|
</ServiceProviderContext.Provider>
|
||||||
);
|
);
|
||||||
expect(await findByText('title: Jan 1, 2021')).toBeDefined();
|
expect(await findByText('title: Jan 1, 2021')).toBeDefined();
|
||||||
});
|
});
|
||||||
|
@ -4,17 +4,17 @@ import {
|
|||||||
FavoriteTag,
|
FavoriteTag,
|
||||||
} from '@affine/core/components/page-list';
|
} from '@affine/core/components/page-list';
|
||||||
import { useBlockSuitePageMeta } from '@affine/core/hooks/use-block-suite-page-meta';
|
import { useBlockSuitePageMeta } from '@affine/core/hooks/use-block-suite-page-meta';
|
||||||
import { waitForCurrentWorkspaceAtom } from '@affine/core/modules/workspace';
|
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import type { PageMeta } from '@blocksuite/store';
|
import type { PageMeta } from '@blocksuite/store';
|
||||||
import { useAtomValue } from 'jotai';
|
import { Workspace } from '@toeverything/infra';
|
||||||
|
import { useService } from '@toeverything/infra/di';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
import { usePageHelper } from '../../components/blocksuite/block-suite-page-list/utils';
|
import { usePageHelper } from '../../components/blocksuite/block-suite-page-list/utils';
|
||||||
import { useBlockSuiteMetaHelper } from './use-block-suite-meta-helper';
|
import { useBlockSuiteMetaHelper } from './use-block-suite-meta-helper';
|
||||||
|
|
||||||
export const useAllPageListConfig = () => {
|
export const useAllPageListConfig = () => {
|
||||||
const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom);
|
const currentWorkspace = useService(Workspace);
|
||||||
const workspace = currentWorkspace.blockSuiteWorkspace;
|
const workspace = currentWorkspace.blockSuiteWorkspace;
|
||||||
const pageMetas = useBlockSuitePageMeta(workspace);
|
const pageMetas = useBlockSuitePageMeta(workspace);
|
||||||
const { isPreferredEdgeless } = usePageHelper(workspace);
|
const { isPreferredEdgeless } = usePageHelper(workspace);
|
||||||
|
@ -4,6 +4,8 @@ import {
|
|||||||
usePageMetaHelper,
|
usePageMetaHelper,
|
||||||
} from '@affine/core/hooks/use-block-suite-page-meta';
|
} from '@affine/core/hooks/use-block-suite-page-meta';
|
||||||
import { useBlockSuiteWorkspaceHelper } from '@affine/core/hooks/use-block-suite-workspace-helper';
|
import { useBlockSuiteWorkspaceHelper } from '@affine/core/hooks/use-block-suite-workspace-helper';
|
||||||
|
import { CollectionService } from '@affine/core/modules/collection';
|
||||||
|
import { useService } from '@toeverything/infra';
|
||||||
import { useAtomValue, useSetAtom } from 'jotai';
|
import { useAtomValue, useSetAtom } from 'jotai';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { applyUpdate, encodeStateAsUpdate } from 'yjs';
|
import { applyUpdate, encodeStateAsUpdate } from 'yjs';
|
||||||
@ -11,7 +13,6 @@ import { applyUpdate, encodeStateAsUpdate } from 'yjs';
|
|||||||
import { setPageModeAtom } from '../../atoms';
|
import { setPageModeAtom } from '../../atoms';
|
||||||
import { currentModeAtom } from '../../atoms/mode';
|
import { currentModeAtom } from '../../atoms/mode';
|
||||||
import type { BlockSuiteWorkspace } from '../../shared';
|
import type { BlockSuiteWorkspace } from '../../shared';
|
||||||
import { getWorkspaceSetting } from '../../utils/workspace-setting';
|
|
||||||
import { useNavigateHelper } from '../use-navigate-helper';
|
import { useNavigateHelper } from '../use-navigate-helper';
|
||||||
import { useReferenceLinkHelper } from './use-reference-link-helper';
|
import { useReferenceLinkHelper } from './use-reference-link-helper';
|
||||||
|
|
||||||
@ -26,6 +27,7 @@ export function useBlockSuiteMetaHelper(
|
|||||||
const currentMode = useAtomValue(currentModeAtom);
|
const currentMode = useAtomValue(currentModeAtom);
|
||||||
const { createPage } = useBlockSuiteWorkspaceHelper(blockSuiteWorkspace);
|
const { createPage } = useBlockSuiteWorkspaceHelper(blockSuiteWorkspace);
|
||||||
const { openPage } = useNavigateHelper();
|
const { openPage } = useNavigateHelper();
|
||||||
|
const collectionService = useService(CollectionService);
|
||||||
|
|
||||||
const switchToPageMode = useCallback(
|
const switchToPageMode = useCallback(
|
||||||
(pageId: string) => {
|
(pageId: string) => {
|
||||||
@ -89,9 +91,9 @@ export function useBlockSuiteMetaHelper(
|
|||||||
trashRelate: isRoot ? parentMeta?.id : undefined,
|
trashRelate: isRoot ? parentMeta?.id : undefined,
|
||||||
});
|
});
|
||||||
setPageReadonly(pageId, true);
|
setPageReadonly(pageId, true);
|
||||||
getWorkspaceSetting(blockSuiteWorkspace).deletePages([pageId]);
|
collectionService.deletePagesFromCollections([pageId]);
|
||||||
},
|
},
|
||||||
[blockSuiteWorkspace, getPageMeta, metas, setPageMeta, setPageReadonly]
|
[collectionService, getPageMeta, metas, setPageMeta, setPageReadonly]
|
||||||
);
|
);
|
||||||
|
|
||||||
const restoreFromTrash = useCallback(
|
const restoreFromTrash = useCallback(
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||||
import { getIsOwnerQuery } from '@affine/graphql';
|
import { getIsOwnerQuery } from '@affine/graphql';
|
||||||
import type { WorkspaceMetadata } from '@affine/workspace/metadata';
|
import type { WorkspaceMetadata } from '@toeverything/infra';
|
||||||
|
|
||||||
import { useQueryImmutable } from '../use-query';
|
import { useQueryImmutable } from '../use-query';
|
||||||
|
|
||||||
|
@ -1,15 +1,16 @@
|
|||||||
import { toast } from '@affine/component';
|
import { toast } from '@affine/component';
|
||||||
import { usePageMetaHelper } from '@affine/core/hooks/use-block-suite-page-meta';
|
import { usePageMetaHelper } from '@affine/core/hooks/use-block-suite-page-meta';
|
||||||
import { waitForCurrentWorkspaceAtom } from '@affine/core/modules/workspace';
|
|
||||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { assertExists } from '@blocksuite/global/utils';
|
import { assertExists } from '@blocksuite/global/utils';
|
||||||
import { EdgelessIcon, HistoryIcon, PageIcon } from '@blocksuite/icons';
|
import { EdgelessIcon, HistoryIcon, PageIcon } from '@blocksuite/icons';
|
||||||
|
import { Workspace } from '@toeverything/infra';
|
||||||
import {
|
import {
|
||||||
PreconditionStrategy,
|
PreconditionStrategy,
|
||||||
registerAffineCommand,
|
registerAffineCommand,
|
||||||
} from '@toeverything/infra/command';
|
} from '@toeverything/infra/command';
|
||||||
import { useAtomValue, useSetAtom } from 'jotai';
|
import { useService } from '@toeverything/infra/di';
|
||||||
|
import { useSetAtom } from 'jotai';
|
||||||
import { useCallback, useEffect } from 'react';
|
import { useCallback, useEffect } from 'react';
|
||||||
|
|
||||||
import { pageHistoryModalAtom } from '../../atoms/page-history';
|
import { pageHistoryModalAtom } from '../../atoms/page-history';
|
||||||
@ -22,7 +23,7 @@ export function useRegisterBlocksuiteEditorCommands(
|
|||||||
mode: 'page' | 'edgeless'
|
mode: 'page' | 'edgeless'
|
||||||
) {
|
) {
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
const workspace = useAtomValue(waitForCurrentWorkspaceAtom);
|
const workspace = useService(Workspace);
|
||||||
const blockSuiteWorkspace = workspace.blockSuiteWorkspace;
|
const blockSuiteWorkspace = workspace.blockSuiteWorkspace;
|
||||||
const { getPageMeta } = usePageMetaHelper(blockSuiteWorkspace);
|
const { getPageMeta } = usePageMetaHelper(blockSuiteWorkspace);
|
||||||
const currentPage = blockSuiteWorkspace.getPage(pageId);
|
const currentPage = blockSuiteWorkspace.getPage(pageId);
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import { toast } from '@affine/component';
|
import { toast } from '@affine/component';
|
||||||
import type { DraggableTitleCellData } from '@affine/core/components/page-list';
|
import type { DraggableTitleCellData } from '@affine/core/components/page-list';
|
||||||
import { usePageMetaHelper } from '@affine/core/hooks/use-block-suite-page-meta';
|
import { usePageMetaHelper } from '@affine/core/hooks/use-block-suite-page-meta';
|
||||||
import { waitForCurrentWorkspaceAtom } from '@affine/core/modules/workspace';
|
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import type { DragEndEvent, UniqueIdentifier } from '@dnd-kit/core';
|
import type { DragEndEvent, UniqueIdentifier } from '@dnd-kit/core';
|
||||||
import { useAtomValue } from 'jotai';
|
import { Workspace } from '@toeverything/infra';
|
||||||
|
import { useService } from '@toeverything/infra/di';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
import { useBlockSuiteMetaHelper } from './use-block-suite-meta-helper';
|
import { useBlockSuiteMetaHelper } from './use-block-suite-meta-helper';
|
||||||
@ -69,7 +69,7 @@ export function getDragItemId(
|
|||||||
|
|
||||||
export const useSidebarDrag = () => {
|
export const useSidebarDrag = () => {
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom);
|
const currentWorkspace = useService(Workspace);
|
||||||
const workspace = currentWorkspace.blockSuiteWorkspace;
|
const workspace = currentWorkspace.blockSuiteWorkspace;
|
||||||
const { setTrashModal } = useTrashModalHelper(workspace);
|
const { setTrashModal } = useTrashModalHelper(workspace);
|
||||||
const { addToFavorite, removeFromFavorite } =
|
const { addToFavorite, removeFromFavorite } =
|
||||||
|
@ -1,15 +1,15 @@
|
|||||||
import { useBlockSuiteWorkspacePage } from '@affine/core/hooks/use-block-suite-workspace-page';
|
import { useBlockSuiteWorkspacePage } from '@affine/core/hooks/use-block-suite-workspace-page';
|
||||||
import { waitForCurrentWorkspaceAtom } from '@affine/core/modules/workspace';
|
import { Workspace } from '@toeverything/infra';
|
||||||
|
import { useService } from '@toeverything/infra/di';
|
||||||
import { useAtomValue } from 'jotai';
|
import { useAtomValue } from 'jotai';
|
||||||
|
|
||||||
import { currentPageIdAtom } from '../../atoms/mode';
|
import { currentPageIdAtom } from '../../atoms/mode';
|
||||||
|
|
||||||
export const useCurrentPage = () => {
|
export const useCurrentPage = () => {
|
||||||
const currentPageId = useAtomValue(currentPageIdAtom);
|
const currentPageId = useAtomValue(currentPageIdAtom);
|
||||||
const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom);
|
const currentWorkspace = useService(Workspace);
|
||||||
|
|
||||||
return useBlockSuiteWorkspacePage(
|
return useBlockSuiteWorkspacePage(
|
||||||
currentWorkspace?.blockSuiteWorkspace,
|
currentWorkspace.blockSuiteWorkspace,
|
||||||
currentPageId
|
currentPageId
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,12 +1,7 @@
|
|||||||
import type { Workspace } from '@blocksuite/store';
|
import { useService } from '@toeverything/infra/di';
|
||||||
import { useAtomValue } from 'jotai';
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import type { WorkspacePropertiesAdapter } from '../modules/workspace/properties';
|
import { WorkspacePropertiesAdapter } from '../modules/workspace/properties';
|
||||||
import {
|
|
||||||
currentWorkspacePropertiesAdapterAtom,
|
|
||||||
workspaceAdapterAtomFamily,
|
|
||||||
} from '../modules/workspace/properties';
|
|
||||||
|
|
||||||
function getProxy<T extends object>(obj: T) {
|
function getProxy<T extends object>(obj: T) {
|
||||||
return new Proxy(obj, {});
|
return new Proxy(obj, {});
|
||||||
@ -31,11 +26,6 @@ const useReactiveAdapter = (adapter: WorkspacePropertiesAdapter) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function useCurrentWorkspacePropertiesAdapter() {
|
export function useCurrentWorkspacePropertiesAdapter() {
|
||||||
const adapter = useAtomValue(currentWorkspacePropertiesAdapterAtom);
|
const adapter = useService(WorkspacePropertiesAdapter);
|
||||||
return useReactiveAdapter(adapter);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useWorkspacePropertiesAdapter(workspace: Workspace) {
|
|
||||||
const adapter = useAtomValue(workspaceAdapterAtomFamily(workspace));
|
|
||||||
return useReactiveAdapter(adapter);
|
return useReactiveAdapter(adapter);
|
||||||
}
|
}
|
||||||
|
@ -1,15 +0,0 @@
|
|||||||
import { useCallback, useSyncExternalStore } from 'react';
|
|
||||||
import type { DataSourceAdapter, Status } from 'y-provider';
|
|
||||||
|
|
||||||
type UIStatus =
|
|
||||||
| Status
|
|
||||||
| {
|
|
||||||
type: 'unknown';
|
|
||||||
};
|
|
||||||
|
|
||||||
export function useDataSourceStatus(provider: DataSourceAdapter): UIStatus {
|
|
||||||
return useSyncExternalStore(
|
|
||||||
provider.subscribeStatusChange,
|
|
||||||
useCallback(() => provider.status, [provider])
|
|
||||||
);
|
|
||||||
}
|
|
@ -4,7 +4,7 @@ import { useCallback, useMemo } from 'react';
|
|||||||
|
|
||||||
import type { BlockSuiteWorkspace } from '../shared';
|
import type { BlockSuiteWorkspace } from '../shared';
|
||||||
import { timestampToLocalDate } from '../utils';
|
import { timestampToLocalDate } from '../utils';
|
||||||
import { useWorkspacePropertiesAdapter } from './use-affine-adapter';
|
import { useCurrentWorkspacePropertiesAdapter } from './use-affine-adapter';
|
||||||
import { useBlockSuiteWorkspaceHelper } from './use-block-suite-workspace-helper';
|
import { useBlockSuiteWorkspaceHelper } from './use-block-suite-workspace-helper';
|
||||||
import { useNavigateHelper } from './use-navigate-helper';
|
import { useNavigateHelper } from './use-navigate-helper';
|
||||||
|
|
||||||
@ -24,7 +24,7 @@ function toDayjs(j?: string | false) {
|
|||||||
|
|
||||||
export const useJournalHelper = (workspace: BlockSuiteWorkspace) => {
|
export const useJournalHelper = (workspace: BlockSuiteWorkspace) => {
|
||||||
const bsWorkspaceHelper = useBlockSuiteWorkspaceHelper(workspace);
|
const bsWorkspaceHelper = useBlockSuiteWorkspaceHelper(workspace);
|
||||||
const adapter = useWorkspacePropertiesAdapter(workspace);
|
const adapter = useCurrentWorkspacePropertiesAdapter();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user