mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-04 08:38:34 +00:00
fix(core): handle the getSession network error properly (#4909)
If network offline or API error happens, the `session` returned by the `useSession` hook will be null, so we can't assume it is not null. There should be following changes: 1. create a page in ErrorBoundary to let the user refetch the session. 2. The `SessionProvider` stop to pull the new session once the session is null, we need to figure out a way to pull the new session when the network is back or the user click the refetch button.
This commit is contained in:
@@ -7,12 +7,7 @@ import { useCallback, useState } from 'react';
|
||||
import { pushNotificationAtom } from '../notification-center';
|
||||
import { AuthPageContainer } from './auth-page-container';
|
||||
import { SetPassword } from './set-password';
|
||||
type User = {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
image: string;
|
||||
};
|
||||
import type { User } from './type';
|
||||
|
||||
export const ChangePasswordPage: FC<{
|
||||
user: User;
|
||||
|
||||
@@ -13,3 +13,4 @@ export * from './set-password-page';
|
||||
export * from './sign-in-page-container';
|
||||
export * from './sign-in-success-page';
|
||||
export * from './sign-up-page';
|
||||
export type { User } from './type';
|
||||
|
||||
@@ -7,13 +7,7 @@ import { useCallback, useState } from 'react';
|
||||
import { pushNotificationAtom } from '../notification-center';
|
||||
import { AuthPageContainer } from './auth-page-container';
|
||||
import { SetPassword } from './set-password';
|
||||
|
||||
type User = {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
image: string;
|
||||
};
|
||||
import type { User } from './type';
|
||||
|
||||
export const SetPasswordPage: FC<{
|
||||
user: User;
|
||||
|
||||
@@ -7,12 +7,7 @@ import { useCallback, useState } from 'react';
|
||||
import { pushNotificationAtom } from '../notification-center';
|
||||
import { AuthPageContainer } from './auth-page-container';
|
||||
import { SetPassword } from './set-password';
|
||||
type User = {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
image: string;
|
||||
};
|
||||
import type { User } from './type';
|
||||
|
||||
export const SignUpPage: FC<{
|
||||
user: User;
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
export interface User {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
image?: string | null;
|
||||
}
|
||||
@@ -114,7 +114,10 @@ export const createConfiguration: (
|
||||
buildFlags.mode === 'production'
|
||||
? 'js/chunk.[name]-[contenthash:8].js'
|
||||
: 'js/chunk.[name].js',
|
||||
assetModuleFilename: 'assets/[name]-[contenthash:8][ext][query]',
|
||||
assetModuleFilename:
|
||||
buildFlags.mode === 'production'
|
||||
? 'assets/[name]-[contenthash:8][ext][query]'
|
||||
: '[name][ext]',
|
||||
devtoolModuleFilenameTemplate: 'webpack://[namespace]/[resource-path]',
|
||||
hotUpdateChunkFilename: 'hot/[id].[fullhash].js',
|
||||
hotUpdateMainFilename: 'hot/[runtime].[fullhash].json',
|
||||
@@ -292,7 +295,7 @@ export const createConfiguration: (
|
||||
},
|
||||
},
|
||||
],
|
||||
exclude: [/node_modules/],
|
||||
exclude: [/node_modules/, /\.assets\.svg$/],
|
||||
},
|
||||
{
|
||||
test: /\.(png|jpg|gif|svg|webp|mp4)$/,
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const errorLayout = style({
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
});
|
||||
|
||||
export const errorDetailStyle = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
maxWidth: '420px',
|
||||
});
|
||||
|
||||
export const errorTitle = style({
|
||||
fontSize: '36px',
|
||||
lineHeight: '44px',
|
||||
fontWeight: 700,
|
||||
});
|
||||
|
||||
export const errorImage = style({
|
||||
height: '178px',
|
||||
maxWidth: '400px',
|
||||
flexGrow: 1,
|
||||
});
|
||||
|
||||
export const errorDescription = style({
|
||||
marginTop: '24px',
|
||||
});
|
||||
|
||||
export const errorRetryButton = style({
|
||||
marginTop: '24px',
|
||||
width: '94px',
|
||||
});
|
||||
|
||||
export const errorDivider = style({
|
||||
width: '20px',
|
||||
height: '100%',
|
||||
});
|
||||
@@ -0,0 +1,176 @@
|
||||
import type {
|
||||
QueryParamError,
|
||||
Unreachable,
|
||||
WorkspaceNotFoundError,
|
||||
} from '@affine/env/constant';
|
||||
import { PageNotFoundError } from '@affine/env/constant';
|
||||
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
|
||||
import { Button } from '@toeverything/components/button';
|
||||
import {
|
||||
currentPageIdAtom,
|
||||
currentWorkspaceIdAtom,
|
||||
getCurrentStore,
|
||||
} from '@toeverything/infra/atom';
|
||||
import { useAtomValue } from 'jotai/react';
|
||||
import { Provider } from 'jotai/react';
|
||||
import type { ErrorInfo, ReactElement, ReactNode } from 'react';
|
||||
import type React from 'react';
|
||||
import { Component, useEffect } from 'react';
|
||||
import { useLocation, useParams } from 'react-router-dom';
|
||||
|
||||
import {
|
||||
RecoverableError,
|
||||
SessionFetchErrorRightAfterLoginOrSignUp,
|
||||
} from '../../unexpected-application-state/errors';
|
||||
import {
|
||||
errorDescription,
|
||||
errorDetailStyle,
|
||||
errorDivider,
|
||||
errorImage,
|
||||
errorLayout,
|
||||
errorRetryButton,
|
||||
errorTitle,
|
||||
} from './affine-error-boundary.css';
|
||||
import errorBackground from './error-status.assets.svg';
|
||||
|
||||
export type AffineErrorBoundaryProps = React.PropsWithChildren;
|
||||
|
||||
type AffineError =
|
||||
| QueryParamError
|
||||
| Unreachable
|
||||
| WorkspaceNotFoundError
|
||||
| PageNotFoundError
|
||||
| Error
|
||||
| SessionFetchErrorRightAfterLoginOrSignUp;
|
||||
|
||||
interface AffineErrorBoundaryState {
|
||||
error: AffineError | null;
|
||||
canRetryRecoveredError: boolean;
|
||||
}
|
||||
|
||||
export const DumpInfo = () => {
|
||||
const location = useLocation();
|
||||
const metadata = useAtomValue(rootWorkspacesMetadataAtom);
|
||||
const currentWorkspaceId = useAtomValue(currentWorkspaceIdAtom);
|
||||
const currentPageId = useAtomValue(currentPageIdAtom);
|
||||
const path = location.pathname;
|
||||
const query = useParams();
|
||||
useEffect(() => {
|
||||
console.info('DumpInfo', {
|
||||
path,
|
||||
query,
|
||||
currentWorkspaceId,
|
||||
currentPageId,
|
||||
metadata,
|
||||
});
|
||||
}, [path, query, currentWorkspaceId, currentPageId, metadata]);
|
||||
return null;
|
||||
};
|
||||
|
||||
export class AffineErrorBoundary extends Component<
|
||||
AffineErrorBoundaryProps,
|
||||
AffineErrorBoundaryState
|
||||
> {
|
||||
override state: AffineErrorBoundaryState = {
|
||||
error: null,
|
||||
canRetryRecoveredError: true,
|
||||
};
|
||||
|
||||
private readonly handleRecoverableRetry = () => {
|
||||
if (this.state.error instanceof RecoverableError) {
|
||||
if (this.state.error.canRetry()) {
|
||||
this.state.error.retry();
|
||||
this.setState({
|
||||
error: this.state.error,
|
||||
canRetryRecoveredError: this.state.error.canRetry(),
|
||||
});
|
||||
} else {
|
||||
document.location.reload();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
static getDerivedStateFromError(
|
||||
error: AffineError
|
||||
): AffineErrorBoundaryState {
|
||||
return {
|
||||
error,
|
||||
canRetryRecoveredError:
|
||||
error instanceof RecoverableError ? error.canRetry() : true,
|
||||
};
|
||||
}
|
||||
|
||||
override componentDidCatch(error: AffineError, errorInfo: ErrorInfo) {
|
||||
console.error('Uncaught error:', error, errorInfo);
|
||||
}
|
||||
|
||||
override render(): ReactNode {
|
||||
if (this.state.error) {
|
||||
let errorDetail: ReactElement | null = null;
|
||||
const error = this.state.error;
|
||||
if (error instanceof PageNotFoundError) {
|
||||
errorDetail = (
|
||||
<>
|
||||
<h1>Sorry.. there was an error</h1>
|
||||
<>
|
||||
<span> Page error </span>
|
||||
<span>
|
||||
Cannot find page {error.pageId} in workspace{' '}
|
||||
{error.workspace.id}
|
||||
</span>
|
||||
</>
|
||||
</>
|
||||
);
|
||||
} else if (error instanceof SessionFetchErrorRightAfterLoginOrSignUp) {
|
||||
const retryButtonDesc = this.state.canRetryRecoveredError
|
||||
? 'Refetch'
|
||||
: 'Reload';
|
||||
errorDetail = (
|
||||
<>
|
||||
<h1 className={errorTitle}>Sorry.. there was an error</h1>
|
||||
<span className={errorDescription}> Fetching session failed </span>
|
||||
<span className={errorDescription}>
|
||||
If you are still experiencing this issue, please{' '}
|
||||
<a
|
||||
style={{ color: 'var(--affine-primary-color)' }}
|
||||
href="https://community.affine.pro"
|
||||
target="__blank"
|
||||
>
|
||||
contact us through the community.
|
||||
</a>
|
||||
</span>
|
||||
<Button
|
||||
className={errorRetryButton}
|
||||
onClick={this.handleRecoverableRetry}
|
||||
type="primary"
|
||||
>
|
||||
{retryButtonDesc}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
errorDetail = (
|
||||
<>
|
||||
<h1>Sorry.. there was an error</h1>
|
||||
{error.message ?? error.toString()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className={errorLayout}>
|
||||
<div className={errorDetailStyle}>{errorDetail}</div>
|
||||
<span className={errorDivider} />
|
||||
<div
|
||||
className={errorImage}
|
||||
style={{ backgroundImage: `url(${errorBackground})` }}
|
||||
/>
|
||||
<Provider key="JotaiProvider" store={getCurrentStore()}>
|
||||
<DumpInfo />
|
||||
</Provider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
import type {
|
||||
QueryParamError,
|
||||
Unreachable,
|
||||
WorkspaceNotFoundError,
|
||||
} from '@affine/env/constant';
|
||||
import { PageNotFoundError } from '@affine/env/constant';
|
||||
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
|
||||
import {
|
||||
currentPageIdAtom,
|
||||
currentWorkspaceIdAtom,
|
||||
getCurrentStore,
|
||||
} from '@toeverything/infra/atom';
|
||||
import { useAtomValue } from 'jotai/react';
|
||||
import { Provider } from 'jotai/react';
|
||||
import type { ErrorInfo, ReactElement, ReactNode } from 'react';
|
||||
import type React from 'react';
|
||||
import { Component } from 'react';
|
||||
import { useLocation, useParams } from 'react-router-dom';
|
||||
export type AffineErrorBoundaryProps = React.PropsWithChildren;
|
||||
|
||||
type AffineError =
|
||||
| QueryParamError
|
||||
| Unreachable
|
||||
| WorkspaceNotFoundError
|
||||
| PageNotFoundError
|
||||
| Error;
|
||||
|
||||
interface AffineErrorBoundaryState {
|
||||
error: AffineError | null;
|
||||
}
|
||||
|
||||
export const DumpInfo = () => {
|
||||
const location = useLocation();
|
||||
const metadata = useAtomValue(rootWorkspacesMetadataAtom);
|
||||
const currentWorkspaceId = useAtomValue(currentWorkspaceIdAtom);
|
||||
const currentPageId = useAtomValue(currentPageIdAtom);
|
||||
const path = location.pathname;
|
||||
const query = useParams();
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
Please copy the following information and send it to the developer.
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
border: '1px solid red',
|
||||
}}
|
||||
>
|
||||
<div>path: {path}</div>
|
||||
<div>query: {JSON.stringify(query)}</div>
|
||||
<div>currentWorkspaceId: {currentWorkspaceId}</div>
|
||||
<div>currentPageId: {currentPageId}</div>
|
||||
<div>metadata: {JSON.stringify(metadata)}</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export class AffineErrorBoundary extends Component<
|
||||
AffineErrorBoundaryProps,
|
||||
AffineErrorBoundaryState
|
||||
> {
|
||||
public override state: AffineErrorBoundaryState = {
|
||||
error: null,
|
||||
};
|
||||
|
||||
public static getDerivedStateFromError(
|
||||
error: AffineError
|
||||
): AffineErrorBoundaryState {
|
||||
return { error };
|
||||
}
|
||||
|
||||
public override componentDidCatch(error: AffineError, errorInfo: ErrorInfo) {
|
||||
console.error('Uncaught error:', error, errorInfo);
|
||||
}
|
||||
|
||||
public override render(): ReactNode {
|
||||
if (this.state.error) {
|
||||
let errorDetail: ReactElement | null = null;
|
||||
const error = this.state.error;
|
||||
if (error instanceof PageNotFoundError) {
|
||||
errorDetail = (
|
||||
<>
|
||||
<h1>Sorry.. there was an error</h1>
|
||||
<>
|
||||
<span> Page error </span>
|
||||
<span>
|
||||
Cannot find page {error.pageId} in workspace{' '}
|
||||
{error.workspace.id}
|
||||
</span>
|
||||
</>
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
errorDetail = (
|
||||
<>
|
||||
<h1>Sorry.. there was an error</h1>
|
||||
{error.message ?? error.toString()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{errorDetail}
|
||||
<Provider key="JotaiProvider" store={getCurrentStore()}>
|
||||
<DumpInfo />
|
||||
</Provider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<svg width="402" height="178" viewBox="0 0 402 178" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M93.7434 129.308H1L71.7965 15.1021V167.142" stroke="#121212" stroke-width="2" stroke-linecap="square" stroke-linejoin="bevel" />
|
||||
<ellipse cx="71.4426" cy="14.7483" rx="3.89381" ry="3.88938" fill="#121212" />
|
||||
<ellipse cx="93.3894" cy="129.308" rx="3.89381" ry="3.88938" fill="#121212" />
|
||||
<path d="M140.357 27.1235L264.717 151.342" stroke="#E3E2E4" />
|
||||
<rect x="140.392" y="27.1582" width="124.291" height="124.149" stroke="#E3E2E4" />
|
||||
<path d="M264.339 89.4652C264.895 85.8242 265.183 82.0954 265.183 78.2995C265.183 37.7024 232.235 4.79199 191.592 4.79199C150.948 4.79199 118 37.7024 118 78.2995C118 118.897 150.948 151.807 191.592 151.807C195.23 151.807 198.807 151.543 202.304 151.034" stroke="#E3E2E4" />
|
||||
<path d="M202.304 150.964C205.949 151.519 209.682 151.807 213.483 151.807C254.126 151.807 287.074 118.897 287.074 78.2995C287.074 37.7024 254.126 4.79199 213.483 4.79199C172.839 4.79199 139.892 37.7024 139.892 78.2995C139.892 82.0955 140.18 85.8242 140.735 89.4652" stroke="#E3E2E4" />
|
||||
<path d="M140.2 89.4652C139.69 92.9584 139.426 96.5312 139.426 100.166C139.426 140.763 172.374 173.673 213.017 173.673C253.66 173.673 286.608 140.763 286.608 100.166C286.608 59.5686 253.66 26.6582 213.017 26.6582C209.378 26.6582 205.801 26.922 202.304 27.4314" stroke="#E3E2E4" />
|
||||
<path d="M202.304 27.4314C198.807 26.922 195.23 26.6582 191.592 26.6582C150.948 26.6582 118 59.5686 118 100.166C118 140.763 150.948 173.673 191.592 173.673C232.235 173.673 265.183 140.763 265.183 100.166C265.183 96.5312 264.919 92.9584 264.409 89.4652" stroke="#E3E2E4" />
|
||||
<path d="M264.717 27.1235L140.357 151.342" stroke="#E3E2E4" />
|
||||
<path d="M139.892 89.4653H264.717" stroke="#E3E2E4" />
|
||||
<path d="M202.304 26.6582V151.807" stroke="#E3E2E4" />
|
||||
<ellipse cx="264.717" cy="89.4651" rx="3.72614" ry="3.7219" fill="#121212" />
|
||||
<ellipse cx="264.717" cy="27.1233" rx="3.72614" ry="3.7219" fill="#121212" />
|
||||
<ellipse cx="264.717" cy="151.807" rx="3.72614" ry="3.7219" fill="#121212" />
|
||||
<ellipse cx="140.357" cy="151.807" rx="3.72613" ry="3.7219" fill="#121212" />
|
||||
<ellipse cx="202.304" cy="89.4651" rx="3.72613" ry="3.7219" fill="#121212" />
|
||||
<ellipse cx="202.304" cy="27.1233" rx="3.72613" ry="3.7219" fill="#121212" />
|
||||
<ellipse cx="140.357" cy="89.4651" rx="3.72613" ry="3.7219" fill="#121212" />
|
||||
<ellipse cx="140.357" cy="27.1233" rx="3.72613" ry="3.7219" fill="#121212" />
|
||||
<ellipse cx="202.304" cy="151.807" rx="3.72613" ry="3.7219" fill="#121212" />
|
||||
<path d="M401 127.187H308.257L379.053 12.9805V165.02" stroke="#121212" stroke-width="2" stroke-linecap="square" stroke-linejoin="bevel" />
|
||||
<ellipse cx="379.407" cy="127.187" rx="3.89381" ry="3.88938" fill="#121212" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
@@ -1,44 +1,107 @@
|
||||
import type { DefaultSession } from 'next-auth';
|
||||
import { type User } from '@affine/component/auth-components';
|
||||
import type { DefaultSession, Session } from 'next-auth';
|
||||
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
||||
import { useSession } from 'next-auth/react';
|
||||
export type CheckedUser = {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
image: string;
|
||||
import { getSession, useSession } from 'next-auth/react';
|
||||
import { useEffect, useReducer } from 'react';
|
||||
|
||||
import { SessionFetchErrorRightAfterLoginOrSignUp } from '../../unexpected-application-state/errors';
|
||||
|
||||
export type CheckedUser = User & {
|
||||
hasPassword: boolean;
|
||||
update: ReturnType<typeof useSession>['update'];
|
||||
};
|
||||
|
||||
// FIXME: Should this namespace be here?
|
||||
declare module 'next-auth' {
|
||||
interface Session {
|
||||
user: {
|
||||
name: string;
|
||||
email: string;
|
||||
id: string;
|
||||
hasPassword: boolean;
|
||||
} & DefaultSession['user'];
|
||||
} & Omit<NonNullable<DefaultSession['user']>, 'name' | 'email'>;
|
||||
}
|
||||
}
|
||||
|
||||
type UpdateSessionAction =
|
||||
| {
|
||||
type: 'update';
|
||||
payload: Session;
|
||||
}
|
||||
| {
|
||||
type: 'fetchError';
|
||||
payload: null;
|
||||
};
|
||||
|
||||
function updateSessionReducer(prevState: Session, action: UpdateSessionAction) {
|
||||
const { type, payload } = action;
|
||||
switch (type) {
|
||||
case 'update':
|
||||
return payload;
|
||||
case 'fetchError':
|
||||
return prevState;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This hook checks if the user is logged in.
|
||||
* If not, it will throw an error.
|
||||
* If so, the user object will be cached and returned.
|
||||
* If not, and there is no cache, it will throw an error.
|
||||
* If network error or API response error, it will use the cached value.
|
||||
*/
|
||||
export function useCurrentUser(): CheckedUser {
|
||||
const { data: session, status, update } = useSession();
|
||||
// If you are seeing this error, it means that you are not logged in.
|
||||
// This should be prohibited in the development environment, please re-write your component logic.
|
||||
if (status === 'unauthenticated') {
|
||||
throw new Error('session.status should be authenticated');
|
||||
}
|
||||
const { data, update } = useSession();
|
||||
|
||||
const user = session?.user;
|
||||
const [session, dispatcher] = useReducer(
|
||||
updateSessionReducer,
|
||||
data,
|
||||
firstSession => {
|
||||
if (!firstSession) {
|
||||
// barely possible.
|
||||
// login succeed but the session request failed then.
|
||||
// also need a error boundary to handle this error.
|
||||
throw new SessionFetchErrorRightAfterLoginOrSignUp(
|
||||
'First session should not be null',
|
||||
() => {
|
||||
getSession()
|
||||
.then(session => {
|
||||
if (session) {
|
||||
dispatcher({
|
||||
type: 'update',
|
||||
payload: session,
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
return firstSession;
|
||||
}
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
dispatcher({
|
||||
type: 'update',
|
||||
payload: data,
|
||||
});
|
||||
} else {
|
||||
dispatcher({
|
||||
type: 'fetchError',
|
||||
payload: null,
|
||||
});
|
||||
}
|
||||
}, [data, update]);
|
||||
|
||||
const user = session.user;
|
||||
|
||||
return {
|
||||
id: user?.id ?? 'REPLACE_ME_DEFAULT_ID',
|
||||
name: user?.name ?? 'REPLACE_ME_DEFAULT_NAME',
|
||||
email: user?.email ?? 'REPLACE_ME_DEFAULT_EMAIL',
|
||||
image: user?.image ?? 'REPLACE_ME_DEFAULT_URL',
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
image: user.image,
|
||||
hasPassword: user?.hasPassword ?? false,
|
||||
update,
|
||||
};
|
||||
|
||||
@@ -23,6 +23,7 @@ import { getUIAdapter } from '../../adapters/workspace';
|
||||
import { setPageModeAtom } from '../../atoms';
|
||||
import { collectionsCRUDAtom } from '../../atoms/collections';
|
||||
import { currentModeAtom } from '../../atoms/mode';
|
||||
import { AffineErrorBoundary } from '../../components/affine/affine-error-boundary';
|
||||
import { WorkspaceHeader } from '../../components/workspace-header';
|
||||
import { useRegisterBlocksuiteEditorCommands } from '../../hooks/affine/use-register-blocksuite-editor-commands';
|
||||
import { useCurrentSyncEngineStatus } from '../../hooks/current/use-current-sync-engine';
|
||||
@@ -214,5 +215,9 @@ export const Component = () => {
|
||||
}
|
||||
}, [params, setContentLayout, setCurrentPageId, setCurrentWorkspaceId]);
|
||||
|
||||
return <DetailPage />;
|
||||
return (
|
||||
<AffineErrorBoundary>
|
||||
<DetailPage />
|
||||
</AffineErrorBoundary>
|
||||
);
|
||||
};
|
||||
|
||||
5
packages/frontend/core/src/types/types.d.ts
vendored
5
packages/frontend/core/src/types/types.d.ts
vendored
@@ -8,3 +8,8 @@ declare module '*.md' {
|
||||
const text: string;
|
||||
export default text;
|
||||
}
|
||||
|
||||
declare module '*.assets.svg' {
|
||||
const url: string;
|
||||
export default url;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
export abstract class RecoverableError extends Error {
|
||||
protected ttl = 3;
|
||||
|
||||
canRetry(): boolean {
|
||||
return this.ttl > 0;
|
||||
}
|
||||
|
||||
abstract retry(): void;
|
||||
}
|
||||
|
||||
// the first session request failed after login or signup succeed.
|
||||
// should give a hint to the user to refetch the session.
|
||||
export class SessionFetchErrorRightAfterLoginOrSignUp extends RecoverableError {
|
||||
constructor(
|
||||
message: string,
|
||||
private readonly onRetry: () => void
|
||||
) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
retry(): void {
|
||||
if (this.ttl <= 0) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
this.onRetry();
|
||||
} catch (e) {
|
||||
console.error('Retry error', e);
|
||||
} finally {
|
||||
this.ttl--;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user