mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-19 11:06:25 +08:00
feat(core): share in workspace link (#7897)
ShareDocsService -> ShareDocsListService ShareService -> ShareInfoService (*new) ShareReaderService `/share/:workspaceId/:docId` -> redirect to -> `/workspace/:workspaceId/:docId` workspace loading process 1. find workspace in workspace list 2. (if not found) revalidate workspace list 3. (if still not found) try load share page 4. (if share page found) => share page 5. (if share page not found) => 404 6. (if workspace found) => workspace page
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
export class NetworkError extends Error {
|
||||
constructor(public readonly originError: Error) {
|
||||
constructor(
|
||||
public readonly originError: Error,
|
||||
public readonly status?: number
|
||||
) {
|
||||
super(`Network error: ${originError.message}`);
|
||||
this.stack = originError.stack;
|
||||
}
|
||||
@@ -10,7 +13,10 @@ export function isNetworkError(error: Error): error is NetworkError {
|
||||
}
|
||||
|
||||
export class BackendError extends Error {
|
||||
constructor(public readonly originError: Error) {
|
||||
constructor(
|
||||
public readonly originError: Error,
|
||||
public readonly status?: number
|
||||
) {
|
||||
super(`Server error: ${originError.message}`);
|
||||
this.stack = originError.stack;
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ export class FetchService extends Service {
|
||||
if (res.status === 504) {
|
||||
const error = new Error('Gateway Timeout');
|
||||
logger.debug('network error', error);
|
||||
throw new NetworkError(error);
|
||||
throw new NetworkError(error, res.status);
|
||||
}
|
||||
if (!res.ok) {
|
||||
logger.warn(
|
||||
@@ -76,7 +76,10 @@ export class FetchService extends Service {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
throw new BackendError(UserFriendlyError.fromAnyError(reason));
|
||||
throw new BackendError(
|
||||
UserFriendlyError.fromAnyError(reason),
|
||||
res.status
|
||||
);
|
||||
}
|
||||
return res;
|
||||
};
|
||||
|
||||
@@ -12,6 +12,6 @@ import { DocsSearchService } from './services/docs-search';
|
||||
export function configureDocsSearchModule(framework: Framework) {
|
||||
framework
|
||||
.scope(WorkspaceScope)
|
||||
.service(DocsSearchService)
|
||||
.service(DocsSearchService, [WorkspaceService])
|
||||
.entity(DocsIndexer, [WorkspaceService]);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { WorkspaceService } from '@toeverything/infra';
|
||||
import {
|
||||
fromPromise,
|
||||
OnEvent,
|
||||
@@ -12,7 +13,15 @@ import { DocsIndexer } from '../entities/docs-indexer';
|
||||
export class DocsSearchService extends Service {
|
||||
readonly indexer = this.framework.createEntity(DocsIndexer);
|
||||
|
||||
constructor(private readonly workspaceService: WorkspaceService) {
|
||||
super();
|
||||
}
|
||||
|
||||
handleWorkspaceEngineBeforeStart() {
|
||||
// skip if in shared mode
|
||||
if (this.workspaceService.workspace.openOptions.isSharedMode) {
|
||||
return;
|
||||
}
|
||||
this.indexer.setupListener();
|
||||
this.indexer.startCrawling();
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
import { track } from '@affine/core/mixpanel';
|
||||
import { CollectionService } from '@affine/core/modules/collection';
|
||||
import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/properties';
|
||||
import { ShareDocsService } from '@affine/core/modules/share-doc';
|
||||
import { ShareDocsListService } from '@affine/core/modules/share-doc';
|
||||
import type { AffineDNDData } from '@affine/core/types/dnd';
|
||||
import type { Collection } from '@affine/env/filter';
|
||||
import { PublicPageMode } from '@affine/graphql';
|
||||
@@ -247,19 +247,19 @@ const ExplorerCollectionNodeChildren = ({
|
||||
const {
|
||||
docsService,
|
||||
compatibleFavoriteItemsAdapter,
|
||||
shareDocsService,
|
||||
shareDocsListService,
|
||||
collectionService,
|
||||
} = useServices({
|
||||
DocsService,
|
||||
CompatibleFavoriteItemsAdapter,
|
||||
ShareDocsService,
|
||||
ShareDocsListService,
|
||||
CollectionService,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
// TODO(@eyhn): loading & error UI
|
||||
shareDocsService.shareDocs?.revalidate();
|
||||
}, [shareDocsService]);
|
||||
shareDocsListService.shareDocs?.revalidate();
|
||||
}, [shareDocsListService]);
|
||||
|
||||
const docMetas = useLiveData(
|
||||
useMemo(
|
||||
@@ -277,7 +277,7 @@ const ExplorerCollectionNodeChildren = ({
|
||||
() => new Set(collection.allowList),
|
||||
[collection.allowList]
|
||||
);
|
||||
const shareDocs = useLiveData(shareDocsService.shareDocs?.list$);
|
||||
const shareDocs = useLiveData(shareDocsListService.shareDocs?.list$);
|
||||
|
||||
const handleRemoveFromAllowList = useCallback(
|
||||
(id: string) => {
|
||||
|
||||
@@ -21,7 +21,7 @@ import type { ShareStore } from '../stores/share';
|
||||
|
||||
type ShareInfoType = GetWorkspacePublicPageByIdQuery['workspace']['publicPage'];
|
||||
|
||||
export class Share extends Entity {
|
||||
export class ShareInfo extends Entity {
|
||||
info$ = new LiveData<ShareInfoType | undefined | null>(null);
|
||||
isShared$ = this.info$.map(info =>
|
||||
// null means not loaded yet, undefined means not shared
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import { UserFriendlyError } from '@affine/graphql';
|
||||
import {
|
||||
type DocMode,
|
||||
effect,
|
||||
Entity,
|
||||
fromPromise,
|
||||
LiveData,
|
||||
onComplete,
|
||||
onStart,
|
||||
} from '@toeverything/infra';
|
||||
import { catchError, EMPTY, mergeMap, switchMap } from 'rxjs';
|
||||
|
||||
import type { ShareReaderStore } from '../stores/share-reader';
|
||||
|
||||
export class ShareReader extends Entity {
|
||||
isLoading$ = new LiveData<boolean>(false);
|
||||
error$ = new LiveData<UserFriendlyError | null>(null);
|
||||
data$ = new LiveData<{
|
||||
workspaceId: string;
|
||||
docId: string;
|
||||
workspaceBinary: Uint8Array;
|
||||
docBinary: Uint8Array;
|
||||
|
||||
// Used for old share server-side mode control
|
||||
publishMode?: DocMode;
|
||||
} | null>(null);
|
||||
|
||||
constructor(private readonly store: ShareReaderStore) {
|
||||
super();
|
||||
}
|
||||
|
||||
loadShare = effect(
|
||||
switchMap(
|
||||
({ workspaceId, docId }: { workspaceId: string; docId: string }) => {
|
||||
return fromPromise(this.store.loadShare(workspaceId, docId)).pipe(
|
||||
mergeMap(data => {
|
||||
if (!data) {
|
||||
this.data$.next(null);
|
||||
} else {
|
||||
this.data$.next({
|
||||
workspaceId,
|
||||
docId,
|
||||
workspaceBinary: data.workspace,
|
||||
docBinary: data.doc,
|
||||
publishMode: data.publishMode ?? undefined,
|
||||
});
|
||||
}
|
||||
return EMPTY;
|
||||
}),
|
||||
catchError((error: any) => {
|
||||
this.error$.next(UserFriendlyError.fromAnyError(error));
|
||||
return EMPTY;
|
||||
}),
|
||||
onStart(() => {
|
||||
this.isLoading$.next(true);
|
||||
this.data$.next(null);
|
||||
this.error$.next(null);
|
||||
}),
|
||||
onComplete(() => {
|
||||
this.isLoading$.next(false);
|
||||
})
|
||||
);
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
export { ShareService } from './services/share';
|
||||
export { ShareDocsService } from './services/share-docs';
|
||||
export type { ShareReader } from './entities/share-reader';
|
||||
export { ShareDocsListService } from './services/share-docs-list';
|
||||
export { ShareInfoService } from './services/share-info';
|
||||
export { ShareReaderService } from './services/share-reader';
|
||||
|
||||
import {
|
||||
DocScope,
|
||||
@@ -10,18 +12,24 @@ import {
|
||||
WorkspaceService,
|
||||
} from '@toeverything/infra';
|
||||
|
||||
import { GraphQLService } from '../cloud';
|
||||
import { FetchService, GraphQLService } from '../cloud';
|
||||
import { ShareDocsList } from './entities/share-docs-list';
|
||||
import { Share } from './entities/share-info';
|
||||
import { ShareService } from './services/share';
|
||||
import { ShareDocsService } from './services/share-docs';
|
||||
import { ShareInfo } from './entities/share-info';
|
||||
import { ShareReader } from './entities/share-reader';
|
||||
import { ShareDocsListService } from './services/share-docs-list';
|
||||
import { ShareInfoService } from './services/share-info';
|
||||
import { ShareReaderService } from './services/share-reader';
|
||||
import { ShareStore } from './stores/share';
|
||||
import { ShareDocsStore } from './stores/share-docs';
|
||||
import { ShareReaderStore } from './stores/share-reader';
|
||||
|
||||
export function configureShareDocsModule(framework: Framework) {
|
||||
framework
|
||||
.service(ShareReaderService)
|
||||
.entity(ShareReader, [ShareReaderStore])
|
||||
.store(ShareReaderStore, [FetchService])
|
||||
.scope(WorkspaceScope)
|
||||
.service(ShareDocsService, [WorkspaceService])
|
||||
.service(ShareDocsListService, [WorkspaceService])
|
||||
.store(ShareDocsStore, [GraphQLService])
|
||||
.entity(ShareDocsList, [
|
||||
WorkspaceService,
|
||||
@@ -29,7 +37,7 @@ export function configureShareDocsModule(framework: Framework) {
|
||||
WorkspaceLocalCache,
|
||||
])
|
||||
.scope(DocScope)
|
||||
.service(ShareService)
|
||||
.entity(Share, [WorkspaceService, DocService, ShareStore])
|
||||
.service(ShareInfoService)
|
||||
.entity(ShareInfo, [WorkspaceService, DocService, ShareStore])
|
||||
.store(ShareStore, [GraphQLService]);
|
||||
}
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ import { Service } from '@toeverything/infra';
|
||||
|
||||
import { ShareDocsList } from '../entities/share-docs-list';
|
||||
|
||||
export class ShareDocsService extends Service {
|
||||
export class ShareDocsListService extends Service {
|
||||
constructor(private readonly workspaceService: WorkspaceService) {
|
||||
super();
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { Service } from '@toeverything/infra';
|
||||
|
||||
import { ShareInfo } from '../entities/share-info';
|
||||
|
||||
export class ShareInfoService extends Service {
|
||||
shareInfo = this.framework.createEntity(ShareInfo);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { Service } from '@toeverything/infra';
|
||||
|
||||
import { ShareReader } from '../entities/share-reader';
|
||||
|
||||
export class ShareReaderService extends Service {
|
||||
reader = this.framework.createEntity(ShareReader);
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import { Service } from '@toeverything/infra';
|
||||
|
||||
import { Share } from '../entities/share-info';
|
||||
|
||||
export class ShareService extends Service {
|
||||
share = this.framework.createEntity(Share);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { ErrorNames, UserFriendlyError } from '@affine/graphql';
|
||||
import { type DocMode, Store } from '@toeverything/infra';
|
||||
|
||||
import { type FetchService, isBackendError } from '../../cloud';
|
||||
|
||||
export class ShareReaderStore extends Store {
|
||||
constructor(private readonly fetchService: FetchService) {
|
||||
super();
|
||||
}
|
||||
|
||||
async loadShare(workspaceId: string, docId: string) {
|
||||
try {
|
||||
const docResponse = await this.fetchService.fetch(
|
||||
`/api/workspaces/${workspaceId}/docs/${docId}`
|
||||
);
|
||||
const publishMode = docResponse.headers.get(
|
||||
'publish-mode'
|
||||
) as DocMode | null;
|
||||
const docBinary = await docResponse.arrayBuffer();
|
||||
|
||||
const workspaceResponse = await this.fetchService.fetch(
|
||||
`/api/workspaces/${workspaceId}/docs/${workspaceId}`
|
||||
);
|
||||
const workspaceBinary = await workspaceResponse.arrayBuffer();
|
||||
|
||||
return {
|
||||
doc: new Uint8Array(docBinary),
|
||||
workspace: new Uint8Array(workspaceBinary),
|
||||
publishMode,
|
||||
};
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof Error &&
|
||||
isBackendError(error) &&
|
||||
UserFriendlyError.fromAnyError(error).name === ErrorNames.ACCESS_DENIED
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -170,8 +170,8 @@ export class CloudWorkspaceFlavourProviderService
|
||||
catchErrorInto(this.error$, err => {
|
||||
logger.error('error to revalidate cloud workspaces', err);
|
||||
}),
|
||||
onStart(() => this.isLoading$.next(true)),
|
||||
onComplete(() => this.isLoading$.next(false))
|
||||
onStart(() => this.isRevalidating$.next(true)),
|
||||
onComplete(() => this.isRevalidating$.next(false))
|
||||
);
|
||||
},
|
||||
({ accountId }) => {
|
||||
@@ -186,7 +186,7 @@ export class CloudWorkspaceFlavourProviderService
|
||||
)
|
||||
);
|
||||
error$ = new LiveData<any>(null);
|
||||
isLoading$ = new LiveData(false);
|
||||
isRevalidating$ = new LiveData(false);
|
||||
workspaces$ = new LiveData<WorkspaceMetadata[]>([]);
|
||||
async getWorkspaceProfile(
|
||||
id: string,
|
||||
@@ -277,6 +277,6 @@ export class CloudWorkspaceFlavourProviderService
|
||||
}
|
||||
|
||||
private waitForLoaded() {
|
||||
return this.isLoading$.waitFor(loading => !loading);
|
||||
return this.isRevalidating$.waitFor(loading => !loading);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,7 +125,7 @@ export class LocalWorkspaceFlavourProvider
|
||||
}),
|
||||
[]
|
||||
);
|
||||
isLoading$ = new LiveData(false);
|
||||
isRevalidating$ = new LiveData(false);
|
||||
revalidate(): void {
|
||||
// notify livedata to re-scan workspaces
|
||||
this.notifyChannel.postMessage(null);
|
||||
|
||||
@@ -20,6 +20,8 @@ import {
|
||||
} from './impls/local';
|
||||
import { WorkspaceEngineStorageProvider } from './providers/engine';
|
||||
|
||||
export { CloudBlobStorage } from './impls/engine/blob-cloud';
|
||||
|
||||
export function configureBrowserWorkspaceFlavours(framework: Framework) {
|
||||
framework
|
||||
.impl(WorkspaceFlavourProvider('LOCAL'), LocalWorkspaceFlavourProvider, [
|
||||
|
||||
Reference in New Issue
Block a user