mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-11 05:58:56 +08:00
feat(server): synthetic root doc (#14794)
This commit is contained in:
@@ -1,9 +1,13 @@
|
||||
import { UserFriendlyError } from '@affine/error';
|
||||
import { TimeoutError } from 'rxjs';
|
||||
import { afterEach, describe, expect, test, vi } from 'vitest';
|
||||
|
||||
import {
|
||||
fetchSharedPublishMode,
|
||||
getResolvedPublishMode,
|
||||
getSearchWithMode,
|
||||
isSharePagePermissionError,
|
||||
isSharePageTimeoutError,
|
||||
parsePublishMode,
|
||||
} from '../desktop/pages/workspace/share/share-page.utils';
|
||||
|
||||
@@ -56,7 +60,7 @@ describe('fetchSharedPublishMode', () => {
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
new URL(
|
||||
'/api/workspaces/workspace-id/docs/doc-id',
|
||||
'/api/workspaces/workspace-id/public-docs/doc-id',
|
||||
'https://app.affine.pro'
|
||||
),
|
||||
expect.objectContaining({ method: 'HEAD' })
|
||||
@@ -95,3 +99,34 @@ describe('getSearchWithMode', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('share page error helpers', () => {
|
||||
test('recognizes permission errors only', () => {
|
||||
const permissionError = new UserFriendlyError({
|
||||
status: 403,
|
||||
code: 'DOC_ACTION_DENIED',
|
||||
type: 'DOC_ACTION_DENIED',
|
||||
name: 'DOC_ACTION_DENIED',
|
||||
message: 'forbidden',
|
||||
});
|
||||
|
||||
expect(isSharePagePermissionError(permissionError)).toBe(true);
|
||||
expect(isSharePagePermissionError(new TimeoutError())).toBe(false);
|
||||
expect(isSharePagePermissionError(new Error('x'))).toBe(false);
|
||||
});
|
||||
|
||||
test('recognizes timeout errors only', () => {
|
||||
expect(isSharePageTimeoutError(new TimeoutError())).toBe(true);
|
||||
expect(
|
||||
isSharePageTimeoutError(
|
||||
new UserFriendlyError({
|
||||
status: 403,
|
||||
code: 'DOC_ACTION_DENIED',
|
||||
type: 'DOC_ACTION_DENIED',
|
||||
name: 'DOC_ACTION_DENIED',
|
||||
message: 'forbidden',
|
||||
})
|
||||
)
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -32,6 +32,7 @@ import { FrameworkScope, useLiveData, useService } from '@toeverything/infra';
|
||||
import clsx from 'clsx';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { filter, firstValueFrom, timeout } from 'rxjs';
|
||||
|
||||
import { PageNotFound } from '../../404';
|
||||
import { ShareFooter } from './share-footer';
|
||||
@@ -40,9 +41,24 @@ import * as styles from './share-page.css';
|
||||
import {
|
||||
fetchSharedPublishMode,
|
||||
getResolvedPublishMode,
|
||||
isSharePagePermissionError,
|
||||
isSharePageTimeoutError,
|
||||
} from './share-page.utils';
|
||||
import { useSharedModeQuerySync } from './use-shared-mode-query-sync';
|
||||
|
||||
const waitForSharedDocRecord = async (
|
||||
docsService: DocsService,
|
||||
docId: string
|
||||
): Promise<void> => {
|
||||
if (docsService.list.doc$(docId).value) {
|
||||
return;
|
||||
}
|
||||
|
||||
await firstValueFrom(
|
||||
docsService.list.doc$(docId).pipe(filter(Boolean), timeout(3000))
|
||||
);
|
||||
};
|
||||
|
||||
const useUpdateBasename = (workspace: Workspace | null) => {
|
||||
const location = useLocation();
|
||||
const basename = location.pathname.match(/\/workspace\/[^/]+/g)?.[0] ?? '/';
|
||||
@@ -131,6 +147,7 @@ const SharePageInner = ({
|
||||
const [page, setPage] = useState<Doc | null>(null);
|
||||
const [editor, setEditor] = useState<Editor | null>(null);
|
||||
const [noPermission, setNoPermission] = useState(false);
|
||||
const [loadFailed, setLoadFailed] = useState(false);
|
||||
const [fetchedPublishMode, setFetchedPublishMode] = useState<
|
||||
DocMode | null | undefined
|
||||
>(() => (publishMode === undefined ? undefined : null));
|
||||
@@ -198,6 +215,7 @@ const SharePageInner = ({
|
||||
name: 'StaticCloudDocStorage',
|
||||
opts: {
|
||||
id: workspaceId,
|
||||
publicRootDocId: docId,
|
||||
serverBaseUrl: serverService.server.baseUrl,
|
||||
},
|
||||
},
|
||||
@@ -218,7 +236,10 @@ const SharePageInner = ({
|
||||
sharedWorkspace.engine.doc
|
||||
.waitForDocLoaded(sharedWorkspace.id)
|
||||
.then(async () => {
|
||||
const { doc } = sharedWorkspace.scope.get(DocsService).open(docId);
|
||||
const docsService = sharedWorkspace.scope.get(DocsService);
|
||||
await waitForSharedDocRecord(docsService, docId);
|
||||
|
||||
const { doc } = docsService.open(docId);
|
||||
doc.blockSuiteDoc.load();
|
||||
doc.blockSuiteDoc.readonly = true;
|
||||
|
||||
@@ -241,7 +262,17 @@ const SharePageInner = ({
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
setNoPermission(true);
|
||||
if (isSharePagePermissionError(err)) {
|
||||
setNoPermission(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isSharePageTimeoutError(err)) {
|
||||
setLoadFailed(true);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoadFailed(true);
|
||||
});
|
||||
}, [
|
||||
docId,
|
||||
@@ -308,6 +339,10 @@ const SharePageInner = ({
|
||||
return <PageNotFound noPermission />;
|
||||
}
|
||||
|
||||
if (loadFailed) {
|
||||
return <PageNotFound />;
|
||||
}
|
||||
|
||||
if (!workspace || !page || !editor || !currentPublishMode) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { UserFriendlyError } from '@affine/error';
|
||||
import { type DocMode, DocModes } from '@blocksuite/affine/model';
|
||||
import { TimeoutError } from 'rxjs';
|
||||
|
||||
export const getResolvedPublishMode = (
|
||||
queryMode: DocMode | null,
|
||||
@@ -35,7 +37,7 @@ export const fetchSharedPublishMode = async ({
|
||||
signal?: AbortSignal;
|
||||
}): Promise<DocMode | null> => {
|
||||
const url = new URL(
|
||||
`/api/workspaces/${encodeURIComponent(workspaceId)}/docs/${encodeURIComponent(docId)}`,
|
||||
`/api/workspaces/${encodeURIComponent(workspaceId)}/public-docs/${encodeURIComponent(docId)}`,
|
||||
serverBaseUrl
|
||||
);
|
||||
const headers = {
|
||||
@@ -71,3 +73,12 @@ export const getSearchWithMode = (search: string, mode: DocMode) => {
|
||||
const nextSearch = searchParams.toString();
|
||||
return nextSearch ? `?${nextSearch}` : '';
|
||||
};
|
||||
|
||||
export const isSharePagePermissionError = (error: unknown) =>
|
||||
error instanceof UserFriendlyError &&
|
||||
(error.isStatus(403) ||
|
||||
error.name === 'DOC_ACTION_DENIED' ||
|
||||
error.name === 'SPACE_ACCESS_DENIED');
|
||||
|
||||
export const isSharePageTimeoutError = (error: unknown) =>
|
||||
error instanceof TimeoutError;
|
||||
|
||||
+7
-2
@@ -1,5 +1,6 @@
|
||||
import { Menu, MenuItem, MenuTrigger, notify } from '@affine/component';
|
||||
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
||||
import { EditorService } from '@affine/core/modules/editor';
|
||||
import { ShareInfoService } from '@affine/core/modules/share-doc';
|
||||
import { UserFriendlyError } from '@affine/error';
|
||||
import { PublicDocMode } from '@affine/graphql';
|
||||
@@ -19,11 +20,13 @@ import * as styles from './styles.css';
|
||||
|
||||
export const PublicDoc = ({ disabled }: { disabled?: boolean }) => {
|
||||
const t = useI18n();
|
||||
const editorService = useService(EditorService);
|
||||
const shareInfoService = useService(ShareInfoService);
|
||||
const isSharedPage = useLiveData(shareInfoService.shareInfo.isShared$);
|
||||
const isRevalidating = useLiveData(
|
||||
shareInfoService.shareInfo.isRevalidating$
|
||||
);
|
||||
const currentMode = useLiveData(editorService.editor.mode$);
|
||||
|
||||
useEffect(() => {
|
||||
shareInfoService.shareInfo.revalidate();
|
||||
@@ -63,7 +66,9 @@ export const PublicDoc = ({ disabled }: { disabled?: boolean }) => {
|
||||
}
|
||||
try {
|
||||
// TODO(@JimmFly): remove mode when we have a better way to handle it
|
||||
await shareInfoService.shareInfo.enableShare(PublicDocMode.Page);
|
||||
await shareInfoService.shareInfo.enableShare(
|
||||
currentMode === 'edgeless' ? PublicDocMode.Edgeless : PublicDocMode.Page
|
||||
);
|
||||
track.$.sharePanel.$.createShareLink();
|
||||
notify.success({
|
||||
title:
|
||||
@@ -84,7 +89,7 @@ export const PublicDoc = ({ disabled }: { disabled?: boolean }) => {
|
||||
message: err.message,
|
||||
});
|
||||
}
|
||||
}, [isSharedPage, shareInfoService.shareInfo, t]);
|
||||
}, [currentMode, isSharedPage, shareInfoService.shareInfo, t]);
|
||||
|
||||
return (
|
||||
<div className={styles.rowContainerStyle}>
|
||||
|
||||
Reference in New Issue
Block a user