mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-09-23 20:18:42 +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:
@@ -56,6 +56,7 @@ describe('example', () => {
|
|||||||
fetchUser.mockRejectedValue(new Error('some error'));
|
fetchUser.mockRejectedValue(new Error('some error'));
|
||||||
loadUser(1);
|
loadUser(1);
|
||||||
await vi.waitFor(() => expect(error$.value).toBeInstanceOf(Error));
|
await vi.waitFor(() => expect(error$.value).toBeInstanceOf(Error));
|
||||||
|
await vi.waitFor(() => expect(isLoading$.value).toBe(false));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('isLoading', async () => {
|
test('isLoading', async () => {
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ export class WorkspaceList extends Entity {
|
|||||||
.map(workspaces => {
|
.map(workspaces => {
|
||||||
return workspaces.flat();
|
return workspaces.flat();
|
||||||
});
|
});
|
||||||
isLoading$ = new LiveData(
|
isRevalidating$ = new LiveData(
|
||||||
this.providers.map(p => p.isLoading$ ?? new LiveData(false))
|
this.providers.map(p => p.isRevalidating$ ?? new LiveData(false))
|
||||||
)
|
)
|
||||||
.flat()
|
.flat()
|
||||||
.map(isLoadings => isLoadings.some(isLoading => isLoading));
|
.map(isLoadings => isLoadings.some(isLoading => isLoading));
|
||||||
@@ -24,4 +24,9 @@ export class WorkspaceList extends Entity {
|
|||||||
revalidate() {
|
revalidate() {
|
||||||
this.providers.forEach(provider => provider.revalidate?.());
|
this.providers.forEach(provider => provider.revalidate?.());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
waitForRevalidation(signal?: AbortSignal) {
|
||||||
|
this.revalidate();
|
||||||
|
return this.isRevalidating$.waitFor(isLoading => !isLoading, signal);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,10 +62,6 @@ export class Workspace extends Entity {
|
|||||||
return this.framework.get(WorkspaceUpgradeService).upgrade;
|
return this.framework.get(WorkspaceUpgradeService).upgrade;
|
||||||
}
|
}
|
||||||
|
|
||||||
get flavourProvider() {
|
|
||||||
return this.scope.props.flavourProvider;
|
|
||||||
}
|
|
||||||
|
|
||||||
name$ = LiveData.from<string | undefined>(
|
name$ = LiveData.from<string | undefined>(
|
||||||
new Observable(subscriber => {
|
new Observable(subscriber => {
|
||||||
subscriber.next(this.docCollection.meta.name);
|
subscriber.next(this.docCollection.meta.name);
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export interface WorkspaceFlavourProvider {
|
|||||||
/**
|
/**
|
||||||
* means the workspace list is loading. if it's true, the workspace page will show loading spinner.
|
* means the workspace list is loading. if it's true, the workspace page will show loading spinner.
|
||||||
*/
|
*/
|
||||||
isLoading$?: LiveData<boolean>;
|
isRevalidating$?: LiveData<boolean>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* revalidate the workspace list.
|
* revalidate the workspace list.
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { Scope } from '../../../framework';
|
import { Scope } from '../../../framework';
|
||||||
import type { WorkspaceOpenOptions } from '../open-options';
|
import type { WorkspaceOpenOptions } from '../open-options';
|
||||||
import type { WorkspaceFlavourProvider } from '../providers/flavour';
|
import type { WorkspaceEngineProvider } from '../providers/flavour';
|
||||||
|
|
||||||
export type { DocCollection } from '@blocksuite/store';
|
export type { DocCollection } from '@blocksuite/store';
|
||||||
|
|
||||||
export class WorkspaceScope extends Scope<{
|
export class WorkspaceScope extends Scope<{
|
||||||
openOptions: WorkspaceOpenOptions;
|
openOptions: WorkspaceOpenOptions;
|
||||||
flavourProvider: WorkspaceFlavourProvider;
|
engineProvider: WorkspaceEngineProvider;
|
||||||
}> {}
|
}> {}
|
||||||
|
|||||||
@@ -7,10 +7,7 @@ export class WorkspaceEngineService extends Service {
|
|||||||
get engine() {
|
get engine() {
|
||||||
if (!this._engine) {
|
if (!this._engine) {
|
||||||
this._engine = this.framework.createEntity(WorkspaceEngine, {
|
this._engine = this.framework.createEntity(WorkspaceEngine, {
|
||||||
engineProvider:
|
engineProvider: this.workspaceScope.props.engineProvider,
|
||||||
this.workspaceScope.props.flavourProvider.getEngineProvider(
|
|
||||||
this.workspaceScope.props.openOptions.metadata.id
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return this._engine;
|
return this._engine;
|
||||||
|
|||||||
@@ -6,7 +6,10 @@ import { ObjectPool } from '../../../utils';
|
|||||||
import type { Workspace } from '../entities/workspace';
|
import type { Workspace } from '../entities/workspace';
|
||||||
import { WorkspaceInitialized } from '../events';
|
import { WorkspaceInitialized } from '../events';
|
||||||
import type { WorkspaceOpenOptions } from '../open-options';
|
import type { WorkspaceOpenOptions } from '../open-options';
|
||||||
import type { WorkspaceFlavourProvider } from '../providers/flavour';
|
import type {
|
||||||
|
WorkspaceEngineProvider,
|
||||||
|
WorkspaceFlavourProvider,
|
||||||
|
} from '../providers/flavour';
|
||||||
import { WorkspaceScope } from '../scopes/workspace';
|
import { WorkspaceScope } from '../scopes/workspace';
|
||||||
import type { WorkspaceProfileService } from './profile';
|
import type { WorkspaceProfileService } from './profile';
|
||||||
import { WorkspaceService } from './workspace';
|
import { WorkspaceService } from './workspace';
|
||||||
@@ -39,7 +42,7 @@ export class WorkspaceRepositoryService extends Service {
|
|||||||
*/
|
*/
|
||||||
open = (
|
open = (
|
||||||
options: WorkspaceOpenOptions,
|
options: WorkspaceOpenOptions,
|
||||||
customProvider?: WorkspaceFlavourProvider
|
customProvider?: WorkspaceEngineProvider
|
||||||
): {
|
): {
|
||||||
workspace: Workspace;
|
workspace: Workspace;
|
||||||
dispose: () => void;
|
dispose: () => void;
|
||||||
@@ -76,14 +79,16 @@ export class WorkspaceRepositoryService extends Service {
|
|||||||
|
|
||||||
instantiate(
|
instantiate(
|
||||||
openOptions: WorkspaceOpenOptions,
|
openOptions: WorkspaceOpenOptions,
|
||||||
customProvider?: WorkspaceFlavourProvider
|
customProvider?: WorkspaceEngineProvider
|
||||||
) {
|
) {
|
||||||
logger.info(
|
logger.info(
|
||||||
`open workspace [${openOptions.metadata.flavour}] ${openOptions.metadata.id} `
|
`open workspace [${openOptions.metadata.flavour}] ${openOptions.metadata.id} `
|
||||||
);
|
);
|
||||||
const provider =
|
const provider =
|
||||||
customProvider ??
|
customProvider ??
|
||||||
this.providers.find(p => p.flavour === openOptions.metadata.flavour);
|
this.providers
|
||||||
|
.find(p => p.flavour === openOptions.metadata.flavour)
|
||||||
|
?.getEngineProvider(openOptions.metadata.id);
|
||||||
if (!provider) {
|
if (!provider) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Unknown workspace flavour: ${openOptions.metadata.flavour}`
|
`Unknown workspace flavour: ${openOptions.metadata.flavour}`
|
||||||
@@ -92,7 +97,7 @@ export class WorkspaceRepositoryService extends Service {
|
|||||||
|
|
||||||
const workspaceScope = this.framework.createScope(WorkspaceScope, {
|
const workspaceScope = this.framework.createScope(WorkspaceScope, {
|
||||||
openOptions,
|
openOptions,
|
||||||
flavourProvider: provider,
|
engineProvider: provider,
|
||||||
});
|
});
|
||||||
|
|
||||||
const workspace = workspaceScope.get(WorkspaceService).workspace;
|
const workspace = workspaceScope.get(WorkspaceService).workspace;
|
||||||
|
|||||||
+5
-5
@@ -1,7 +1,7 @@
|
|||||||
import { Button } from '@affine/component/ui/button';
|
import { Button } from '@affine/component/ui/button';
|
||||||
import { Divider } from '@affine/component/ui/divider';
|
import { Divider } from '@affine/component/ui/divider';
|
||||||
import { Menu } from '@affine/component/ui/menu';
|
import { Menu } from '@affine/component/ui/menu';
|
||||||
import { ShareService } from '@affine/core/modules/share-doc';
|
import { ShareInfoService } from '@affine/core/modules/share-doc';
|
||||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import { WebIcon } from '@blocksuite/icons/rc';
|
import { WebIcon } from '@blocksuite/icons/rc';
|
||||||
@@ -48,12 +48,12 @@ const DefaultShareButton = forwardRef(function DefaultShareButton(
|
|||||||
ref: Ref<HTMLButtonElement>
|
ref: Ref<HTMLButtonElement>
|
||||||
) {
|
) {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const shareService = useService(ShareService);
|
const shareInfoService = useService(ShareInfoService);
|
||||||
const shared = useLiveData(shareService.share.isShared$);
|
const shared = useLiveData(shareInfoService.shareInfo.isShared$);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
shareService.share.revalidate();
|
shareInfoService.shareInfo.revalidate();
|
||||||
}, [shareService]);
|
}, [shareInfoService]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button ref={ref} className={styles.shareButton} variant="primary">
|
<Button ref={ref} className={styles.shareButton} variant="primary">
|
||||||
|
|||||||
+12
-12
@@ -6,7 +6,7 @@ import { useSharingUrl } from '@affine/core/hooks/affine/use-share-url';
|
|||||||
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||||
import { track } from '@affine/core/mixpanel';
|
import { track } from '@affine/core/mixpanel';
|
||||||
import { ServerConfigService } from '@affine/core/modules/cloud';
|
import { ServerConfigService } from '@affine/core/modules/cloud';
|
||||||
import { ShareService } from '@affine/core/modules/share-doc';
|
import { ShareInfoService } from '@affine/core/modules/share-doc';
|
||||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||||
import { PublicPageMode } from '@affine/graphql';
|
import { PublicPageMode } from '@affine/graphql';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
@@ -59,13 +59,13 @@ export const AffineSharePage = (props: ShareMenuProps) => {
|
|||||||
workspaceMetadata: { id: workspaceId },
|
workspaceMetadata: { id: workspaceId },
|
||||||
} = props;
|
} = props;
|
||||||
const doc = useService(DocService).doc;
|
const doc = useService(DocService).doc;
|
||||||
const shareService = useService(ShareService);
|
const shareInfoService = useService(ShareInfoService);
|
||||||
const serverConfig = useService(ServerConfigService).serverConfig;
|
const serverConfig = useService(ServerConfigService).serverConfig;
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
shareService.share.revalidate();
|
shareInfoService.shareInfo.revalidate();
|
||||||
}, [shareService]);
|
}, [shareInfoService]);
|
||||||
const isSharedPage = useLiveData(shareService.share.isShared$);
|
const isSharedPage = useLiveData(shareInfoService.shareInfo.isShared$);
|
||||||
const sharedMode = useLiveData(shareService.share.sharedMode$);
|
const sharedMode = useLiveData(shareInfoService.shareInfo.sharedMode$);
|
||||||
const baseUrl = useLiveData(serverConfig.config$.map(c => c?.baseUrl));
|
const baseUrl = useLiveData(serverConfig.config$.map(c => c?.baseUrl));
|
||||||
const isLoading =
|
const isLoading =
|
||||||
isSharedPage === null || sharedMode === null || baseUrl === null;
|
isSharedPage === null || sharedMode === null || baseUrl === null;
|
||||||
@@ -103,7 +103,7 @@ export const AffineSharePage = (props: ShareMenuProps) => {
|
|||||||
|
|
||||||
const onClickCreateLink = useAsyncCallback(async () => {
|
const onClickCreateLink = useAsyncCallback(async () => {
|
||||||
try {
|
try {
|
||||||
await shareService.share.enableShare(
|
await shareInfoService.shareInfo.enableShare(
|
||||||
mode === 'edgeless' ? PublicPageMode.Edgeless : PublicPageMode.Page
|
mode === 'edgeless' ? PublicPageMode.Edgeless : PublicPageMode.Page
|
||||||
);
|
);
|
||||||
track.$.sharePanel.$.createShareLink({
|
track.$.sharePanel.$.createShareLink({
|
||||||
@@ -139,11 +139,11 @@ export const AffineSharePage = (props: ShareMenuProps) => {
|
|||||||
});
|
});
|
||||||
console.error(err);
|
console.error(err);
|
||||||
}
|
}
|
||||||
}, [mode, shareService.share, sharingUrl, t]);
|
}, [mode, shareInfoService.shareInfo, sharingUrl, t]);
|
||||||
|
|
||||||
const onDisablePublic = useAsyncCallback(async () => {
|
const onDisablePublic = useAsyncCallback(async () => {
|
||||||
try {
|
try {
|
||||||
await shareService.share.disableShare();
|
await shareInfoService.shareInfo.disableShare();
|
||||||
notify.error({
|
notify.error({
|
||||||
title:
|
title:
|
||||||
t[
|
t[
|
||||||
@@ -168,13 +168,13 @@ export const AffineSharePage = (props: ShareMenuProps) => {
|
|||||||
console.log(err);
|
console.log(err);
|
||||||
}
|
}
|
||||||
setShowDisable(false);
|
setShowDisable(false);
|
||||||
}, [shareService, t]);
|
}, [shareInfoService, t]);
|
||||||
|
|
||||||
const onShareModeChange = useAsyncCallback(
|
const onShareModeChange = useAsyncCallback(
|
||||||
async (value: DocMode) => {
|
async (value: DocMode) => {
|
||||||
try {
|
try {
|
||||||
if (isSharedPage) {
|
if (isSharedPage) {
|
||||||
await shareService.share.changeShare(
|
await shareInfoService.shareInfo.changeShare(
|
||||||
value === 'edgeless' ? PublicPageMode.Edgeless : PublicPageMode.Page
|
value === 'edgeless' ? PublicPageMode.Edgeless : PublicPageMode.Page
|
||||||
);
|
);
|
||||||
notify.success({
|
notify.success({
|
||||||
@@ -208,7 +208,7 @@ export const AffineSharePage = (props: ShareMenuProps) => {
|
|||||||
console.error(err);
|
console.error(err);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[isSharedPage, shareService.share, t]
|
[isSharedPage, shareInfoService.shareInfo, t]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/properties';
|
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 { Collection, Filter } from '@affine/env/filter';
|
import type { Collection, Filter } from '@affine/env/filter';
|
||||||
import { PublicPageMode } from '@affine/graphql';
|
import { PublicPageMode } from '@affine/graphql';
|
||||||
import type { DocMeta } from '@blocksuite/store';
|
import type { DocMeta } from '@blocksuite/store';
|
||||||
@@ -16,8 +16,8 @@ export const useFilteredPageMetas = (
|
|||||||
collection?: Collection;
|
collection?: Collection;
|
||||||
} = {}
|
} = {}
|
||||||
) => {
|
) => {
|
||||||
const shareDocsService = useService(ShareDocsService);
|
const shareDocsListService = useService(ShareDocsListService);
|
||||||
const shareDocs = useLiveData(shareDocsService.shareDocs?.list$);
|
const shareDocs = useLiveData(shareDocsListService.shareDocs?.list$);
|
||||||
|
|
||||||
const getPublicMode = useCallback(
|
const getPublicMode = useCallback(
|
||||||
(id: string) => {
|
(id: string) => {
|
||||||
@@ -33,8 +33,8 @@ export const useFilteredPageMetas = (
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// TODO(@eyhn): loading & error UI
|
// TODO(@eyhn): loading & error UI
|
||||||
shareDocsService.shareDocs?.revalidate();
|
shareDocsListService.shareDocs?.revalidate();
|
||||||
}, [shareDocsService]);
|
}, [shareDocsListService]);
|
||||||
|
|
||||||
const favAdapter = useService(CompatibleFavoriteItemsAdapter);
|
const favAdapter = useService(CompatibleFavoriteItemsAdapter);
|
||||||
const favoriteItems = useLiveData(favAdapter.favorites$);
|
const favoriteItems = useLiveData(favAdapter.favorites$);
|
||||||
|
|||||||
+13
-10
@@ -1,7 +1,7 @@
|
|||||||
import { IconButton, Menu, toast } from '@affine/component';
|
import { IconButton, Menu, toast } from '@affine/component';
|
||||||
import { useBlockSuiteDocMeta } from '@affine/core/hooks/use-block-suite-page-meta';
|
import { useBlockSuiteDocMeta } from '@affine/core/hooks/use-block-suite-page-meta';
|
||||||
import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/properties';
|
import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/properties';
|
||||||
import { ShareDocsService } from '@affine/core/modules/share-doc';
|
import { ShareDocsListService } from '@affine/core/modules/share-doc';
|
||||||
import { PublicPageMode } from '@affine/graphql';
|
import { PublicPageMode } from '@affine/graphql';
|
||||||
import { Trans, useI18n } from '@affine/i18n';
|
import { Trans, useI18n } from '@affine/i18n';
|
||||||
import { FilterIcon } from '@blocksuite/icons/rc';
|
import { FilterIcon } from '@blocksuite/icons/rc';
|
||||||
@@ -56,21 +56,24 @@ export const SelectPage = ({
|
|||||||
const clearSelected = useCallback(() => {
|
const clearSelected = useCallback(() => {
|
||||||
onChange([]);
|
onChange([]);
|
||||||
}, [onChange]);
|
}, [onChange]);
|
||||||
const { workspaceService, compatibleFavoriteItemsAdapter, shareDocsService } =
|
const {
|
||||||
useServices({
|
workspaceService,
|
||||||
ShareDocsService,
|
compatibleFavoriteItemsAdapter,
|
||||||
WorkspaceService,
|
shareDocsListService,
|
||||||
CompatibleFavoriteItemsAdapter,
|
} = useServices({
|
||||||
});
|
ShareDocsListService,
|
||||||
const shareDocs = useLiveData(shareDocsService.shareDocs?.list$);
|
WorkspaceService,
|
||||||
|
CompatibleFavoriteItemsAdapter,
|
||||||
|
});
|
||||||
|
const shareDocs = useLiveData(shareDocsListService.shareDocs?.list$);
|
||||||
const workspace = workspaceService.workspace;
|
const workspace = workspaceService.workspace;
|
||||||
const docCollection = workspace.docCollection;
|
const docCollection = workspace.docCollection;
|
||||||
const pageMetas = useBlockSuiteDocMeta(docCollection);
|
const pageMetas = useBlockSuiteDocMeta(docCollection);
|
||||||
const favourites = useLiveData(compatibleFavoriteItemsAdapter.favorites$);
|
const favourites = useLiveData(compatibleFavoriteItemsAdapter.favorites$);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
shareDocsService.shareDocs?.revalidate();
|
shareDocsListService.shareDocs?.revalidate();
|
||||||
}, [shareDocsService.shareDocs]);
|
}, [shareDocsListService.shareDocs]);
|
||||||
|
|
||||||
const getPublicMode = useCallback(
|
const getPublicMode = useCallback(
|
||||||
(id: string) => {
|
(id: string) => {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import type { AllPageListConfig } from '@affine/core/components/page-list';
|
|||||||
import { FavoriteTag } from '@affine/core/components/page-list';
|
import { FavoriteTag } from '@affine/core/components/page-list';
|
||||||
import { useBlockSuiteDocMeta } from '@affine/core/hooks/use-block-suite-page-meta';
|
import { useBlockSuiteDocMeta } from '@affine/core/hooks/use-block-suite-page-meta';
|
||||||
import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/properties';
|
import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/properties';
|
||||||
import { ShareDocsService } from '@affine/core/modules/share-doc';
|
import { ShareDocsListService } from '@affine/core/modules/share-doc';
|
||||||
import { PublicPageMode } from '@affine/graphql';
|
import { PublicPageMode } from '@affine/graphql';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import type { DocMeta } from '@blocksuite/store';
|
import type { DocMeta } from '@blocksuite/store';
|
||||||
@@ -15,13 +15,13 @@ import { useCallback, useEffect, useMemo } from 'react';
|
|||||||
*/
|
*/
|
||||||
export const useAllPageListConfig = () => {
|
export const useAllPageListConfig = () => {
|
||||||
const currentWorkspace = useService(WorkspaceService).workspace;
|
const currentWorkspace = useService(WorkspaceService).workspace;
|
||||||
const shareDocService = useService(ShareDocsService);
|
const shareDocsListService = useService(ShareDocsListService);
|
||||||
const shareDocs = useLiveData(shareDocService.shareDocs?.list$);
|
const shareDocs = useLiveData(shareDocsListService.shareDocs?.list$);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// TODO(@eyhn): loading & error UI
|
// TODO(@eyhn): loading & error UI
|
||||||
shareDocService.shareDocs?.revalidate();
|
shareDocsListService.shareDocs?.revalidate();
|
||||||
}, [shareDocService]);
|
}, [shareDocsListService]);
|
||||||
|
|
||||||
const workspace = currentWorkspace.docCollection;
|
const workspace = currentWorkspace.docCollection;
|
||||||
const pageMetas = useBlockSuiteDocMeta(workspace);
|
const pageMetas = useBlockSuiteDocMeta(workspace);
|
||||||
|
|||||||
@@ -228,10 +228,10 @@ const WorkspaceLayoutUIContainer = ({ children }: PropsWithChildren) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
export const WorkspaceLayoutInner = ({ children }: PropsWithChildren) => {
|
export const WorkspaceLayoutInner = ({ children }: PropsWithChildren) => {
|
||||||
const currentWorkspace = useService(WorkspaceService).workspace;
|
const workspace = useService(WorkspaceService).workspace;
|
||||||
|
|
||||||
const upgrading = useLiveData(currentWorkspace.upgrade.upgrading$);
|
const upgrading = useLiveData(workspace.upgrade.upgrading$);
|
||||||
const needUpgrade = useLiveData(currentWorkspace.upgrade.needUpgrade$);
|
const needUpgrade = useLiveData(workspace.upgrade.needUpgrade$);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<WorkspaceLayoutProviders>
|
<WorkspaceLayoutProviders>
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
export class NetworkError extends Error {
|
export class NetworkError extends Error {
|
||||||
constructor(public readonly originError: Error) {
|
constructor(
|
||||||
|
public readonly originError: Error,
|
||||||
|
public readonly status?: number
|
||||||
|
) {
|
||||||
super(`Network error: ${originError.message}`);
|
super(`Network error: ${originError.message}`);
|
||||||
this.stack = originError.stack;
|
this.stack = originError.stack;
|
||||||
}
|
}
|
||||||
@@ -10,7 +13,10 @@ export function isNetworkError(error: Error): error is NetworkError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class BackendError extends Error {
|
export class BackendError extends Error {
|
||||||
constructor(public readonly originError: Error) {
|
constructor(
|
||||||
|
public readonly originError: Error,
|
||||||
|
public readonly status?: number
|
||||||
|
) {
|
||||||
super(`Server error: ${originError.message}`);
|
super(`Server error: ${originError.message}`);
|
||||||
this.stack = originError.stack;
|
this.stack = originError.stack;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ export class FetchService extends Service {
|
|||||||
if (res.status === 504) {
|
if (res.status === 504) {
|
||||||
const error = new Error('Gateway Timeout');
|
const error = new Error('Gateway Timeout');
|
||||||
logger.debug('network error', error);
|
logger.debug('network error', error);
|
||||||
throw new NetworkError(error);
|
throw new NetworkError(error, res.status);
|
||||||
}
|
}
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
@@ -76,7 +76,10 @@ export class FetchService extends Service {
|
|||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw new BackendError(UserFriendlyError.fromAnyError(reason));
|
throw new BackendError(
|
||||||
|
UserFriendlyError.fromAnyError(reason),
|
||||||
|
res.status
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,6 +12,6 @@ import { DocsSearchService } from './services/docs-search';
|
|||||||
export function configureDocsSearchModule(framework: Framework) {
|
export function configureDocsSearchModule(framework: Framework) {
|
||||||
framework
|
framework
|
||||||
.scope(WorkspaceScope)
|
.scope(WorkspaceScope)
|
||||||
.service(DocsSearchService)
|
.service(DocsSearchService, [WorkspaceService])
|
||||||
.entity(DocsIndexer, [WorkspaceService]);
|
.entity(DocsIndexer, [WorkspaceService]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { WorkspaceService } from '@toeverything/infra';
|
||||||
import {
|
import {
|
||||||
fromPromise,
|
fromPromise,
|
||||||
OnEvent,
|
OnEvent,
|
||||||
@@ -12,7 +13,15 @@ import { DocsIndexer } from '../entities/docs-indexer';
|
|||||||
export class DocsSearchService extends Service {
|
export class DocsSearchService extends Service {
|
||||||
readonly indexer = this.framework.createEntity(DocsIndexer);
|
readonly indexer = this.framework.createEntity(DocsIndexer);
|
||||||
|
|
||||||
|
constructor(private readonly workspaceService: WorkspaceService) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
handleWorkspaceEngineBeforeStart() {
|
handleWorkspaceEngineBeforeStart() {
|
||||||
|
// skip if in shared mode
|
||||||
|
if (this.workspaceService.workspace.openOptions.isSharedMode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.indexer.setupListener();
|
this.indexer.setupListener();
|
||||||
this.indexer.startCrawling();
|
this.indexer.startCrawling();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
import { track } from '@affine/core/mixpanel';
|
import { track } from '@affine/core/mixpanel';
|
||||||
import { CollectionService } from '@affine/core/modules/collection';
|
import { CollectionService } from '@affine/core/modules/collection';
|
||||||
import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/properties';
|
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 { AffineDNDData } from '@affine/core/types/dnd';
|
||||||
import type { Collection } from '@affine/env/filter';
|
import type { Collection } from '@affine/env/filter';
|
||||||
import { PublicPageMode } from '@affine/graphql';
|
import { PublicPageMode } from '@affine/graphql';
|
||||||
@@ -247,19 +247,19 @@ const ExplorerCollectionNodeChildren = ({
|
|||||||
const {
|
const {
|
||||||
docsService,
|
docsService,
|
||||||
compatibleFavoriteItemsAdapter,
|
compatibleFavoriteItemsAdapter,
|
||||||
shareDocsService,
|
shareDocsListService,
|
||||||
collectionService,
|
collectionService,
|
||||||
} = useServices({
|
} = useServices({
|
||||||
DocsService,
|
DocsService,
|
||||||
CompatibleFavoriteItemsAdapter,
|
CompatibleFavoriteItemsAdapter,
|
||||||
ShareDocsService,
|
ShareDocsListService,
|
||||||
CollectionService,
|
CollectionService,
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// TODO(@eyhn): loading & error UI
|
// TODO(@eyhn): loading & error UI
|
||||||
shareDocsService.shareDocs?.revalidate();
|
shareDocsListService.shareDocs?.revalidate();
|
||||||
}, [shareDocsService]);
|
}, [shareDocsListService]);
|
||||||
|
|
||||||
const docMetas = useLiveData(
|
const docMetas = useLiveData(
|
||||||
useMemo(
|
useMemo(
|
||||||
@@ -277,7 +277,7 @@ const ExplorerCollectionNodeChildren = ({
|
|||||||
() => new Set(collection.allowList),
|
() => new Set(collection.allowList),
|
||||||
[collection.allowList]
|
[collection.allowList]
|
||||||
);
|
);
|
||||||
const shareDocs = useLiveData(shareDocsService.shareDocs?.list$);
|
const shareDocs = useLiveData(shareDocsListService.shareDocs?.list$);
|
||||||
|
|
||||||
const handleRemoveFromAllowList = useCallback(
|
const handleRemoveFromAllowList = useCallback(
|
||||||
(id: string) => {
|
(id: string) => {
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import type { ShareStore } from '../stores/share';
|
|||||||
|
|
||||||
type ShareInfoType = GetWorkspacePublicPageByIdQuery['workspace']['publicPage'];
|
type ShareInfoType = GetWorkspacePublicPageByIdQuery['workspace']['publicPage'];
|
||||||
|
|
||||||
export class Share extends Entity {
|
export class ShareInfo extends Entity {
|
||||||
info$ = new LiveData<ShareInfoType | undefined | null>(null);
|
info$ = new LiveData<ShareInfoType | undefined | null>(null);
|
||||||
isShared$ = this.info$.map(info =>
|
isShared$ = this.info$.map(info =>
|
||||||
// null means not loaded yet, undefined means not shared
|
// 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 type { ShareReader } from './entities/share-reader';
|
||||||
export { ShareDocsService } from './services/share-docs';
|
export { ShareDocsListService } from './services/share-docs-list';
|
||||||
|
export { ShareInfoService } from './services/share-info';
|
||||||
|
export { ShareReaderService } from './services/share-reader';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DocScope,
|
DocScope,
|
||||||
@@ -10,18 +12,24 @@ import {
|
|||||||
WorkspaceService,
|
WorkspaceService,
|
||||||
} from '@toeverything/infra';
|
} from '@toeverything/infra';
|
||||||
|
|
||||||
import { GraphQLService } from '../cloud';
|
import { FetchService, GraphQLService } from '../cloud';
|
||||||
import { ShareDocsList } from './entities/share-docs-list';
|
import { ShareDocsList } from './entities/share-docs-list';
|
||||||
import { Share } from './entities/share-info';
|
import { ShareInfo } from './entities/share-info';
|
||||||
import { ShareService } from './services/share';
|
import { ShareReader } from './entities/share-reader';
|
||||||
import { ShareDocsService } from './services/share-docs';
|
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 { ShareStore } from './stores/share';
|
||||||
import { ShareDocsStore } from './stores/share-docs';
|
import { ShareDocsStore } from './stores/share-docs';
|
||||||
|
import { ShareReaderStore } from './stores/share-reader';
|
||||||
|
|
||||||
export function configureShareDocsModule(framework: Framework) {
|
export function configureShareDocsModule(framework: Framework) {
|
||||||
framework
|
framework
|
||||||
|
.service(ShareReaderService)
|
||||||
|
.entity(ShareReader, [ShareReaderStore])
|
||||||
|
.store(ShareReaderStore, [FetchService])
|
||||||
.scope(WorkspaceScope)
|
.scope(WorkspaceScope)
|
||||||
.service(ShareDocsService, [WorkspaceService])
|
.service(ShareDocsListService, [WorkspaceService])
|
||||||
.store(ShareDocsStore, [GraphQLService])
|
.store(ShareDocsStore, [GraphQLService])
|
||||||
.entity(ShareDocsList, [
|
.entity(ShareDocsList, [
|
||||||
WorkspaceService,
|
WorkspaceService,
|
||||||
@@ -29,7 +37,7 @@ export function configureShareDocsModule(framework: Framework) {
|
|||||||
WorkspaceLocalCache,
|
WorkspaceLocalCache,
|
||||||
])
|
])
|
||||||
.scope(DocScope)
|
.scope(DocScope)
|
||||||
.service(ShareService)
|
.service(ShareInfoService)
|
||||||
.entity(Share, [WorkspaceService, DocService, ShareStore])
|
.entity(ShareInfo, [WorkspaceService, DocService, ShareStore])
|
||||||
.store(ShareStore, [GraphQLService]);
|
.store(ShareStore, [GraphQLService]);
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ import { Service } from '@toeverything/infra';
|
|||||||
|
|
||||||
import { ShareDocsList } from '../entities/share-docs-list';
|
import { ShareDocsList } from '../entities/share-docs-list';
|
||||||
|
|
||||||
export class ShareDocsService extends Service {
|
export class ShareDocsListService extends Service {
|
||||||
constructor(private readonly workspaceService: WorkspaceService) {
|
constructor(private readonly workspaceService: WorkspaceService) {
|
||||||
super();
|
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 => {
|
catchErrorInto(this.error$, err => {
|
||||||
logger.error('error to revalidate cloud workspaces', err);
|
logger.error('error to revalidate cloud workspaces', err);
|
||||||
}),
|
}),
|
||||||
onStart(() => this.isLoading$.next(true)),
|
onStart(() => this.isRevalidating$.next(true)),
|
||||||
onComplete(() => this.isLoading$.next(false))
|
onComplete(() => this.isRevalidating$.next(false))
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
({ accountId }) => {
|
({ accountId }) => {
|
||||||
@@ -186,7 +186,7 @@ export class CloudWorkspaceFlavourProviderService
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
error$ = new LiveData<any>(null);
|
error$ = new LiveData<any>(null);
|
||||||
isLoading$ = new LiveData(false);
|
isRevalidating$ = new LiveData(false);
|
||||||
workspaces$ = new LiveData<WorkspaceMetadata[]>([]);
|
workspaces$ = new LiveData<WorkspaceMetadata[]>([]);
|
||||||
async getWorkspaceProfile(
|
async getWorkspaceProfile(
|
||||||
id: string,
|
id: string,
|
||||||
@@ -277,6 +277,6 @@ export class CloudWorkspaceFlavourProviderService
|
|||||||
}
|
}
|
||||||
|
|
||||||
private waitForLoaded() {
|
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 {
|
revalidate(): void {
|
||||||
// notify livedata to re-scan workspaces
|
// notify livedata to re-scan workspaces
|
||||||
this.notifyChannel.postMessage(null);
|
this.notifyChannel.postMessage(null);
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ import {
|
|||||||
} from './impls/local';
|
} from './impls/local';
|
||||||
import { WorkspaceEngineStorageProvider } from './providers/engine';
|
import { WorkspaceEngineStorageProvider } from './providers/engine';
|
||||||
|
|
||||||
|
export { CloudBlobStorage } from './impls/engine/blob-cloud';
|
||||||
|
|
||||||
export function configureBrowserWorkspaceFlavours(framework: Framework) {
|
export function configureBrowserWorkspaceFlavours(framework: Framework) {
|
||||||
framework
|
framework
|
||||||
.impl(WorkspaceFlavourProvider('LOCAL'), LocalWorkspaceFlavourProvider, [
|
.impl(WorkspaceFlavourProvider('LOCAL'), LocalWorkspaceFlavourProvider, [
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ export const Component = () => {
|
|||||||
|
|
||||||
const workspacesService = useService(WorkspacesService);
|
const workspacesService = useService(WorkspacesService);
|
||||||
const list = useLiveData(workspacesService.list.workspaces$);
|
const list = useLiveData(workspacesService.list.workspaces$);
|
||||||
const listIsLoading = useLiveData(workspacesService.list.isLoading$);
|
const listIsLoading = useLiveData(workspacesService.list.isRevalidating$);
|
||||||
|
|
||||||
const { openPage, jumpToPage } = useNavigateHelper();
|
const { openPage, jumpToPage } = useNavigateHelper();
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
import { AppFallback } from '@affine/core/components/affine/app-container';
|
import { AppFallback } from '@affine/core/components/affine/app-container';
|
||||||
import { useWorkspace } from '@affine/core/hooks/use-workspace';
|
import { viewRoutes } from '@affine/core/router';
|
||||||
import { ZipTransformer } from '@blocksuite/blocks';
|
import { ZipTransformer } from '@blocksuite/blocks';
|
||||||
import type { Workspace } from '@toeverything/infra';
|
import type { Workspace, WorkspaceMetadata } from '@toeverything/infra';
|
||||||
import {
|
import {
|
||||||
FrameworkScope,
|
FrameworkScope,
|
||||||
GlobalContextService,
|
GlobalContextService,
|
||||||
useLiveData,
|
useLiveData,
|
||||||
useService,
|
useServices,
|
||||||
WorkspacesService,
|
WorkspacesService,
|
||||||
} from '@toeverything/infra';
|
} from '@toeverything/infra';
|
||||||
import type { ReactElement } from 'react';
|
import type { ReactElement } from 'react';
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useLayoutEffect, useMemo, useState } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { matchPath, useLocation, useParams } from 'react-router-dom';
|
||||||
|
|
||||||
import { AffineErrorBoundary } from '../../components/affine/affine-error-boundary';
|
import { AffineErrorBoundary } from '../../components/affine/affine-error-boundary';
|
||||||
import { WorkspaceLayout } from '../../layouts/workspace-layout';
|
import { WorkspaceLayout } from '../../layouts/workspace-layout';
|
||||||
@@ -19,6 +19,7 @@ import { WorkbenchRoot } from '../../modules/workbench';
|
|||||||
import { AllWorkspaceModals } from '../../providers/modal-provider';
|
import { AllWorkspaceModals } from '../../providers/modal-provider';
|
||||||
import { performanceRenderLogger } from '../../shared';
|
import { performanceRenderLogger } from '../../shared';
|
||||||
import { PageNotFound } from '../404';
|
import { PageNotFound } from '../404';
|
||||||
|
import { SharePage } from './share/share-page';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
/**
|
/**
|
||||||
@@ -37,24 +38,104 @@ declare global {
|
|||||||
|
|
||||||
export const Component = (): ReactElement => {
|
export const Component = (): ReactElement => {
|
||||||
performanceRenderLogger.debug('WorkspaceLayout');
|
performanceRenderLogger.debug('WorkspaceLayout');
|
||||||
|
const { workspacesService } = useServices({
|
||||||
|
WorkspacesService,
|
||||||
|
});
|
||||||
|
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
const [showNotFound, setShowNotFound] = useState(false);
|
// check if we are in detail doc route, if so, maybe render share page
|
||||||
const workspacesService = useService(WorkspacesService);
|
const detailDocRoute = useMemo(() => {
|
||||||
const listLoading = useLiveData(workspacesService.list.isLoading$);
|
const match = matchPath(
|
||||||
|
'/workspace/:workspaceId/:docId',
|
||||||
|
location.pathname
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
match &&
|
||||||
|
match.params.docId &&
|
||||||
|
match.params.workspaceId &&
|
||||||
|
// TODO(eyhn): need a better way to check if it's a docId
|
||||||
|
viewRoutes.find(route => matchPath(route.path, '/' + match.params.docId))
|
||||||
|
?.path === '/:pageId'
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
docId: match.params.docId,
|
||||||
|
workspaceId: match.params.workspaceId,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}, [location.pathname]);
|
||||||
|
|
||||||
|
const [workspaceNotFound, setWorkspaceNotFound] = useState(false);
|
||||||
|
const listLoading = useLiveData(workspacesService.list.isRevalidating$);
|
||||||
const workspaces = useLiveData(workspacesService.list.workspaces$);
|
const workspaces = useLiveData(workspacesService.list.workspaces$);
|
||||||
|
|
||||||
const meta = useMemo(() => {
|
const meta = useMemo(() => {
|
||||||
return workspaces.find(({ id }) => id === params.workspaceId);
|
return workspaces.find(({ id }) => id === params.workspaceId);
|
||||||
}, [workspaces, params.workspaceId]);
|
}, [workspaces, params.workspaceId]);
|
||||||
|
|
||||||
const workspace = useWorkspace(meta);
|
// if listLoading is false, we can show 404 page, otherwise we should show loading page.
|
||||||
const globalContext = useService(GlobalContextService).globalContext;
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
workspacesService.list.revalidate();
|
if (listLoading === false && meta === undefined) {
|
||||||
}, [workspacesService]);
|
setWorkspaceNotFound(true);
|
||||||
|
}
|
||||||
|
if (meta) {
|
||||||
|
setWorkspaceNotFound(false);
|
||||||
|
}
|
||||||
|
}, [listLoading, meta, workspacesService]);
|
||||||
|
|
||||||
|
// if workspace is not found, we should revalidate in interval
|
||||||
|
useEffect(() => {
|
||||||
|
if (listLoading === false && meta === undefined) {
|
||||||
|
const timer = setInterval(
|
||||||
|
() => workspacesService.list.revalidate(),
|
||||||
|
5000
|
||||||
|
);
|
||||||
|
return () => clearInterval(timer);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}, [listLoading, meta, workspaceNotFound, workspacesService]);
|
||||||
|
|
||||||
|
if (workspaceNotFound) {
|
||||||
|
if (
|
||||||
|
detailDocRoute /* */ &&
|
||||||
|
environment.isBrowser /* only browser has share page */
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<SharePage
|
||||||
|
docId={detailDocRoute.docId}
|
||||||
|
workspaceId={detailDocRoute.workspaceId}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <PageNotFound noPermission />;
|
||||||
|
}
|
||||||
|
if (!meta) {
|
||||||
|
return <AppFallback key="workspaceLoading" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <WorkspacePage meta={meta} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const WorkspacePage = ({ meta }: { meta: WorkspaceMetadata }) => {
|
||||||
|
const { workspacesService, globalContextService } = useServices({
|
||||||
|
WorkspacesService,
|
||||||
|
GlobalContextService,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [workspace, setWorkspace] = useState<Workspace | null>(null);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
const ref = workspacesService.open({ metadata: meta });
|
||||||
|
setWorkspace(ref.workspace);
|
||||||
|
return () => {
|
||||||
|
ref.dispose();
|
||||||
|
};
|
||||||
|
}, [meta, workspacesService]);
|
||||||
|
|
||||||
|
const isRootDocReady =
|
||||||
|
useLiveData(workspace?.engine.rootDocState$.map(v => v.ready)) ?? false;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (workspace) {
|
if (workspace) {
|
||||||
@@ -108,46 +189,17 @@ export const Component = (): ReactElement => {
|
|||||||
input.click();
|
input.click();
|
||||||
};
|
};
|
||||||
localStorage.setItem('last_workspace_id', workspace.id);
|
localStorage.setItem('last_workspace_id', workspace.id);
|
||||||
globalContext.workspaceId.set(workspace.id);
|
globalContextService.globalContext.workspaceId.set(workspace.id);
|
||||||
return () => {
|
return () => {
|
||||||
window.currentWorkspace = undefined;
|
window.currentWorkspace = undefined;
|
||||||
globalContext.workspaceId.set(null);
|
globalContextService.globalContext.workspaceId.set(null);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}, [globalContext, meta, workspace]);
|
}, [globalContextService, workspace]);
|
||||||
|
|
||||||
// avoid doing operation, before workspace is loaded
|
|
||||||
const isRootDocReady =
|
|
||||||
useLiveData(workspace?.engine.rootDocState$.map(v => v.ready)) ?? false;
|
|
||||||
|
|
||||||
// if listLoading is false, we can show 404 page, otherwise we should show loading page.
|
|
||||||
useEffect(() => {
|
|
||||||
if (listLoading === false && meta === undefined) {
|
|
||||||
setShowNotFound(true);
|
|
||||||
}
|
|
||||||
if (meta) {
|
|
||||||
setShowNotFound(false);
|
|
||||||
}
|
|
||||||
}, [listLoading, meta, workspacesService]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (showNotFound) {
|
|
||||||
const timer = setInterval(() => {
|
|
||||||
workspacesService.list.revalidate();
|
|
||||||
}, 3000);
|
|
||||||
return () => {
|
|
||||||
clearInterval(timer);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}, [showNotFound, workspacesService]);
|
|
||||||
|
|
||||||
if (showNotFound) {
|
|
||||||
return <PageNotFound noPermission />;
|
|
||||||
}
|
|
||||||
if (!workspace) {
|
if (!workspace) {
|
||||||
return <AppFallback key="workspaceLoading" />;
|
return null; // skip this, workspace will be set in layout effect
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isRootDocReady) {
|
if (!isRootDocReady) {
|
||||||
|
|||||||
+1
-1
@@ -1,9 +1,9 @@
|
|||||||
|
import { BlocksuiteHeaderTitle } from '@affine/core/components/blocksuite/block-suite-header/title';
|
||||||
import { EditorModeSwitch } from '@affine/core/components/blocksuite/block-suite-mode-switch';
|
import { EditorModeSwitch } from '@affine/core/components/blocksuite/block-suite-mode-switch';
|
||||||
import ShareHeaderRightItem from '@affine/core/components/cloud/share-header-right-item';
|
import ShareHeaderRightItem from '@affine/core/components/cloud/share-header-right-item';
|
||||||
import type { DocCollection } from '@blocksuite/store';
|
import type { DocCollection } from '@blocksuite/store';
|
||||||
import type { DocMode } from '@toeverything/infra';
|
import type { DocMode } from '@toeverything/infra';
|
||||||
|
|
||||||
import { BlocksuiteHeaderTitle } from '../../components/blocksuite/block-suite-header/title/index';
|
|
||||||
import * as styles from './share-header.css';
|
import * as styles from './share-header.css';
|
||||||
|
|
||||||
export function ShareHeader({
|
export function ShareHeader({
|
||||||
+78
-125
@@ -1,8 +1,15 @@
|
|||||||
import { Scrollable } from '@affine/component';
|
import { Scrollable } from '@affine/component';
|
||||||
|
import { AppFallback } from '@affine/core/components/affine/app-container';
|
||||||
|
import { PageDetailEditor } from '@affine/core/components/page-detail-editor';
|
||||||
|
import { SharePageNotFoundError } from '@affine/core/components/share-page-not-found-error';
|
||||||
|
import { AppContainer, MainContainer } from '@affine/core/components/workspace';
|
||||||
import { useActiveBlocksuiteEditor } from '@affine/core/hooks/use-block-suite-editor';
|
import { useActiveBlocksuiteEditor } from '@affine/core/hooks/use-block-suite-editor';
|
||||||
import { usePageDocumentTitle } from '@affine/core/hooks/use-global-state';
|
import { usePageDocumentTitle } from '@affine/core/hooks/use-global-state';
|
||||||
import { AuthService } from '@affine/core/modules/cloud';
|
import { AuthService } from '@affine/core/modules/cloud';
|
||||||
import { type Editor, EditorsService } from '@affine/core/modules/editor';
|
import { type Editor, EditorsService } from '@affine/core/modules/editor';
|
||||||
|
import { PeekViewManagerModal } from '@affine/core/modules/peek-view';
|
||||||
|
import { ShareReaderService } from '@affine/core/modules/share-doc';
|
||||||
|
import { CloudBlobStorage } from '@affine/core/modules/workspace-engine';
|
||||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import { noop } from '@blocksuite/global/utils';
|
import { noop } from '@blocksuite/global/utils';
|
||||||
@@ -17,118 +24,81 @@ import {
|
|||||||
ReadonlyDocStorage,
|
ReadonlyDocStorage,
|
||||||
useLiveData,
|
useLiveData,
|
||||||
useService,
|
useService,
|
||||||
WorkspaceFlavourProvider,
|
useServices,
|
||||||
WorkspacesService,
|
WorkspacesService,
|
||||||
} from '@toeverything/infra';
|
} from '@toeverything/infra';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import type { LoaderFunction } from 'react-router-dom';
|
|
||||||
import {
|
|
||||||
isRouteErrorResponse,
|
|
||||||
redirect,
|
|
||||||
useLoaderData,
|
|
||||||
useRouteError,
|
|
||||||
} from 'react-router-dom';
|
|
||||||
|
|
||||||
import { AppContainer } from '../../components/affine/app-container';
|
import { PageNotFound } from '../../404';
|
||||||
import { PageDetailEditor } from '../../components/page-detail-editor';
|
|
||||||
import { SharePageNotFoundError } from '../../components/share-page-not-found-error';
|
|
||||||
import { MainContainer } from '../../components/workspace';
|
|
||||||
import { PeekViewManagerModal } from '../../modules/peek-view';
|
|
||||||
import { CloudBlobStorage } from '../../modules/workspace-engine/impls/engine/blob-cloud';
|
|
||||||
import * as styles from './share-detail-page.css';
|
|
||||||
import { ShareFooter } from './share-footer';
|
import { ShareFooter } from './share-footer';
|
||||||
import { ShareHeader } from './share-header';
|
import { ShareHeader } from './share-header';
|
||||||
|
import * as styles from './share-page.css';
|
||||||
|
|
||||||
type DocPublishMode = 'edgeless' | 'page';
|
export const SharePage = ({
|
||||||
|
workspaceId,
|
||||||
export type CloudDoc = {
|
docId,
|
||||||
arrayBuffer: ArrayBuffer;
|
}: {
|
||||||
publishMode: DocPublishMode;
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function downloadBinaryFromCloud(
|
|
||||||
rootGuid: string,
|
|
||||||
pageGuid: string
|
|
||||||
): Promise<CloudDoc | null> {
|
|
||||||
const response = await fetch(`/api/workspaces/${rootGuid}/docs/${pageGuid}`);
|
|
||||||
if (response.ok) {
|
|
||||||
const publishMode = (response.headers.get('publish-mode') ||
|
|
||||||
'page') as DocPublishMode;
|
|
||||||
const arrayBuffer = await response.arrayBuffer();
|
|
||||||
|
|
||||||
// return both arrayBuffer and publish mode
|
|
||||||
return { arrayBuffer, publishMode };
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
type LoaderData = {
|
|
||||||
pageId: string;
|
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
publishMode: DocMode;
|
docId: string;
|
||||||
pageArrayBuffer: ArrayBuffer;
|
}) => {
|
||||||
workspaceArrayBuffer: ArrayBuffer;
|
const { shareReaderService } = useServices({
|
||||||
};
|
ShareReaderService,
|
||||||
|
});
|
||||||
|
|
||||||
function assertDownloadResponse(
|
const isLoading = useLiveData(shareReaderService.reader.isLoading$);
|
||||||
value: CloudDoc | null
|
const error = useLiveData(shareReaderService.reader.error$);
|
||||||
): asserts value is CloudDoc {
|
const data = useLiveData(shareReaderService.reader.data$);
|
||||||
if (
|
|
||||||
!value ||
|
|
||||||
!((value as CloudDoc).arrayBuffer instanceof ArrayBuffer) ||
|
|
||||||
typeof (value as CloudDoc).publishMode !== 'string'
|
|
||||||
) {
|
|
||||||
throw new Error('value is not a valid download response');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const loader: LoaderFunction = async ({ params }) => {
|
useEffect(() => {
|
||||||
const workspaceId = params?.workspaceId;
|
shareReaderService.reader.loadShare({ workspaceId, docId });
|
||||||
const pageId = params?.pageId;
|
}, [shareReaderService, docId, workspaceId]);
|
||||||
if (!workspaceId || !pageId) {
|
|
||||||
return redirect('/404');
|
if (isLoading) {
|
||||||
|
return <AppFallback />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [workspaceResponse, pageResponse] = await Promise.all([
|
if (error) {
|
||||||
downloadBinaryFromCloud(workspaceId, workspaceId),
|
// TODO(@eyhn): show error details
|
||||||
downloadBinaryFromCloud(workspaceId, pageId),
|
return <SharePageNotFoundError />;
|
||||||
]);
|
}
|
||||||
assertDownloadResponse(workspaceResponse);
|
|
||||||
const { arrayBuffer: workspaceArrayBuffer } = workspaceResponse;
|
|
||||||
assertDownloadResponse(pageResponse);
|
|
||||||
const { arrayBuffer: pageArrayBuffer, publishMode } = pageResponse;
|
|
||||||
|
|
||||||
return {
|
if (data) {
|
||||||
workspaceId,
|
return (
|
||||||
pageId,
|
<SharePageInner
|
||||||
publishMode,
|
workspaceId={data.workspaceId}
|
||||||
workspaceArrayBuffer,
|
docId={data.docId}
|
||||||
pageArrayBuffer,
|
workspaceBinary={data.workspaceBinary}
|
||||||
} satisfies LoaderData;
|
docBinary={data.docBinary}
|
||||||
|
publishMode={data.publishMode}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return <PageNotFound noPermission />;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Component = () => {
|
const SharePageInner = ({
|
||||||
const {
|
workspaceId,
|
||||||
workspaceId,
|
docId,
|
||||||
pageId: docId,
|
workspaceBinary,
|
||||||
publishMode,
|
docBinary,
|
||||||
workspaceArrayBuffer,
|
publishMode = 'page',
|
||||||
pageArrayBuffer,
|
}: {
|
||||||
} = useLoaderData() as LoaderData;
|
workspaceId: string;
|
||||||
|
docId: string;
|
||||||
|
workspaceBinary: Uint8Array;
|
||||||
|
docBinary: Uint8Array;
|
||||||
|
publishMode?: DocMode;
|
||||||
|
}) => {
|
||||||
|
const t = useI18n();
|
||||||
const workspacesService = useService(WorkspacesService);
|
const workspacesService = useService(WorkspacesService);
|
||||||
|
|
||||||
const t = useI18n();
|
|
||||||
const [workspace, setWorkspace] = useState<Workspace | null>(null);
|
const [workspace, setWorkspace] = useState<Workspace | null>(null);
|
||||||
const [page, setPage] = useState<Doc | null>(null);
|
const [page, setPage] = useState<Doc | null>(null);
|
||||||
const [editor, setEditor] = useState<Editor | null>(null);
|
const [editor, setEditor] = useState<Editor | null>(null);
|
||||||
const [_, setActiveBlocksuiteEditor] = useActiveBlocksuiteEditor();
|
const [_, setActiveBlocksuiteEditor] = useActiveBlocksuiteEditor();
|
||||||
|
|
||||||
const defaultCloudProvider = workspacesService.framework.get(
|
|
||||||
WorkspaceFlavourProvider('CLOUD')
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// create a workspace for share page
|
// create a workspace for share page
|
||||||
const { workspace } = workspacesService.open(
|
const { workspace } = workspacesService.open(
|
||||||
@@ -140,28 +110,23 @@ export const Component = () => {
|
|||||||
isSharedMode: true,
|
isSharedMode: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
...defaultCloudProvider,
|
getDocStorage() {
|
||||||
getEngineProvider(workspaceId) {
|
return new ReadonlyDocStorage({
|
||||||
return {
|
[workspaceId]: workspaceBinary,
|
||||||
getDocStorage() {
|
[docId]: docBinary,
|
||||||
return new ReadonlyDocStorage({
|
});
|
||||||
[workspaceId]: new Uint8Array(workspaceArrayBuffer),
|
},
|
||||||
[docId]: new Uint8Array(pageArrayBuffer),
|
getAwarenessConnections() {
|
||||||
});
|
return [];
|
||||||
},
|
},
|
||||||
getAwarenessConnections() {
|
getDocServer() {
|
||||||
return [];
|
return null;
|
||||||
},
|
},
|
||||||
getDocServer() {
|
getLocalBlobStorage() {
|
||||||
return null;
|
return EmptyBlobStorage;
|
||||||
},
|
},
|
||||||
getLocalBlobStorage() {
|
getRemoteBlobStorages() {
|
||||||
return EmptyBlobStorage;
|
return [new CloudBlobStorage(workspaceId)];
|
||||||
},
|
|
||||||
getRemoteBlobStorages() {
|
|
||||||
return [new CloudBlobStorage(workspaceId)];
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -188,13 +153,12 @@ export const Component = () => {
|
|||||||
console.error(err);
|
console.error(err);
|
||||||
});
|
});
|
||||||
}, [
|
}, [
|
||||||
defaultCloudProvider,
|
|
||||||
pageArrayBuffer,
|
|
||||||
docId,
|
docId,
|
||||||
workspaceArrayBuffer,
|
|
||||||
workspaceId,
|
workspaceId,
|
||||||
workspacesService,
|
workspacesService,
|
||||||
publishMode,
|
publishMode,
|
||||||
|
workspaceBinary,
|
||||||
|
docBinary,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const pageTitle = useLiveData(page?.title$);
|
const pageTitle = useLiveData(page?.title$);
|
||||||
@@ -269,14 +233,3 @@ export const Component = () => {
|
|||||||
</FrameworkScope>
|
</FrameworkScope>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export function ErrorBoundary() {
|
|
||||||
const error = useRouteError();
|
|
||||||
return isRouteErrorResponse(error) ? (
|
|
||||||
<h1>
|
|
||||||
{error.status} {error.statusText}
|
|
||||||
</h1>
|
|
||||||
) : (
|
|
||||||
<SharePageNotFoundError />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -42,7 +42,9 @@ export const topLevelRoutes = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/share/:workspaceId/:pageId',
|
path: '/share/:workspaceId/:pageId',
|
||||||
lazy: () => import('./pages/share/share-detail-page'),
|
loader: ({ params }) => {
|
||||||
|
return redirect(`/workspace/${params.workspaceId}/${params.pageId}`);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/404',
|
path: '/404',
|
||||||
|
|||||||
@@ -26,6 +26,13 @@ export class UserFriendlyError implements UserFriendlyErrorResponse {
|
|||||||
return new UserFriendlyError(response.extensions);
|
return new UserFriendlyError(response.extensions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
'originError' in response &&
|
||||||
|
response.originError instanceof UserFriendlyError
|
||||||
|
) {
|
||||||
|
return response.originError as UserFriendlyError;
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
response &&
|
response &&
|
||||||
typeof response === 'object' &&
|
typeof response === 'object' &&
|
||||||
|
|||||||
Reference in New Issue
Block a user