mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-12 04:18:54 +00:00
fix(core): implement editor timeout and report error from boundary (#5105) ci: add sentry env when frontend assets build (#5131) fix(core): expose catched editor load error (#5133) fix(infra): use blocksuite api to check compatibility (#5137) fix(infra): compatibility logic follow blocksuite (#5143) fix(core): rerender error boundary when route change and improve sentry report (#5147)
This commit is contained in:
6
.github/workflows/deploy.yml
vendored
6
.github/workflows/deploy.yml
vendored
@@ -33,7 +33,7 @@ jobs:
|
||||
build-core:
|
||||
name: Build @affine/core
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
environment: ${{ github.event.inputs.flavor }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup Node.js
|
||||
@@ -50,6 +50,10 @@ jobs:
|
||||
SHOULD_REPORT_TRACE: true
|
||||
TRACE_REPORT_ENDPOINT: ${{ secrets.TRACE_REPORT_ENDPOINT }}
|
||||
CAPTCHA_SITE_KEY: ${{ secrets.CAPTCHA_SITE_KEY }}
|
||||
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
|
||||
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||
- name: Upload core artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
|
||||
2
.github/workflows/nightly-build.yml
vendored
2
.github/workflows/nightly-build.yml
vendored
@@ -69,8 +69,8 @@ jobs:
|
||||
env:
|
||||
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
|
||||
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
|
||||
NEXT_PUBLIC_SENTRY_DSN: ${{ secrets.NEXT_PUBLIC_SENTRY_DSN }}
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||
RELEASE_VERSION: ${{ needs.set-build-version.outputs.version }}
|
||||
SKIP_PLUGIN_BUILD: 'true'
|
||||
SKIP_NX_CACHE: 'true'
|
||||
|
||||
2
.github/workflows/release-desktop-app.yml
vendored
2
.github/workflows/release-desktop-app.yml
vendored
@@ -40,6 +40,7 @@ env:
|
||||
jobs:
|
||||
before-make:
|
||||
runs-on: ubuntu-latest
|
||||
environment: ${{ github.event.inputs.build-type || (github.ref_type == 'tag' && contains(github.ref, 'canary') && 'canary') }}
|
||||
outputs:
|
||||
RELEASE_VERSION: ${{ steps.get-canary-version.outputs.RELEASE_VERSION }}
|
||||
steps:
|
||||
@@ -65,6 +66,7 @@ jobs:
|
||||
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
|
||||
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||
RELEASE_VERSION: ${{ github.event.inputs.version || steps.get-canary-version.outputs.RELEASE_VERSION }}
|
||||
SKIP_PLUGIN_BUILD: 'true'
|
||||
SKIP_NX_CACHE: 'true'
|
||||
|
||||
2
nx.json
2
nx.json
@@ -56,7 +56,7 @@
|
||||
"env": "SENTRY_AUTH_TOKEN"
|
||||
},
|
||||
{
|
||||
"env": "NEXT_PUBLIC_SENTRY_DSN"
|
||||
"env": "SENTRY_DSN"
|
||||
},
|
||||
{
|
||||
"env": "DISTRIBUTION"
|
||||
|
||||
@@ -4,6 +4,10 @@ import type { createStore, WritableAtom } from 'jotai/vanilla';
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
import { migratePages } from '../migration/blocksuite';
|
||||
import {
|
||||
checkWorkspaceCompatibility,
|
||||
MigrationPoint,
|
||||
} from '../migration/workspace';
|
||||
|
||||
export async function initEmptyPage(page: Page, title?: string) {
|
||||
await page.load(() => {
|
||||
@@ -244,46 +248,48 @@ export async function buildShowcaseWorkspace(
|
||||
{} as Record<string, string>
|
||||
);
|
||||
});
|
||||
await Promise.all(
|
||||
data.map(async ([id, promise, newId]) => {
|
||||
const { default: template } = await promise;
|
||||
let json = JSON.stringify(template);
|
||||
Object.entries(idMap).forEach(([oldId, newId]) => {
|
||||
json = json.replaceAll(oldId, newId);
|
||||
});
|
||||
json = JSON.parse(json);
|
||||
await workspace
|
||||
.importPageSnapshot(structuredClone(json), newId)
|
||||
.catch(error => {
|
||||
console.error('error importing page', id, error);
|
||||
});
|
||||
const page = workspace.getPage(newId);
|
||||
assertExists(page);
|
||||
await page.load();
|
||||
workspace.schema.upgradePage(
|
||||
0,
|
||||
{
|
||||
'affine:note': 1,
|
||||
'affine:bookmark': 1,
|
||||
'affine:database': 2,
|
||||
'affine:divider': 1,
|
||||
'affine:image': 1,
|
||||
'affine:list': 1,
|
||||
'affine:code': 1,
|
||||
'affine:page': 2,
|
||||
'affine:paragraph': 1,
|
||||
'affine:surface': 3,
|
||||
},
|
||||
page.spaceDoc
|
||||
);
|
||||
|
||||
// The showcase building will create multiple pages once, and may skip the version writing.
|
||||
// https://github.com/toeverything/blocksuite/blob/master/packages/store/src/workspace/page.ts#L662
|
||||
if (!workspace.meta.blockVersions) {
|
||||
await migratePages(workspace.doc, workspace.schema);
|
||||
}
|
||||
})
|
||||
);
|
||||
// Import page one by one to prevent workspace meta race condition problem.
|
||||
for (const [id, promise, newId] of data) {
|
||||
const { default: template } = await promise;
|
||||
let json = JSON.stringify(template);
|
||||
Object.entries(idMap).forEach(([oldId, newId]) => {
|
||||
json = json.replaceAll(oldId, newId);
|
||||
});
|
||||
json = JSON.parse(json);
|
||||
await workspace
|
||||
.importPageSnapshot(structuredClone(json), newId)
|
||||
.catch(error => {
|
||||
console.error('error importing page', id, error);
|
||||
});
|
||||
const page = workspace.getPage(newId);
|
||||
assertExists(page);
|
||||
await page.load();
|
||||
workspace.schema.upgradePage(
|
||||
0,
|
||||
{
|
||||
'affine:note': 1,
|
||||
'affine:bookmark': 1,
|
||||
'affine:database': 2,
|
||||
'affine:divider': 1,
|
||||
'affine:image': 1,
|
||||
'affine:list': 1,
|
||||
'affine:code': 1,
|
||||
'affine:page': 2,
|
||||
'affine:paragraph': 1,
|
||||
'affine:surface': 3,
|
||||
},
|
||||
page.spaceDoc
|
||||
);
|
||||
}
|
||||
|
||||
// The showcase building will create multiple pages once, and may skip the version writing.
|
||||
// https://github.com/toeverything/blocksuite/blob/master/packages/store/src/workspace/page.ts#L662
|
||||
const compatibilityResult = checkWorkspaceCompatibility(workspace);
|
||||
if (compatibilityResult === MigrationPoint.BlockVersion) {
|
||||
await migratePages(workspace.doc, workspace.schema);
|
||||
}
|
||||
|
||||
Object.entries(pageMetas).forEach(([oldId, meta]) => {
|
||||
const newId = idMap[oldId];
|
||||
workspace.setPageMeta(newId, meta);
|
||||
|
||||
@@ -20,13 +20,18 @@ export async function migratePages(
|
||||
const meta = rootDoc.getMap('meta') as YMap<unknown>;
|
||||
const versions = meta.get('blockVersions') as YMap<number>;
|
||||
const oldVersions = versions?.toJSON() ?? {};
|
||||
|
||||
spaces.forEach((space: YDoc) => {
|
||||
try {
|
||||
schema.upgradePage(0, oldVersions, space);
|
||||
} catch (e) {
|
||||
console.error(`page ${space.guid} upgrade failed`, e);
|
||||
}
|
||||
schema.upgradePage(0, oldVersions, space);
|
||||
});
|
||||
schema.upgradeWorkspace(rootDoc);
|
||||
|
||||
// Hard code to upgrade page version to 2.
|
||||
// Let e2e to ensure the data version is correct.
|
||||
const pageVersion = meta.get('pageVersion');
|
||||
if (typeof pageVersion !== 'number' || pageVersion < 2) {
|
||||
meta.set('pageVersion', 2);
|
||||
}
|
||||
|
||||
const newVersions = getLatestVersions(schema);
|
||||
meta.set('blockVersions', new YMap(Object.entries(newVersions)));
|
||||
|
||||
@@ -43,3 +43,25 @@ export function guidCompatibilityFix(rootDoc: YDoc) {
|
||||
});
|
||||
return changed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hard code to fix workspace version to be compatible with legacy data.
|
||||
* Let e2e to ensure the data version is correct.
|
||||
*/
|
||||
export function fixWorkspaceVersion(rootDoc: YDoc) {
|
||||
const meta = rootDoc.getMap('meta') as YMap<unknown>;
|
||||
|
||||
/**
|
||||
* It doesn't matter to upgrade workspace version from 1 or undefined to 2.
|
||||
* Blocksuite just set the value, do nothing else.
|
||||
*/
|
||||
const workspaceVersion = meta.get('workspaceVersion');
|
||||
if (typeof workspaceVersion !== 'number' || workspaceVersion < 2) {
|
||||
meta.set('workspaceVersion', 2);
|
||||
|
||||
const pageVersion = meta.get('pageVersion');
|
||||
if (typeof pageVersion !== 'number') {
|
||||
meta.set('pageVersion', 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,15 +58,25 @@ export function checkWorkspaceCompatibility(
|
||||
return MigrationPoint.SubDoc;
|
||||
}
|
||||
|
||||
// Sometimes, blocksuite will not write blockVersions to meta.
|
||||
// Just fix it when user open the workspace.
|
||||
const blockVersions = workspace.meta.blockVersions;
|
||||
if (!blockVersions) {
|
||||
const hasVersion = workspace.meta.hasVersion;
|
||||
if (!hasVersion) {
|
||||
return MigrationPoint.BlockVersion;
|
||||
}
|
||||
|
||||
// TODO: Catch compatibility error from blocksuite to show upgrade page.
|
||||
// Temporarily follow the check logic of blocksuite.
|
||||
if ((workspace.meta.pages?.length ?? 0) <= 1) {
|
||||
try {
|
||||
workspace.meta.validateVersion(workspace);
|
||||
} catch (e) {
|
||||
console.info('validateVersion error', e);
|
||||
return MigrationPoint.BlockVersion;
|
||||
}
|
||||
}
|
||||
|
||||
// From v2, we depend on blocksuite to check and migrate data.
|
||||
for (const [flavour, version] of Object.entries(blockVersions)) {
|
||||
const blockVersions = workspace.meta.blockVersions;
|
||||
for (const [flavour, version] of Object.entries(blockVersions ?? {})) {
|
||||
const schema = workspace.schema.flavourSchemaMap.get(flavour);
|
||||
if (schema?.version !== version) {
|
||||
return MigrationPoint.BlockVersion;
|
||||
|
||||
@@ -7,14 +7,12 @@ import type { CSSProperties, ReactElement } from 'react';
|
||||
import {
|
||||
memo,
|
||||
Suspense,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import type { FallbackProps } from 'react-error-boundary';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
import { type Map as YMap } from 'yjs';
|
||||
|
||||
import { Skeleton } from '../../ui/skeleton';
|
||||
import {
|
||||
@@ -77,6 +75,67 @@ const useBlockElementById = (
|
||||
return blockElement;
|
||||
};
|
||||
|
||||
/**
|
||||
* TODO: Define error to unexpected state together in the future.
|
||||
*/
|
||||
export class NoPageRootError extends Error {
|
||||
constructor(public page: Page) {
|
||||
super('Page root not found when render editor!');
|
||||
|
||||
// Log info to let sentry collect more message
|
||||
const hasExpectSpace = Array.from(page.doc.spaces.values()).some(
|
||||
doc => page.spaceDoc.guid === doc.guid
|
||||
);
|
||||
const blocks = page.spaceDoc.getMap('blocks') as YMap<YMap<any>>;
|
||||
const havePageBlock = Array.from(blocks.values()).some(
|
||||
block => block.get('sys:flavour') === 'affine:page'
|
||||
);
|
||||
console.info(
|
||||
'NoPageRootError current data: %s',
|
||||
JSON.stringify({
|
||||
expectPageId: page.id,
|
||||
expectGuid: page.spaceDoc.guid,
|
||||
hasExpectSpace,
|
||||
blockSize: blocks.size,
|
||||
havePageBlock,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: Defined async cache to support suspense, instead of reflect symbol to provider persistent error cache.
|
||||
*/
|
||||
const PAGE_LOAD_KEY = Symbol('PAGE_LOAD');
|
||||
const PAGE_ROOT_KEY = Symbol('PAGE_ROOT');
|
||||
function usePageRoot(page: Page) {
|
||||
let load$ = Reflect.get(page, PAGE_LOAD_KEY);
|
||||
if (!load$) {
|
||||
load$ = page.load();
|
||||
Reflect.set(page, PAGE_LOAD_KEY, load$);
|
||||
}
|
||||
use(load$);
|
||||
|
||||
if (!page.root) {
|
||||
let root$: Promise<void> | undefined = Reflect.get(page, PAGE_ROOT_KEY);
|
||||
if (!root$) {
|
||||
root$ = new Promise((resolve, reject) => {
|
||||
const disposable = page.slots.rootAdded.once(() => {
|
||||
resolve();
|
||||
});
|
||||
window.setTimeout(() => {
|
||||
disposable.dispose();
|
||||
reject(new NoPageRootError(page));
|
||||
}, 20 * 1000);
|
||||
});
|
||||
Reflect.set(page, PAGE_ROOT_KEY, root$);
|
||||
}
|
||||
use(root$);
|
||||
}
|
||||
|
||||
return page.root;
|
||||
}
|
||||
|
||||
const BlockSuiteEditorImpl = ({
|
||||
mode,
|
||||
page,
|
||||
@@ -86,9 +145,8 @@ const BlockSuiteEditorImpl = ({
|
||||
onModeChange,
|
||||
style,
|
||||
}: EditorProps): ReactElement => {
|
||||
if (!page.loaded) {
|
||||
use(page.waitForLoaded());
|
||||
}
|
||||
usePageRoot(page);
|
||||
|
||||
assertExists(page, 'page should not be null');
|
||||
const editorRef = useRef<EditorContainer | null>(null);
|
||||
if (editorRef.current === null) {
|
||||
@@ -176,27 +234,7 @@ const BlockSuiteEditorImpl = ({
|
||||
);
|
||||
};
|
||||
|
||||
const BlockSuiteErrorFallback = (
|
||||
props: FallbackProps & ErrorBoundaryProps
|
||||
): ReactElement => {
|
||||
return (
|
||||
<div>
|
||||
<h1>Sorry.. there was an error</h1>
|
||||
<div>{props.error.message}</div>
|
||||
<button
|
||||
data-testid="error-fallback-reset-button"
|
||||
onClick={() => {
|
||||
props.onReset?.();
|
||||
props.resetErrorBoundary();
|
||||
}}
|
||||
>
|
||||
Try again
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const BlockSuiteFallback = memo(function BlockSuiteFallback() {
|
||||
export const EditorLoading = memo(function EditorLoading() {
|
||||
return (
|
||||
<div className={blockSuiteEditorStyle}>
|
||||
<Skeleton
|
||||
@@ -210,21 +248,12 @@ export const BlockSuiteFallback = memo(function BlockSuiteFallback() {
|
||||
});
|
||||
|
||||
export const BlockSuiteEditor = memo(function BlockSuiteEditor(
|
||||
props: EditorProps & ErrorBoundaryProps
|
||||
props: EditorProps
|
||||
): ReactElement {
|
||||
return (
|
||||
<ErrorBoundary
|
||||
fallbackRender={useCallback(
|
||||
(fallbackProps: FallbackProps) => (
|
||||
<BlockSuiteErrorFallback {...fallbackProps} onReset={props.onReset} />
|
||||
),
|
||||
[props.onReset]
|
||||
)}
|
||||
>
|
||||
<Suspense fallback={<BlockSuiteFallback />}>
|
||||
<BlockSuiteEditorImpl key={props.page.id} {...props} />
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
<Suspense fallback={<EditorLoading />}>
|
||||
<BlockSuiteEditorImpl key={props.page.id} {...props} />
|
||||
</Suspense>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BlockSuiteFallback } from '../block-suite-editor';
|
||||
import { EditorLoading } from '../block-suite-editor';
|
||||
import {
|
||||
pageDetailSkeletonStyle,
|
||||
pageDetailSkeletonTitleStyle,
|
||||
@@ -8,7 +8,7 @@ export const PageDetailSkeleton = () => {
|
||||
return (
|
||||
<div className={pageDetailSkeletonStyle}>
|
||||
<div className={pageDetailSkeletonTitleStyle} />
|
||||
<BlockSuiteFallback />
|
||||
<EditorLoading />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -351,6 +351,8 @@ export const createConfiguration: (
|
||||
'process.env.CAPTCHA_SITE_KEY': JSON.stringify(
|
||||
process.env.CAPTCHA_SITE_KEY
|
||||
),
|
||||
'process.env.SENTRY_DSN': JSON.stringify(process.env.SENTRY_DSN),
|
||||
'process.env.BUILD_TYPE': JSON.stringify(process.env.BUILD_TYPE),
|
||||
runtimeConfig: JSON.stringify(runtimeConfig),
|
||||
}),
|
||||
new CopyPlugin({
|
||||
|
||||
@@ -45,6 +45,8 @@
|
||||
"@radix-ui/react-scroll-area": "^1.0.5",
|
||||
"@radix-ui/react-select": "^2.0.0",
|
||||
"@react-hookz/web": "^23.1.0",
|
||||
"@sentry/integrations": "^7.83.0",
|
||||
"@sentry/react": "^7.83.0",
|
||||
"@toeverything/components": "^0.0.46",
|
||||
"@toeverything/theme": "^0.7.20",
|
||||
"@vanilla-extract/css": "^1.13.0",
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
"env": "SENTRY_AUTH_TOKEN"
|
||||
},
|
||||
{
|
||||
"env": "NEXT_PUBLIC_SENTRY_DSN"
|
||||
"env": "SENTRY_DSN"
|
||||
},
|
||||
{
|
||||
"env": "DISTRIBUTION"
|
||||
|
||||
@@ -6,7 +6,15 @@ import {
|
||||
rootWorkspacesMetadataAtom,
|
||||
workspaceAdaptersAtom,
|
||||
} from '@affine/workspace/atom';
|
||||
import * as Sentry from '@sentry/react';
|
||||
import type { createStore } from 'jotai/vanilla';
|
||||
import { useEffect } from 'react';
|
||||
import {
|
||||
createRoutesFromChildren,
|
||||
matchRoutes,
|
||||
useLocation,
|
||||
useNavigationType,
|
||||
} from 'react-router-dom';
|
||||
|
||||
import { WorkspaceAdapters } from '../adapters/workspace';
|
||||
import { performanceLogger } from '../shared';
|
||||
@@ -51,6 +59,33 @@ export async function setup(store: ReturnType<typeof createStore>) {
|
||||
performanceSetupLogger.info('setup global');
|
||||
setupGlobal();
|
||||
|
||||
if (window.SENTRY_RELEASE || environment.isDebug) {
|
||||
// https://docs.sentry.io/platforms/javascript/guides/react/#configure
|
||||
Sentry.init({
|
||||
dsn: process.env.SENTRY_DSN,
|
||||
environment: process.env.BUILD_TYPE ?? 'development',
|
||||
integrations: [
|
||||
new Sentry.BrowserTracing({
|
||||
routingInstrumentation: Sentry.reactRouterV6Instrumentation(
|
||||
useEffect,
|
||||
useLocation,
|
||||
useNavigationType,
|
||||
createRoutesFromChildren,
|
||||
matchRoutes
|
||||
),
|
||||
}),
|
||||
new Sentry.Replay(),
|
||||
],
|
||||
// Set tracesSampleRate to 1.0 to capture 100%
|
||||
// of transactions for performance monitoring.
|
||||
tracesSampleRate: 1.0,
|
||||
});
|
||||
Sentry.setTags({
|
||||
appVersion: runtimeConfig.appVersion,
|
||||
editorVersion: runtimeConfig.editorVersion,
|
||||
});
|
||||
}
|
||||
|
||||
performanceSetupLogger.info('get root workspace meta');
|
||||
// do not read `rootWorkspacesMetadataAtom` before migration
|
||||
await store.get(rootWorkspacesMetadataAtom);
|
||||
|
||||
@@ -1,191 +0,0 @@
|
||||
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,
|
||||
type 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 & {
|
||||
height?: number | string;
|
||||
};
|
||||
|
||||
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: null,
|
||||
canRetryRecoveredError: this.state.error.canRetry(),
|
||||
});
|
||||
} else {
|
||||
document.location.reload();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private readonly handleRefresh = () => {
|
||||
this.setState({ error: null });
|
||||
};
|
||||
|
||||
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 RecoverableError) {
|
||||
const retryButtonDesc = this.state.canRetryRecoveredError
|
||||
? 'Refetch'
|
||||
: 'Reload';
|
||||
errorDetail = (
|
||||
<>
|
||||
<h1 className={errorTitle}>Sorry.. there was an error</h1>
|
||||
<span className={errorDescription}> {error.message} </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 className={errorTitle}>Sorry.. there was an error</h1>
|
||||
<code className={errorDescription}>
|
||||
{error.message ?? error.toString()}
|
||||
</code>
|
||||
<Button
|
||||
onClick={this.handleRefresh}
|
||||
className={errorRetryButton}
|
||||
type="primary"
|
||||
>
|
||||
Refresh
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className={errorLayout} style={{ height: this.props.height }}>
|
||||
<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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const viewport = style({
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
import { getCurrentStore } from '@toeverything/infra/atom';
|
||||
import { Provider } from 'jotai/react';
|
||||
import type { FC } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import * as styles from './affine-error-fallback.css';
|
||||
import {
|
||||
ERROR_REFLECT_KEY,
|
||||
type FallbackProps,
|
||||
} from './error-basic/fallback-creator';
|
||||
import { DumpInfo } from './error-basic/info-logger';
|
||||
import { AnyErrorFallback } from './error-fallbacks/any-error-fallback';
|
||||
import { NoPageRootFallback } from './error-fallbacks/no-page-root-fallback';
|
||||
import { PageNotFoundDetail } from './error-fallbacks/page-not-found-fallback';
|
||||
import { RecoverableErrorFallback } from './error-fallbacks/recoverable-error-fallback';
|
||||
|
||||
/**
|
||||
* Register all fallback components here.
|
||||
* If have new one just add it to the set.
|
||||
*/
|
||||
const fallbacks = new Set([
|
||||
PageNotFoundDetail,
|
||||
RecoverableErrorFallback,
|
||||
NoPageRootFallback,
|
||||
]);
|
||||
|
||||
function getErrorFallbackComponent(error: any): FC<FallbackProps> {
|
||||
for (const Component of fallbacks) {
|
||||
const ErrorConstructor = Reflect.get(Component, ERROR_REFLECT_KEY);
|
||||
if (ErrorConstructor && error instanceof ErrorConstructor) {
|
||||
return Component as FC<FallbackProps>;
|
||||
}
|
||||
}
|
||||
return AnyErrorFallback;
|
||||
}
|
||||
|
||||
export interface AffineErrorFallbackProps extends FallbackProps {
|
||||
height?: number | string;
|
||||
}
|
||||
|
||||
export const AffineErrorFallback: FC<AffineErrorFallbackProps> = props => {
|
||||
const { error, resetError, height } = props;
|
||||
const Component = useMemo(() => getErrorFallbackComponent(error), [error]);
|
||||
|
||||
return (
|
||||
<div className={styles.viewport} style={{ height }}>
|
||||
<Component error={error} resetError={resetError} />
|
||||
<Provider key="JotaiProvider" store={getCurrentStore()}>
|
||||
<DumpInfo error={error} />
|
||||
</Provider>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.7 KiB |
@@ -0,0 +1,43 @@
|
||||
<svg width="490" height="242" viewBox="0 0 490 242" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16.5098 155.545C16.4625 163.027 18.0723 169.655 21.3393 175.432C24.6064 181.208 29.1282 185.73 34.9047 188.997C40.6812 192.264 47.3337 193.898 54.8621 193.898C63.2428 193.898 70.6765 191.838 77.1632 187.719C83.65 183.599 88.7399 177.989 92.4331 170.886C96.1263 163.784 97.9965 155.735 98.0439 146.739C98.0912 137.364 96.1026 129.196 92.078 122.236C88.1007 115.228 82.7977 109.807 76.1689 105.972C69.5875 102.089 62.3905 100.148 54.578 100.148C48.7541 100.148 43.4274 101.166 38.5979 103.202C33.7683 105.19 29.5306 107.866 25.8848 111.227H25.3166L32.703 51H93.2143" stroke="black" stroke-width="2"/>
|
||||
<ellipse cx="94.4026" cy="50.8894" rx="3.89381" ry="3.88938" fill="#121212"/>
|
||||
<ellipse cx="16.4026" cy="155.889" rx="3.89381" ry="3.88938" fill="#121212"/>
|
||||
<ellipse cx="32.4026" cy="50.8894" rx="3.89381" ry="3.88938" fill="#121212"/>
|
||||
<ellipse cx="25.4026" cy="110.889" rx="3.89381" ry="3.88938" fill="#121212"/>
|
||||
<path d="M151.592 59.1235L275.951 183.342" stroke="#E3E2E4"/>
|
||||
<rect x="151.626" y="59.1582" width="124.291" height="124.149" stroke="#E3E2E4"/>
|
||||
<path d="M275.573 121.465C276.129 117.824 276.417 114.095 276.417 110.299C276.417 69.7024 243.469 36.792 202.826 36.792C162.183 36.792 129.235 69.7024 129.235 110.299C129.235 150.897 162.183 183.807 202.826 183.807C206.465 183.807 210.041 183.543 213.539 183.034" stroke="#E3E2E4"/>
|
||||
<path d="M213.539 182.964C217.184 183.519 220.917 183.807 224.717 183.807C265.36 183.807 298.308 150.897 298.308 110.299C298.308 69.7024 265.36 36.792 224.717 36.792C184.074 36.792 151.126 69.7024 151.126 110.299C151.126 114.095 151.414 117.824 151.97 121.465" stroke="#E3E2E4"/>
|
||||
<path d="M151.434 121.465C150.924 124.958 150.66 128.531 150.66 132.166C150.66 172.763 183.608 205.673 224.251 205.673C264.894 205.673 297.842 172.763 297.842 132.166C297.842 91.5686 264.894 58.6582 224.251 58.6582C220.613 58.6582 217.036 58.922 213.539 59.4314" stroke="#E3E2E4"/>
|
||||
<path d="M213.539 59.4314C210.041 58.922 206.465 58.6582 202.826 58.6582C162.183 58.6582 129.235 91.5686 129.235 132.166C129.235 172.763 162.183 205.673 202.826 205.673C243.469 205.673 276.417 172.763 276.417 132.166C276.417 128.531 276.153 124.958 275.643 121.465" stroke="#E3E2E4"/>
|
||||
<path d="M275.951 59.1235L151.592 183.342" stroke="#E3E2E4"/>
|
||||
<path d="M151.126 121.465H275.951" stroke="#E3E2E4"/>
|
||||
<path d="M213.539 58.6582V183.807" stroke="#E3E2E4"/>
|
||||
<ellipse cx="275.951" cy="121.465" rx="3.72614" ry="3.7219" fill="#121212"/>
|
||||
<ellipse cx="275.951" cy="59.1233" rx="3.72614" ry="3.7219" fill="#121212"/>
|
||||
<ellipse cx="275.951" cy="183.807" rx="3.72614" ry="3.7219" fill="#121212"/>
|
||||
<ellipse cx="151.592" cy="183.807" rx="3.72613" ry="3.7219" fill="#121212"/>
|
||||
<ellipse cx="213.539" cy="121.465" rx="3.72613" ry="3.7219" fill="#121212"/>
|
||||
<ellipse cx="213.539" cy="59.1233" rx="3.72613" ry="3.7219" fill="#121212"/>
|
||||
<ellipse cx="151.592" cy="121.465" rx="3.72613" ry="3.7219" fill="#121212"/>
|
||||
<ellipse cx="151.592" cy="59.1233" rx="3.72613" ry="3.7219" fill="#121212"/>
|
||||
<ellipse cx="213.539" cy="183.807" rx="3.72613" ry="3.7219" fill="#121212"/>
|
||||
<path d="M338.583 59.1235L462.943 183.342" stroke="#E3E2E4"/>
|
||||
<rect x="338.617" y="59.1582" width="124.291" height="124.149" stroke="#E3E2E4"/>
|
||||
<path d="M462.565 121.465C463.12 117.824 463.408 114.095 463.408 110.299C463.408 69.7024 430.46 36.792 389.817 36.792C349.174 36.792 316.226 69.7024 316.226 110.299C316.226 150.897 349.174 183.807 389.817 183.807C393.456 183.807 397.033 183.543 400.53 183.034" stroke="#E3E2E4"/>
|
||||
<path d="M400.53 182.964C404.175 183.519 407.908 183.807 411.708 183.807C452.351 183.807 485.299 150.897 485.299 110.299C485.299 69.7024 452.351 36.792 411.708 36.792C371.065 36.792 338.117 69.7024 338.117 110.299C338.117 114.095 338.405 117.824 338.961 121.465" stroke="#E3E2E4"/>
|
||||
<path d="M338.425 121.465C337.915 124.958 337.651 128.531 337.651 132.166C337.651 172.763 370.599 205.673 411.242 205.673C451.886 205.673 484.833 172.763 484.833 132.166C484.833 91.5686 451.886 58.6582 411.242 58.6582C407.604 58.6582 404.027 58.922 400.53 59.4314" stroke="#E3E2E4"/>
|
||||
<path d="M400.53 59.4314C397.033 58.922 393.456 58.6582 389.817 58.6582C349.174 58.6582 316.226 91.5686 316.226 132.166C316.226 172.763 349.174 205.673 389.817 205.673C430.46 205.673 463.408 172.763 463.408 132.166C463.408 128.531 463.144 124.958 462.634 121.465" stroke="#E3E2E4"/>
|
||||
<path d="M462.943 59.1235L338.583 183.342" stroke="#E3E2E4"/>
|
||||
<path d="M338.117 121.465H462.943" stroke="#E3E2E4"/>
|
||||
<path d="M400.53 58.6582V183.807" stroke="#E3E2E4"/>
|
||||
<ellipse cx="462.942" cy="121.465" rx="3.72614" ry="3.7219" fill="#121212"/>
|
||||
<ellipse cx="462.942" cy="59.1233" rx="3.72614" ry="3.7219" fill="#121212"/>
|
||||
<ellipse cx="462.942" cy="183.807" rx="3.72614" ry="3.7219" fill="#121212"/>
|
||||
<ellipse cx="338.583" cy="183.807" rx="3.72613" ry="3.7219" fill="#121212"/>
|
||||
<ellipse cx="400.53" cy="121.465" rx="3.72613" ry="3.7219" fill="#121212"/>
|
||||
<ellipse cx="400.53" cy="59.1233" rx="3.72613" ry="3.7219" fill="#121212"/>
|
||||
<ellipse cx="338.583" cy="121.465" rx="3.72613" ry="3.7219" fill="#121212"/>
|
||||
<ellipse cx="338.583" cy="59.1233" rx="3.72613" ry="3.7219" fill="#121212"/>
|
||||
<ellipse cx="400.53" cy="183.807" rx="3.72613" ry="3.7219" fill="#121212"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.3 KiB |
@@ -6,6 +6,7 @@ export const errorLayout = style({
|
||||
alignItems: 'center',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
gap: '20px',
|
||||
});
|
||||
|
||||
export const errorDetailStyle = style({
|
||||
@@ -24,15 +25,15 @@ export const errorImage = style({
|
||||
height: '178px',
|
||||
maxWidth: '400px',
|
||||
flexGrow: 1,
|
||||
backgroundSize: 'cover',
|
||||
});
|
||||
|
||||
export const errorDescription = style({
|
||||
marginTop: '24px',
|
||||
});
|
||||
|
||||
export const errorRetryButton = style({
|
||||
export const errorFooter = style({
|
||||
marginTop: '24px',
|
||||
width: '94px',
|
||||
});
|
||||
|
||||
export const errorDivider = style({
|
||||
@@ -0,0 +1,106 @@
|
||||
import { Trans } from '@affine/i18n';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { Button } from '@toeverything/components/button';
|
||||
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
|
||||
import {
|
||||
type FC,
|
||||
type PropsWithChildren,
|
||||
type ReactNode,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import imageUrlFor404 from '../error-assets/404-status.assets.svg';
|
||||
import imageUrlFor500 from '../error-assets/500-status.assets.svg';
|
||||
import * as styles from './error-detail.css';
|
||||
|
||||
export enum ErrorStatus {
|
||||
NotFound = 404,
|
||||
Unexpected = 500,
|
||||
}
|
||||
|
||||
export interface ErrorDetailProps extends PropsWithChildren {
|
||||
status?: ErrorStatus;
|
||||
direction?: 'column' | 'row';
|
||||
title: string;
|
||||
description: ReactNode | Array<ReactNode>;
|
||||
buttonText?: string;
|
||||
onButtonClick?: () => void | Promise<void>;
|
||||
resetError?: () => void;
|
||||
withoutImage?: boolean;
|
||||
}
|
||||
|
||||
const imageMap = new Map([
|
||||
[ErrorStatus.NotFound, imageUrlFor404],
|
||||
[ErrorStatus.Unexpected, imageUrlFor500],
|
||||
]);
|
||||
|
||||
/**
|
||||
* TODO: Unify with NotFoundPage.
|
||||
*/
|
||||
export const ErrorDetail: FC<ErrorDetailProps> = props => {
|
||||
const {
|
||||
status = ErrorStatus.Unexpected,
|
||||
direction = 'row',
|
||||
description,
|
||||
onButtonClick,
|
||||
resetError,
|
||||
withoutImage,
|
||||
} = props;
|
||||
const descriptions = Array.isArray(description) ? description : [description];
|
||||
const [isBtnLoading, setBtnLoading] = useState(false);
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
const onBtnClick = useAsyncCallback(async () => {
|
||||
try {
|
||||
setBtnLoading(true);
|
||||
await onButtonClick?.();
|
||||
resetError?.(); // Only reset when retry success.
|
||||
} finally {
|
||||
setBtnLoading(false);
|
||||
}
|
||||
}, [onButtonClick, resetError]);
|
||||
|
||||
return (
|
||||
<div className={styles.errorLayout} style={{ flexDirection: direction }}>
|
||||
<div className={styles.errorDetailStyle}>
|
||||
<h1 className={styles.errorTitle}>{props.title}</h1>
|
||||
{descriptions.map((item, i) => (
|
||||
<p key={i} className={styles.errorDescription}>
|
||||
{item}
|
||||
</p>
|
||||
))}
|
||||
<div className={styles.errorFooter}>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={onBtnClick}
|
||||
loading={isBtnLoading}
|
||||
size="extraLarge"
|
||||
>
|
||||
{props.buttonText ?? t['com.affine.error.retry']()}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{withoutImage ? null : (
|
||||
<div
|
||||
className={styles.errorImage}
|
||||
style={{ backgroundImage: `url(${imageMap.get(status)})` }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export function ContactUS() {
|
||||
return (
|
||||
<Trans>
|
||||
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>
|
||||
</Trans>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { FC } from 'react';
|
||||
|
||||
export interface FallbackProps<T extends Error = Error> {
|
||||
error: T;
|
||||
resetError: () => void;
|
||||
}
|
||||
|
||||
export const ERROR_REFLECT_KEY = Symbol('ERROR_REFLECT_KEY');
|
||||
|
||||
export function createErrorFallback<T extends Error>(
|
||||
ErrorConstructor: abstract new (...args: any[]) => T,
|
||||
Component: FC<FallbackProps<T>>
|
||||
): FC<FallbackProps<T>> {
|
||||
Reflect.set(Component, ERROR_REFLECT_KEY, ErrorConstructor);
|
||||
return Component;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
|
||||
import {
|
||||
currentPageIdAtom,
|
||||
currentWorkspaceIdAtom,
|
||||
} from '@toeverything/infra/atom';
|
||||
import { useAtomValue } from 'jotai/react';
|
||||
import { useEffect } from 'react';
|
||||
import { useLocation, useParams } from 'react-router-dom';
|
||||
|
||||
export interface DumpInfoProps {
|
||||
error: any;
|
||||
}
|
||||
|
||||
export const DumpInfo = (_props: DumpInfoProps) => {
|
||||
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;
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { type FC, useCallback } from 'react';
|
||||
|
||||
import { ErrorDetail } from '../error-basic/error-detail';
|
||||
import type { FallbackProps } from '../error-basic/fallback-creator';
|
||||
|
||||
/**
|
||||
* TODO: Support reload and retry two reset actions in page error and area error.
|
||||
*/
|
||||
export const AnyErrorFallback: FC<FallbackProps> = props => {
|
||||
const { error } = props;
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
const reloadPage = useCallback(() => {
|
||||
document.location.reload();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ErrorDetail
|
||||
title={t['com.affine.error.unexpected-error.title']()}
|
||||
resetError={reloadPage}
|
||||
buttonText={t['com.affine.error.reload']()}
|
||||
description={error.message ?? error.toString()}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
import { NoPageRootError } from '@affine/component/block-suite-editor';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
|
||||
import { ContactUS, ErrorDetail } from '../error-basic/error-detail';
|
||||
import { createErrorFallback } from '../error-basic/fallback-creator';
|
||||
|
||||
export const NoPageRootFallback = createErrorFallback(
|
||||
NoPageRootError,
|
||||
props => {
|
||||
const { resetError } = props;
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
return (
|
||||
<ErrorDetail
|
||||
title={t['com.affine.error.no-page-root.title']()}
|
||||
description={<ContactUS />}
|
||||
resetError={resetError}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1,30 @@
|
||||
import { PageNotFoundError } from '@affine/env/constant';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import {
|
||||
RouteLogic,
|
||||
useNavigateHelper,
|
||||
} from '../../../../hooks/use-navigate-helper';
|
||||
import { ErrorDetail, ErrorStatus } from '../error-basic/error-detail';
|
||||
import { createErrorFallback } from '../error-basic/fallback-creator';
|
||||
|
||||
export const PageNotFoundDetail = createErrorFallback(PageNotFoundError, () => {
|
||||
const t = useAFFiNEI18N();
|
||||
const { jumpToIndex } = useNavigateHelper();
|
||||
|
||||
const onBtnClick = useCallback(
|
||||
() => jumpToIndex(RouteLogic.REPLACE),
|
||||
[jumpToIndex]
|
||||
);
|
||||
|
||||
return (
|
||||
<ErrorDetail
|
||||
title={t['com.affine.notFoundPage.title']()}
|
||||
description={t['404.hint']()}
|
||||
buttonText={t['404.back']()}
|
||||
onButtonClick={onBtnClick}
|
||||
status={ErrorStatus.NotFound}
|
||||
/>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import { RecoverableError } from '../../../../unexpected-application-state/errors';
|
||||
import { ContactUS, ErrorDetail } from '../error-basic/error-detail';
|
||||
import { createErrorFallback } from '../error-basic/fallback-creator';
|
||||
|
||||
export const RecoverableErrorFallback = createErrorFallback(
|
||||
RecoverableError,
|
||||
props => {
|
||||
const { error, resetError } = props;
|
||||
const t = useAFFiNEI18N();
|
||||
const [count, rerender] = useState(0);
|
||||
|
||||
const canRetry = error.canRetry();
|
||||
const buttonDesc = useMemo(() => {
|
||||
if (canRetry) {
|
||||
return t['com.affine.error.refetch']();
|
||||
}
|
||||
return t['com.affine.error.reload']();
|
||||
}, [canRetry, t]);
|
||||
const onRetry = useCallback(async () => {
|
||||
if (canRetry) {
|
||||
rerender(count + 1);
|
||||
await error.retry();
|
||||
} else {
|
||||
document.location.reload();
|
||||
}
|
||||
}, [error, count, canRetry]);
|
||||
|
||||
return (
|
||||
<ErrorDetail
|
||||
title={t['com.affine.error.unexpected-error.title']()}
|
||||
resetError={resetError}
|
||||
buttonText={buttonDesc}
|
||||
onButtonClick={onRetry}
|
||||
description={[error.message, <ContactUS key="contact-us" />]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -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 |
@@ -0,0 +1,34 @@
|
||||
import { ErrorBoundary } from '@sentry/react';
|
||||
import type { FC, PropsWithChildren } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { AffineErrorFallback } from './affine-error-fallback';
|
||||
import { type FallbackProps } from './error-basic/fallback-creator';
|
||||
|
||||
export { type FallbackProps } from './error-basic/fallback-creator';
|
||||
|
||||
export interface AffineErrorBoundaryProps extends PropsWithChildren {
|
||||
height?: number | string;
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: Unify with SWRErrorBoundary
|
||||
*/
|
||||
export const AffineErrorBoundary: FC<AffineErrorBoundaryProps> = props => {
|
||||
const fallbackRender = useCallback(
|
||||
(fallbackProps: FallbackProps) => {
|
||||
return <AffineErrorFallback {...fallbackProps} height={props.height} />;
|
||||
},
|
||||
[props.height]
|
||||
);
|
||||
|
||||
const onError = useCallback((error: Error, componentStack: string) => {
|
||||
console.error('Uncaught error:', error, componentStack);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ErrorBoundary fallback={fallbackRender} onError={onError}>
|
||||
{props.children}
|
||||
</ErrorBoundary>
|
||||
);
|
||||
};
|
||||
@@ -1,11 +0,0 @@
|
||||
import type { ReactElement } from 'react';
|
||||
import type { FallbackProps } from 'react-error-boundary';
|
||||
|
||||
export const AnyErrorBoundary = (props: FallbackProps): ReactElement => {
|
||||
return (
|
||||
<div>
|
||||
<p>Something went wrong:</p>
|
||||
<p>{props.error.toString()}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -29,7 +29,6 @@ import {
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
|
||||
import { openSettingModalAtom } from '../../../atoms';
|
||||
import type { CheckedUser } from '../../../hooks/affine/use-current-user';
|
||||
@@ -39,7 +38,7 @@ import { useMemberCount } from '../../../hooks/affine/use-member-count';
|
||||
import { type Member, useMembers } from '../../../hooks/affine/use-members';
|
||||
import { useRevokeMemberPermission } from '../../../hooks/affine/use-revoke-member-permission';
|
||||
import { useUserSubscription } from '../../../hooks/use-subscription';
|
||||
import { AnyErrorBoundary } from '../any-error-boundary';
|
||||
import { AffineErrorBoundary } from '../affine-error-boundary';
|
||||
import * as style from './style.css';
|
||||
import type { WorkspaceSettingDetailProps } from './types';
|
||||
|
||||
@@ -362,10 +361,10 @@ export const MembersPanel = (props: MembersPanelProps): ReactElement | null => {
|
||||
return <MembersPanelLocal />;
|
||||
}
|
||||
return (
|
||||
<ErrorBoundary FallbackComponent={AnyErrorBoundary}>
|
||||
<AffineErrorBoundary>
|
||||
<Suspense>
|
||||
<CloudWorkspaceMembersPanel {...props} />
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
</AffineErrorBoundary>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Scrollable } from '@affine/component';
|
||||
import {
|
||||
BlockSuiteEditor,
|
||||
BlockSuiteFallback,
|
||||
EditorLoading,
|
||||
} from '@affine/component/block-suite-editor';
|
||||
import type { PageMode } from '@affine/core/atoms';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
@@ -147,7 +147,7 @@ const HistoryEditorPreview = ({
|
||||
onModeChange={onModeChange}
|
||||
/>
|
||||
) : (
|
||||
<BlockSuiteFallback />
|
||||
<EditorLoading />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
@@ -410,7 +410,7 @@ export const PageHistoryModal = ({
|
||||
|
||||
return (
|
||||
<ModalContainer onOpenChange={onOpenChange} open={open}>
|
||||
<Suspense fallback={<BlockSuiteFallback />}>
|
||||
<Suspense fallback={<EditorLoading />}>
|
||||
<PageHistoryManager
|
||||
onClose={onClose}
|
||||
pageId={pageId}
|
||||
|
||||
@@ -19,6 +19,7 @@ export const upgradeTips = style({
|
||||
fontStyle: 'normal',
|
||||
fontWeight: '400',
|
||||
lineHeight: '20px',
|
||||
textAlign: 'center',
|
||||
});
|
||||
|
||||
const rotate = keyframes({
|
||||
|
||||
@@ -44,7 +44,7 @@ interface WorkspaceUpgradeProps {
|
||||
export const WorkspaceUpgrade = function WorkspaceUpgrade(
|
||||
props: WorkspaceUpgradeProps
|
||||
) {
|
||||
const [upgradeState, , upgradeWorkspace, newWorkspaceId] =
|
||||
const [upgradeState, error, upgradeWorkspace, newWorkspaceId] =
|
||||
useUpgradeWorkspace(props.migration);
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
@@ -75,7 +75,7 @@ export const WorkspaceUpgrade = function WorkspaceUpgrade(
|
||||
<div className={styles.upgradeBox}>
|
||||
<AffineShapeIcon width={180} height={180} />
|
||||
<p className={styles.upgradeTips}>
|
||||
{t[UPGRADE_TIPS_KEYS[upgradeState]]()}
|
||||
{error ? error.message : t[UPGRADE_TIPS_KEYS[upgradeState]]()}
|
||||
</p>
|
||||
<Button
|
||||
data-testid="upgrade-workspace-button"
|
||||
|
||||
@@ -113,7 +113,6 @@ export const DetailPage = (): ReactElement => {
|
||||
const currentSyncEngineStatus = useCurrentSyncEngineStatus();
|
||||
const currentPageId = useAtomValue(currentPageIdAtom);
|
||||
const [page, setPage] = useState<Page | null>(null);
|
||||
const [pageLoaded, setPageLoaded] = useState<boolean>(false);
|
||||
|
||||
// load page by current page id
|
||||
useEffect(() => {
|
||||
@@ -158,30 +157,7 @@ export const DetailPage = (): ReactElement => {
|
||||
return;
|
||||
}, [currentSyncEngineStatus, navigate, page]);
|
||||
|
||||
// wait for page to be loaded
|
||||
useEffect(() => {
|
||||
if (page) {
|
||||
if (!page.isEmpty) {
|
||||
setPageLoaded(true);
|
||||
} else {
|
||||
setPageLoaded(false);
|
||||
// call waitForLoaded to trigger load
|
||||
page
|
||||
.load(() => {})
|
||||
.catch(() => {
|
||||
// do nothing
|
||||
});
|
||||
return page.slots.ready.on(() => {
|
||||
setPageLoaded(true);
|
||||
}).dispose;
|
||||
}
|
||||
} else {
|
||||
setPageLoaded(false);
|
||||
}
|
||||
return;
|
||||
}, [page]);
|
||||
|
||||
if (!currentPageId || !page || !pageLoaded) {
|
||||
if (!currentPageId || !page) {
|
||||
return <PageDetailSkeleton key="current-page-is-null" />;
|
||||
}
|
||||
|
||||
@@ -218,8 +194,9 @@ export const Component = () => {
|
||||
}
|
||||
}, [params, setContentLayout, setCurrentPageId, setCurrentWorkspaceId]);
|
||||
|
||||
// Add a key to force rerender when page changed, to avoid error boundary persisting.
|
||||
return (
|
||||
<AffineErrorBoundary>
|
||||
<AffineErrorBoundary key={params.pageId}>
|
||||
<DetailPage />
|
||||
</AffineErrorBoundary>
|
||||
);
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
import type { MigrationPoint } from '@toeverything/infra/blocksuite';
|
||||
import {
|
||||
checkWorkspaceCompatibility,
|
||||
fixWorkspaceVersion,
|
||||
guidCompatibilityFix,
|
||||
} from '@toeverything/infra/blocksuite';
|
||||
import { useSetAtom } from 'jotai';
|
||||
@@ -54,6 +55,7 @@ export const loader: LoaderFunction = async args => {
|
||||
workspaceLoaderLogger.info('workspace loaded');
|
||||
|
||||
guidCompatibilityFix(workspace.doc);
|
||||
fixWorkspaceVersion(workspace.doc);
|
||||
return checkWorkspaceCompatibility(workspace);
|
||||
};
|
||||
|
||||
@@ -73,7 +75,7 @@ export const Component = (): ReactElement => {
|
||||
|
||||
const migration = useLoaderData() as MigrationPoint | undefined;
|
||||
return (
|
||||
<AffineErrorBoundary height="100vh">
|
||||
<AffineErrorBoundary key={params.workspaceId} height="100vh">
|
||||
<WorkspaceLayout migration={migration}>
|
||||
<Outlet />
|
||||
</WorkspaceLayout>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as Sentry from '@sentry/react';
|
||||
import type { RouteObject } from 'react-router-dom';
|
||||
import { createBrowserRouter } from 'react-router-dom';
|
||||
import { createBrowserRouter as reactRouterCreateBrowserRouter } from 'react-router-dom';
|
||||
|
||||
export const routes = [
|
||||
{
|
||||
@@ -70,6 +71,9 @@ export const routes = [
|
||||
},
|
||||
] satisfies [RouteObject, ...RouteObject[]];
|
||||
|
||||
const createBrowserRouter = Sentry.wrapCreateBrowserRouter(
|
||||
reactRouterCreateBrowserRouter
|
||||
);
|
||||
export const router = createBrowserRouter(routes, {
|
||||
future: {
|
||||
v7_normalizeFormMethod: true,
|
||||
|
||||
@@ -5,7 +5,7 @@ export abstract class RecoverableError extends Error {
|
||||
return this.ttl > 0;
|
||||
}
|
||||
|
||||
abstract retry(): void;
|
||||
abstract retry(): void | Promise<void>;
|
||||
}
|
||||
|
||||
// the first session request failed after login or signup succeed.
|
||||
@@ -24,8 +24,6 @@ export class SessionFetchErrorRightAfterLoginOrSignUp extends RecoverableError {
|
||||
}
|
||||
try {
|
||||
this.onRetry();
|
||||
} catch (e) {
|
||||
console.error('Retry error', e);
|
||||
} finally {
|
||||
this.ttl--;
|
||||
}
|
||||
|
||||
@@ -689,7 +689,7 @@
|
||||
"com.affine.new_edgeless": "New Edgeless",
|
||||
"com.affine.new_import": "Import",
|
||||
"com.affine.notFoundPage.backButton": "Back Home",
|
||||
"com.affine.notFoundPage.title": "404 - Page Not Found",
|
||||
"com.affine.notFoundPage.title": "Page Not Found",
|
||||
"com.affine.onboarding.title1": "Hyper merged whiteboard and docs",
|
||||
"com.affine.onboarding.title2": "Intuitive & robust block-based editing",
|
||||
"com.affine.onboarding.videoDescription1": "Easily switch between Page mode for structured document creation and Whiteboard mode for the freeform visual expression of creative ideas.",
|
||||
@@ -963,6 +963,13 @@
|
||||
"com.affine.workspaceType.offline": "Available Offline",
|
||||
"com.affine.write_with_a_blank_page": "Write with a blank page",
|
||||
"com.affine.yesterday": "Yesterday",
|
||||
"com.affine.error.retry": "Refresh",
|
||||
"com.affine.error.refetch": "Refetch",
|
||||
"com.affine.error.reload": "Reload",
|
||||
"com.affine.error.page-not-found.title": "Refresh",
|
||||
"com.affine.error.unexpected-error.title": "Something is wrong...",
|
||||
"com.affine.error.contact.description": "If you are still experiencing this issue, please <1>contact us through the community.</1>",
|
||||
"com.affine.error.no-page-root.title": "Page content is missing",
|
||||
"core": "core",
|
||||
"dark": "Dark",
|
||||
"emptyAllPages": "Click on the <1>$t(New Page)</1> button to create your first page.",
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
"env": "SENTRY_AUTH_TOKEN"
|
||||
},
|
||||
{
|
||||
"env": "NEXT_PUBLIC_SENTRY_DSN"
|
||||
"env": "SENTRY_DSN"
|
||||
},
|
||||
{
|
||||
"env": "DISTRIBUTION"
|
||||
|
||||
5
tools/@types/env/__all.d.ts
vendored
5
tools/@types/env/__all.d.ts
vendored
@@ -56,6 +56,11 @@ declare global {
|
||||
var runtimeConfig: RuntimeConfig;
|
||||
// eslint-disable-next-line no-var
|
||||
var $AFFINE_SETUP: boolean | undefined;
|
||||
/**
|
||||
* Inject by https://www.npmjs.com/package/@sentry/webpack-plugin
|
||||
*/
|
||||
// eslint-disable-next-line no-var
|
||||
var SENTRY_RELEASE: { id: string } | undefined;
|
||||
}
|
||||
|
||||
declare module '@blocksuite/store' {
|
||||
|
||||
110
yarn.lock
110
yarn.lock
@@ -357,6 +357,8 @@ __metadata:
|
||||
"@radix-ui/react-scroll-area": "npm:^1.0.5"
|
||||
"@radix-ui/react-select": "npm:^2.0.0"
|
||||
"@react-hookz/web": "npm:^23.1.0"
|
||||
"@sentry/integrations": "npm:^7.83.0"
|
||||
"@sentry/react": "npm:^7.83.0"
|
||||
"@sentry/webpack-plugin": "npm:^2.8.0"
|
||||
"@svgr/webpack": "npm:^8.1.0"
|
||||
"@swc/core": "npm:^1.3.93"
|
||||
@@ -10850,6 +10852,30 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@sentry-internal/tracing@npm:7.83.0":
|
||||
version: 7.83.0
|
||||
resolution: "@sentry-internal/tracing@npm:7.83.0"
|
||||
dependencies:
|
||||
"@sentry/core": "npm:7.83.0"
|
||||
"@sentry/types": "npm:7.83.0"
|
||||
"@sentry/utils": "npm:7.83.0"
|
||||
checksum: 7f5d2a2490f15b907c57fbc96ad3fa34a63f585433e0dfefa82400d363004ddff1e5e3b5c40436679b80a8f2afb7cb18f2802d7134ae44f607f0104a352811c3
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@sentry/browser@npm:7.83.0":
|
||||
version: 7.83.0
|
||||
resolution: "@sentry/browser@npm:7.83.0"
|
||||
dependencies:
|
||||
"@sentry-internal/tracing": "npm:7.83.0"
|
||||
"@sentry/core": "npm:7.83.0"
|
||||
"@sentry/replay": "npm:7.83.0"
|
||||
"@sentry/types": "npm:7.83.0"
|
||||
"@sentry/utils": "npm:7.83.0"
|
||||
checksum: a4d2181dc36d7946b3af133bf9249be3bb3bd6d3eb34c70352c42445416934a98cb537ed7b827b77277ce60cb902e81aa86b5d94bf8de036aedf1f54d301ba78
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@sentry/bundler-plugin-core@npm:2.10.1":
|
||||
version: 2.10.1
|
||||
resolution: "@sentry/bundler-plugin-core@npm:2.10.1"
|
||||
@@ -10891,6 +10917,28 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@sentry/core@npm:7.83.0":
|
||||
version: 7.83.0
|
||||
resolution: "@sentry/core@npm:7.83.0"
|
||||
dependencies:
|
||||
"@sentry/types": "npm:7.83.0"
|
||||
"@sentry/utils": "npm:7.83.0"
|
||||
checksum: c75cca15b750180d73975309d44ff3309a369b60f95c1c01f679b868a3eca43ace768d026aa11333c7cc1bc147a947f5f2b507ec72803033e4e3f87ea895cf7b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@sentry/integrations@npm:^7.83.0":
|
||||
version: 7.83.0
|
||||
resolution: "@sentry/integrations@npm:7.83.0"
|
||||
dependencies:
|
||||
"@sentry/core": "npm:7.83.0"
|
||||
"@sentry/types": "npm:7.83.0"
|
||||
"@sentry/utils": "npm:7.83.0"
|
||||
localforage: "npm:^1.8.1"
|
||||
checksum: 6f614f4f5bb56cdfa7e493b2546ddbd9a45a867fd61e0cf46f5c5af22b74f82b6a8a50c86bab76c9c828c03242652eef72d24a4235c2584e7a0fac58821b7c3d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@sentry/node@npm:^7.60.0":
|
||||
version: 7.81.1
|
||||
resolution: "@sentry/node@npm:7.81.1"
|
||||
@@ -10904,6 +10952,32 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@sentry/react@npm:^7.83.0":
|
||||
version: 7.83.0
|
||||
resolution: "@sentry/react@npm:7.83.0"
|
||||
dependencies:
|
||||
"@sentry/browser": "npm:7.83.0"
|
||||
"@sentry/types": "npm:7.83.0"
|
||||
"@sentry/utils": "npm:7.83.0"
|
||||
hoist-non-react-statics: "npm:^3.3.2"
|
||||
peerDependencies:
|
||||
react: 15.x || 16.x || 17.x || 18.x
|
||||
checksum: 7668d087914c390a30cc8c6e252c96ab8a47e660580224dbceb48179c83565208ce3dc50ed079366047ec1875f21175d55c73d3e623f01614dcd114d29d0c043
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@sentry/replay@npm:7.83.0":
|
||||
version: 7.83.0
|
||||
resolution: "@sentry/replay@npm:7.83.0"
|
||||
dependencies:
|
||||
"@sentry-internal/tracing": "npm:7.83.0"
|
||||
"@sentry/core": "npm:7.83.0"
|
||||
"@sentry/types": "npm:7.83.0"
|
||||
"@sentry/utils": "npm:7.83.0"
|
||||
checksum: 6e2f1960db208c0723537ffcdc8a7370d97ac2df2103d18ecf824c9b69ac83d76ebf29d37833d7a45ef435f480f0553da7148682d8110031074dd7b3c4936ca7
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@sentry/types@npm:7.81.1":
|
||||
version: 7.81.1
|
||||
resolution: "@sentry/types@npm:7.81.1"
|
||||
@@ -10911,7 +10985,14 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@sentry/utils@npm:7.81.1, @sentry/utils@npm:^7.60.0":
|
||||
"@sentry/types@npm:7.83.0":
|
||||
version: 7.83.0
|
||||
resolution: "@sentry/types@npm:7.83.0"
|
||||
checksum: 257b37678c7ea8624d4cdd9a18ccacbdb53a84be90ab6c0f3a6d6d30e8bc021f72a02a09dcf17ba6b1a9f6797580b89b827a3fcd0bc851185e7fc695ae68fbc3
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@sentry/utils@npm:7.81.1":
|
||||
version: 7.81.1
|
||||
resolution: "@sentry/utils@npm:7.81.1"
|
||||
dependencies:
|
||||
@@ -10920,6 +11001,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@sentry/utils@npm:7.83.0, @sentry/utils@npm:^7.60.0":
|
||||
version: 7.83.0
|
||||
resolution: "@sentry/utils@npm:7.83.0"
|
||||
dependencies:
|
||||
"@sentry/types": "npm:7.83.0"
|
||||
checksum: f0e4ef51f32e610ec8a33a43566e6f900e88e5f6c31aaf9a325f1b44a65ec882fa6e46122e23451afb0f2cf8f77bd766cc9684f9e40730c7193814d01d94daa4
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@sentry/webpack-plugin@npm:^2.8.0":
|
||||
version: 2.10.1
|
||||
resolution: "@sentry/webpack-plugin@npm:2.10.1"
|
||||
@@ -25156,6 +25246,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"lie@npm:3.1.1":
|
||||
version: 3.1.1
|
||||
resolution: "lie@npm:3.1.1"
|
||||
dependencies:
|
||||
immediate: "npm:~3.0.5"
|
||||
checksum: c2c7d9dcc3a9aae641f41cde4e2e2cd571e4426b1f5915862781d77776672dcbca43461e16f4d382c9a300825c15e1a4923f1def3a5568d97577e077a3cecb44
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"lie@npm:~3.3.0":
|
||||
version: 3.3.0
|
||||
resolution: "lie@npm:3.3.0"
|
||||
@@ -25356,6 +25455,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"localforage@npm:^1.8.1":
|
||||
version: 1.10.0
|
||||
resolution: "localforage@npm:1.10.0"
|
||||
dependencies:
|
||||
lie: "npm:3.1.1"
|
||||
checksum: d5c44be3a09169b013a3ebe252e678aaeb6938ffe72e9e12c199fd4307c1ec9d1a057ac2dfdfbb1379dfeec467a34ad0fc3ecd27489a2c43a154fb72b2822542
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"locate-path@npm:^2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "locate-path@npm:2.0.0"
|
||||
|
||||
Reference in New Issue
Block a user