mirror of
https://github.com/AppFlowy-IO/AppFlowy-Web.git
synced 2026-03-13 10:00:26 +08:00
Merge pull request #147 from AppFlowy-IO/storybook
chore: create storybook to display error related UI components
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
node_modules/
|
||||
dist/
|
||||
storybook-static/
|
||||
.eslintrc.cjs
|
||||
tsconfig.json
|
||||
vite.config.ts
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -37,3 +37,6 @@ cypress/snapshots/**/__diff_output__/
|
||||
cypress/screenshots
|
||||
cypress/videos
|
||||
.serena
|
||||
|
||||
*storybook.log
|
||||
storybook-static
|
||||
|
||||
61
.storybook/main.ts
Normal file
61
.storybook/main.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import type { StorybookConfig } from '@storybook/react-vite';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
const config: StorybookConfig = {
|
||||
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
|
||||
addons: [
|
||||
'@chromatic-com/storybook',
|
||||
'@storybook/addon-docs',
|
||||
'@storybook/addon-onboarding',
|
||||
'@storybook/addon-a11y',
|
||||
'@storybook/addon-vitest',
|
||||
],
|
||||
framework: {
|
||||
name: '@storybook/react-vite',
|
||||
options: {},
|
||||
},
|
||||
typescript: {
|
||||
reactDocgen: 'react-docgen-typescript',
|
||||
reactDocgenTypescriptOptions: {
|
||||
shouldExtractLiteralValuesFromEnum: true,
|
||||
propFilter: (prop) => {
|
||||
if (prop.parent) {
|
||||
return !prop.parent.fileName.includes('node_modules');
|
||||
}
|
||||
return true;
|
||||
},
|
||||
shouldRemoveUndefinedFromOptional: true,
|
||||
},
|
||||
},
|
||||
// Exclude plugin files and other non-component files from react-docgen
|
||||
features: {
|
||||
buildStoriesJson: true,
|
||||
},
|
||||
async viteFinal(config) {
|
||||
if (config.resolve) {
|
||||
const existingAlias = Array.isArray(config.resolve.alias)
|
||||
? config.resolve.alias
|
||||
: config.resolve.alias
|
||||
? Object.entries(config.resolve.alias).map(([find, replacement]) => ({
|
||||
find,
|
||||
replacement: replacement as string,
|
||||
}))
|
||||
: [];
|
||||
|
||||
config.resolve.alias = [
|
||||
...existingAlias,
|
||||
{ find: 'src/', replacement: path.resolve(__dirname, '../src/') },
|
||||
{ find: '@/', replacement: path.resolve(__dirname, '../src/') },
|
||||
];
|
||||
}
|
||||
|
||||
// PostCSS config is automatically picked up from postcss.config.cjs
|
||||
// No need to configure it explicitly, but ensure CSS processing is enabled
|
||||
|
||||
return config;
|
||||
},
|
||||
};
|
||||
export default config;
|
||||
87
.storybook/preview.tsx
Normal file
87
.storybook/preview.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
import type { Preview } from '@storybook/react-vite';
|
||||
import React, { useEffect } from 'react';
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
|
||||
import { AFConfigContext } from '@/components/main/app.hooks';
|
||||
import '@/i18n/config';
|
||||
import { i18nInstance } from '@/i18n/config';
|
||||
import '@/styles/app.scss';
|
||||
import '@/styles/global.css';
|
||||
|
||||
// Set dark mode attribute early, before React renders
|
||||
if (typeof window !== 'undefined') {
|
||||
const isDark = localStorage.getItem('dark-mode') === 'true' ||
|
||||
window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
document.documentElement.setAttribute('data-dark-mode', isDark ? 'true' : 'false');
|
||||
}
|
||||
|
||||
// Mock AFConfigContext value for Storybook
|
||||
const mockAFConfigValue = {
|
||||
service: undefined,
|
||||
isAuthenticated: true, // Set to true to prevent redirects in Storybook
|
||||
currentUser: {
|
||||
email: 'storybook@example.com',
|
||||
name: 'Storybook User',
|
||||
uid: 'storybook-uid',
|
||||
avatar: null,
|
||||
uuid: 'storybook-uuid',
|
||||
latestWorkspaceId: 'storybook-workspace-id',
|
||||
},
|
||||
updateCurrentUser: async () => {
|
||||
// Mock implementation
|
||||
},
|
||||
openLoginModal: () => {
|
||||
// Mock implementation
|
||||
console.log('Login modal would open here');
|
||||
},
|
||||
};
|
||||
|
||||
const preview: Preview = {
|
||||
parameters: {
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/i,
|
||||
},
|
||||
},
|
||||
layout: 'fullscreen',
|
||||
},
|
||||
decorators: [
|
||||
(Story) => {
|
||||
// Ensure dark mode is set on mount and watch for changes
|
||||
useEffect(() => {
|
||||
const updateDarkMode = () => {
|
||||
const isDark = localStorage.getItem('dark-mode') === 'true' ||
|
||||
window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
document.documentElement.setAttribute('data-dark-mode', isDark ? 'true' : 'false');
|
||||
};
|
||||
|
||||
updateDarkMode();
|
||||
|
||||
// Listen for system theme changes
|
||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
mediaQuery.addEventListener('change', updateDarkMode);
|
||||
|
||||
return () => {
|
||||
mediaQuery.removeEventListener('change', updateDarkMode);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<AFConfigContext.Provider value={mockAFConfigValue}>
|
||||
<I18nextProvider i18n={i18nInstance}>
|
||||
<div id="body" className="bg-background-primary text-text-primary" style={{ height: '100vh', width: '100%' }}>
|
||||
<Story />
|
||||
</div>
|
||||
</I18nextProvider>
|
||||
</AFConfigContext.Provider>
|
||||
</BrowserRouter>
|
||||
);
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default preview;
|
||||
|
||||
12
package.json
12
package.json
@@ -25,7 +25,9 @@
|
||||
"test:integration": "cypress run --spec 'cypress/e2e/**/*.cy.ts'",
|
||||
"coverage": "cross-env COVERAGE=true pnpm run test:unit && cross-env COVERAGE=true pnpm run test:components",
|
||||
"generate-tokens": "node scripts/system-token/convert-tokens.cjs",
|
||||
"generate-protobuf": "pbjs -t static-module -w es6 -o ./src/proto/messages.js ./src/proto/messages.proto & pbts -o ./src/proto/messages.d.ts ./src/proto/messages.js"
|
||||
"generate-protobuf": "pbjs -t static-module -w es6 -o ./src/proto/messages.js ./src/proto/messages.proto & pbts -o ./src/proto/messages.d.ts ./src/proto/messages.js",
|
||||
"storybook": "storybook dev -p 6006",
|
||||
"build-storybook": "storybook build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@appflowyinc/editor": "^0.1.13",
|
||||
@@ -172,9 +174,16 @@
|
||||
"@babel/preset-env": "^7.24.7",
|
||||
"@babel/preset-react": "^7.24.7",
|
||||
"@babel/preset-typescript": "^7.24.7",
|
||||
"@chromatic-com/storybook": "^4.1.2",
|
||||
"@cypress/code-coverage": "^3.12.39",
|
||||
"@istanbuljs/nyc-config-babel": "^3.0.0",
|
||||
"@istanbuljs/nyc-config-typescript": "^1.0.2",
|
||||
"@storybook/addon-a11y": "^10.0.7",
|
||||
"@storybook/addon-docs": "^10.0.7",
|
||||
"@storybook/addon-onboarding": "^10.0.7",
|
||||
"@storybook/addon-vitest": "^10.0.7",
|
||||
"@storybook/react": "^10.0.7",
|
||||
"@storybook/react-vite": "^10.0.7",
|
||||
"@svgr/plugin-svgo": "^8.0.1",
|
||||
"@testing-library/react": "^16.0.0",
|
||||
"@types/cypress-image-snapshot": "^3.1.9",
|
||||
@@ -235,6 +244,7 @@
|
||||
"prettier-plugin-tailwindcss": "^0.2.2",
|
||||
"protobufjs-cli": "^1.1.3",
|
||||
"rollup-plugin-visualizer": "^5.12.0",
|
||||
"storybook": "^10.0.7",
|
||||
"style-dictionary": "^3.9.2",
|
||||
"tailwindcss": "^3.2.7",
|
||||
"ts-jest": "^29.1.1",
|
||||
|
||||
799
pnpm-lock.yaml
generated
799
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -3550,6 +3550,27 @@
|
||||
"description": "You've made too many requests. Please wait a moment and try again.",
|
||||
"retry": "Try again later"
|
||||
},
|
||||
"notInvitee": {
|
||||
"title": "Access denied",
|
||||
"description": "This invitation wasn't sent to your account. Please contact the sender for a new invitation.",
|
||||
"goToHomepage": "Go to homepage"
|
||||
},
|
||||
"gone": {
|
||||
"title": "Resource deleted",
|
||||
"description": "This resource has been permanently deleted and is no longer available.",
|
||||
"goToHomepage": "Go to homepage"
|
||||
},
|
||||
"timeout": {
|
||||
"title": "Request timeout",
|
||||
"description": "The request took too long to complete. Please check your connection and try again.",
|
||||
"retry": "Try again"
|
||||
},
|
||||
"unknown": {
|
||||
"title": "Something went wrong",
|
||||
"description": "An unexpected error occurred. Please try again or contact support if the problem persists.",
|
||||
"goToHomepage": "Go to homepage",
|
||||
"retry": "Try again"
|
||||
},
|
||||
"asGuest": {
|
||||
"title": "You can now collaborate with others on\n<page/>",
|
||||
"viewPage": "View page",
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { ReactEditor } from 'slate-react';
|
||||
import { Range } from 'slate';
|
||||
import { getBlockEntry } from '@/application/slate-yjs/utils/editor';
|
||||
import { YjsEditor } from '@/application/slate-yjs';
|
||||
import { isEmbedBlockTypes } from '@/application/slate-yjs/command/const';
|
||||
import { getBlockEntry } from '@/application/slate-yjs/utils/editor';
|
||||
import { BlockType } from '@/application/types';
|
||||
import { Range } from 'slate';
|
||||
import { ReactEditor } from 'slate-react';
|
||||
|
||||
export const clipboardFormatKey = 'x-appflowy-fragment';
|
||||
|
||||
@@ -35,7 +35,7 @@ export const withCopy = (editor: ReactEditor) => {
|
||||
return;
|
||||
}
|
||||
|
||||
setFragmentData(<DataTransfer>data);
|
||||
setFragmentData(data as DataTransfer);
|
||||
};
|
||||
|
||||
return editor;
|
||||
|
||||
234
src/components/error/RecordNotFound.stories.tsx
Normal file
234
src/components/error/RecordNotFound.stories.tsx
Normal file
@@ -0,0 +1,234 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||
import React from 'react';
|
||||
|
||||
import { ErrorType } from '@/application/utils/error-utils';
|
||||
import { AppContext } from '@/components/app/app.hooks';
|
||||
import { AFConfigContext } from '@/components/main/app.hooks';
|
||||
import RecordNotFound from './RecordNotFound';
|
||||
|
||||
const mockAppContext = {
|
||||
currentWorkspaceId: 'test-workspace-id',
|
||||
outline: [],
|
||||
rendered: true,
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
toView: async () => {},
|
||||
loadViewMeta: async () => {
|
||||
throw new Error('Not implemented in story');
|
||||
},
|
||||
loadView: async () => {
|
||||
throw new Error('Not implemented in story');
|
||||
},
|
||||
createRowDoc: async () => {
|
||||
throw new Error('Not implemented in story');
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
appendBreadcrumb: () => {},
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
onRendered: () => {},
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
updatePage: async () => {},
|
||||
addPage: async () => 'test-page-id',
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
deletePage: async () => {},
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
openPageModal: () => {},
|
||||
loadViews: async () => [],
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
setWordCount: () => {},
|
||||
uploadFile: async () => {
|
||||
throw new Error('Not implemented in story');
|
||||
},
|
||||
eventEmitter: undefined,
|
||||
awarenessMap: {},
|
||||
};
|
||||
|
||||
const mockAFConfigValue = {
|
||||
service: undefined,
|
||||
isAuthenticated: true,
|
||||
currentUser: {
|
||||
email: 'storybook@example.com',
|
||||
name: 'Storybook User',
|
||||
uid: 'storybook-uid',
|
||||
avatar: null,
|
||||
uuid: 'storybook-uuid',
|
||||
latestWorkspaceId: 'storybook-workspace-id',
|
||||
},
|
||||
updateCurrentUser: async () => {
|
||||
// Mock implementation
|
||||
},
|
||||
openLoginModal: () => {
|
||||
// Mock implementation
|
||||
},
|
||||
};
|
||||
|
||||
const meta = {
|
||||
title: 'Error Pages/RecordNotFound',
|
||||
component: RecordNotFound,
|
||||
parameters: {
|
||||
layout: 'fullscreen',
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
(Story: React.ComponentType) => (
|
||||
<AFConfigContext.Provider value={mockAFConfigValue}>
|
||||
<AppContext.Provider value={mockAppContext}>
|
||||
<Story />
|
||||
</AppContext.Provider>
|
||||
</AFConfigContext.Provider>
|
||||
),
|
||||
],
|
||||
} satisfies Meta<typeof RecordNotFound>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const PageNotFound: Story = {
|
||||
args: {
|
||||
error: {
|
||||
type: ErrorType.PageNotFound,
|
||||
message: 'Page or resource not found',
|
||||
statusCode: 404,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const Unauthorized: Story = {
|
||||
args: {
|
||||
error: {
|
||||
type: ErrorType.Unauthorized,
|
||||
message: 'You need to sign in to access this resource',
|
||||
statusCode: 401,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const Forbidden: Story = {
|
||||
args: {
|
||||
error: {
|
||||
type: ErrorType.Forbidden,
|
||||
message: 'You do not have permission to access this resource',
|
||||
statusCode: 403,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const ForbiddenWithViewId: Story = {
|
||||
args: {
|
||||
viewId: 'test-view-id',
|
||||
error: {
|
||||
type: ErrorType.Forbidden,
|
||||
message: 'You do not have permission to access this resource',
|
||||
statusCode: 403,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const ServerError: Story = {
|
||||
args: {
|
||||
error: {
|
||||
type: ErrorType.ServerError,
|
||||
message: 'Server error. Please try again later.',
|
||||
statusCode: 500,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const NetworkError: Story = {
|
||||
args: {
|
||||
error: {
|
||||
type: ErrorType.NetworkError,
|
||||
message: 'Network connection failed. Please check your internet connection.',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const InvalidLink: Story = {
|
||||
args: {
|
||||
error: {
|
||||
type: ErrorType.InvalidLink,
|
||||
message: 'Invalid or expired link',
|
||||
code: 1068,
|
||||
statusCode: 400,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const AlreadyJoined: Story = {
|
||||
args: {
|
||||
error: {
|
||||
type: ErrorType.AlreadyJoined,
|
||||
message: 'You have already joined this workspace',
|
||||
code: 1073,
|
||||
statusCode: 409,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const NotInvitee: Story = {
|
||||
args: {
|
||||
error: {
|
||||
type: ErrorType.NotInvitee,
|
||||
message: 'You are not the intended recipient of this invitation',
|
||||
code: 1041,
|
||||
statusCode: 403,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const Gone: Story = {
|
||||
args: {
|
||||
error: {
|
||||
type: ErrorType.Gone,
|
||||
message: 'This resource has been deleted',
|
||||
statusCode: 410,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const Timeout: Story = {
|
||||
args: {
|
||||
error: {
|
||||
type: ErrorType.Timeout,
|
||||
message: 'Request timed out. Please try again.',
|
||||
statusCode: 408,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const RateLimited: Story = {
|
||||
args: {
|
||||
error: {
|
||||
type: ErrorType.RateLimited,
|
||||
message: 'Too many requests. Please try again later.',
|
||||
statusCode: 429,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const Unknown: Story = {
|
||||
args: {
|
||||
error: {
|
||||
type: ErrorType.Unknown,
|
||||
message: 'An unexpected error occurred',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const LegacyNotFound: Story = {
|
||||
args: {
|
||||
isViewNotFound: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const LegacyRecordNotFound: Story = {
|
||||
args: {
|
||||
isViewNotFound: false,
|
||||
},
|
||||
};
|
||||
|
||||
export const NoContent: Story = {
|
||||
args: {
|
||||
noContent: true,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { ReactComponent as ErrorIcon } from '@/assets/icons/error.svg';
|
||||
@@ -6,6 +7,7 @@ import { ReactComponent as WarningIcon } from '@/assets/icons/warning.svg';
|
||||
import emptyImageSrc from '@/assets/images/empty.png';
|
||||
import { AppError, ErrorType } from '@/application/utils/error-utils';
|
||||
import LandingPage from '@/components/_shared/landing-page/LandingPage';
|
||||
import { Progress } from '@/components/ui/progress';
|
||||
import { useCurrentWorkspaceId } from '@/components/app/app.hooks';
|
||||
import { RequestAccessContent } from '@/components/app/share/RequestAccessContent';
|
||||
|
||||
@@ -22,8 +24,22 @@ function RecordNotFound({
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const currentWorkspaceId = useCurrentWorkspaceId();
|
||||
const [retrying, setRetrying] = useState(false);
|
||||
|
||||
const goToHomepage = () => {
|
||||
window.location.href = '/app';
|
||||
};
|
||||
|
||||
const goToLogin = () => {
|
||||
window.location.href = '/';
|
||||
};
|
||||
|
||||
const handleRetry = async () => {
|
||||
setRetrying(true);
|
||||
await new Promise((resolve) => setTimeout(resolve, 300));
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
// NEW: If error is provided, render specific error page based on error type
|
||||
if (error) {
|
||||
switch (error.type) {
|
||||
case ErrorType.PageNotFound:
|
||||
@@ -33,7 +49,7 @@ function RecordNotFound({
|
||||
title={t('landingPage.pageNotFound.title')}
|
||||
description={t('landingPage.pageNotFound.description')}
|
||||
primaryAction={{
|
||||
onClick: () => window.open('/app', '_self'),
|
||||
onClick: goToHomepage,
|
||||
label: t('landingPage.pageNotFound.goToHomepage'),
|
||||
}}
|
||||
/>
|
||||
@@ -46,14 +62,13 @@ function RecordNotFound({
|
||||
title={t('landingPage.unauthorized.title')}
|
||||
description={t('landingPage.unauthorized.description')}
|
||||
primaryAction={{
|
||||
onClick: () => window.open('/', '_self'),
|
||||
onClick: goToLogin,
|
||||
label: t('landingPage.unauthorized.signIn'),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
case ErrorType.Forbidden:
|
||||
// If viewId and workspaceId available, show request access
|
||||
if (viewId && currentWorkspaceId) {
|
||||
return <RequestAccessContent viewId={viewId} workspaceId={currentWorkspaceId} />;
|
||||
}
|
||||
@@ -63,6 +78,10 @@ function RecordNotFound({
|
||||
Logo={NoAccessIcon}
|
||||
title={t('landingPage.forbidden.title')}
|
||||
description={t('landingPage.forbidden.description')}
|
||||
primaryAction={{
|
||||
onClick: goToHomepage,
|
||||
label: t('landingPage.pageNotFound.goToHomepage'),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -73,8 +92,15 @@ function RecordNotFound({
|
||||
title={t('landingPage.serverError.title')}
|
||||
description={t('landingPage.serverError.description')}
|
||||
primaryAction={{
|
||||
onClick: () => window.location.reload(),
|
||||
label: t('landingPage.serverError.retry'),
|
||||
onClick: handleRetry,
|
||||
label: retrying ? (
|
||||
<span className='flex items-center gap-2'>
|
||||
<Progress />
|
||||
{t('landingPage.serverError.retry')}
|
||||
</span>
|
||||
) : (
|
||||
t('landingPage.serverError.retry')
|
||||
),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
@@ -86,8 +112,15 @@ function RecordNotFound({
|
||||
title={t('landingPage.networkError.title')}
|
||||
description={t('landingPage.networkError.description')}
|
||||
primaryAction={{
|
||||
onClick: () => window.location.reload(),
|
||||
label: t('landingPage.networkError.retry'),
|
||||
onClick: handleRetry,
|
||||
label: retrying ? (
|
||||
<span className='flex items-center gap-2'>
|
||||
<Progress />
|
||||
{t('landingPage.networkError.retry')}
|
||||
</span>
|
||||
) : (
|
||||
t('landingPage.networkError.retry')
|
||||
),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
@@ -99,7 +132,7 @@ function RecordNotFound({
|
||||
title={t('landingPage.invalidLink.title')}
|
||||
description={t('landingPage.invalidLink.description')}
|
||||
primaryAction={{
|
||||
onClick: () => window.open('/app', '_self'),
|
||||
onClick: goToHomepage,
|
||||
label: t('landingPage.invalidLink.goToHomepage'),
|
||||
}}
|
||||
/>
|
||||
@@ -112,12 +145,58 @@ function RecordNotFound({
|
||||
title={t('landingPage.alreadyJoined.title')}
|
||||
description={t('landingPage.alreadyJoined.description')}
|
||||
primaryAction={{
|
||||
onClick: () => window.open('/app', '_self'),
|
||||
onClick: goToHomepage,
|
||||
label: t('landingPage.alreadyJoined.goToWorkspace'),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
case ErrorType.NotInvitee:
|
||||
return (
|
||||
<LandingPage
|
||||
Logo={NoAccessIcon}
|
||||
title={t('landingPage.notInvitee.title')}
|
||||
description={t('landingPage.notInvitee.description')}
|
||||
primaryAction={{
|
||||
onClick: goToHomepage,
|
||||
label: t('landingPage.notInvitee.goToHomepage'),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
case ErrorType.Gone:
|
||||
return (
|
||||
<LandingPage
|
||||
Logo={WarningIcon}
|
||||
title={t('landingPage.gone.title')}
|
||||
description={t('landingPage.gone.description')}
|
||||
primaryAction={{
|
||||
onClick: goToHomepage,
|
||||
label: t('landingPage.gone.goToHomepage'),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
case ErrorType.Timeout:
|
||||
return (
|
||||
<LandingPage
|
||||
Logo={WarningIcon}
|
||||
title={t('landingPage.timeout.title')}
|
||||
description={t('landingPage.timeout.description')}
|
||||
primaryAction={{
|
||||
onClick: handleRetry,
|
||||
label: retrying ? (
|
||||
<span className='flex items-center gap-2'>
|
||||
<Progress />
|
||||
{t('landingPage.timeout.retry')}
|
||||
</span>
|
||||
) : (
|
||||
t('landingPage.timeout.retry')
|
||||
),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
case ErrorType.RateLimited:
|
||||
return (
|
||||
<LandingPage
|
||||
@@ -125,24 +204,50 @@ function RecordNotFound({
|
||||
title={t('landingPage.rateLimited.title')}
|
||||
description={t('landingPage.rateLimited.description')}
|
||||
primaryAction={{
|
||||
onClick: () => window.location.reload(),
|
||||
label: t('landingPage.rateLimited.retry'),
|
||||
onClick: handleRetry,
|
||||
label: retrying ? (
|
||||
<span className='flex items-center gap-2'>
|
||||
<Progress />
|
||||
{t('landingPage.rateLimited.retry')}
|
||||
</span>
|
||||
) : (
|
||||
t('landingPage.rateLimited.retry')
|
||||
),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
case ErrorType.Unknown:
|
||||
default:
|
||||
// Unknown error - fall through to legacy handling
|
||||
break;
|
||||
return (
|
||||
<LandingPage
|
||||
Logo={ErrorIcon}
|
||||
title={t('landingPage.unknown.title')}
|
||||
description={t('landingPage.unknown.description')}
|
||||
primaryAction={{
|
||||
onClick: handleRetry,
|
||||
label: retrying ? (
|
||||
<span className='flex items-center gap-2'>
|
||||
<Progress />
|
||||
{t('landingPage.unknown.retry')}
|
||||
</span>
|
||||
) : (
|
||||
t('landingPage.unknown.retry')
|
||||
),
|
||||
}}
|
||||
secondaryAction={{
|
||||
onClick: goToHomepage,
|
||||
label: t('landingPage.unknown.goToHomepage'),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// LEGACY: If viewId is provided without error object, render the request access component
|
||||
if (viewId && currentWorkspaceId && !error) {
|
||||
return <RequestAccessContent viewId={viewId} workspaceId={currentWorkspaceId} />;
|
||||
}
|
||||
|
||||
// LEGACY: Original fallback rendering
|
||||
return (
|
||||
<div className={'flex h-full w-full flex-col items-center justify-center px-4'}>
|
||||
{!noContent && (
|
||||
|
||||
@@ -3,7 +3,7 @@ import { toast } from 'sonner';
|
||||
|
||||
import { APP_EVENTS } from '@/application/constants';
|
||||
import { UIVariant, ViewLayout, ViewMetaProps, YDoc } from '@/application/types';
|
||||
import { AppError, determineErrorType } from '@/application/utils/error-utils';
|
||||
import { AppError, determineErrorType, formatErrorForLogging } from '@/application/utils/error-utils';
|
||||
import Help from '@/components/_shared/help/Help';
|
||||
import { findView } from '@/components/_shared/outline/utils';
|
||||
import { AIChat } from '@/components/ai-chat';
|
||||
@@ -76,7 +76,7 @@ function AppPage() {
|
||||
const appError = determineErrorType(e);
|
||||
|
||||
setError(appError);
|
||||
console.error('[AppPage] Error loading view:', appError);
|
||||
console.error('[AppPage] Error loading view:', formatErrorForLogging(e));
|
||||
}
|
||||
},
|
||||
[loadView]
|
||||
|
||||
@@ -49,6 +49,7 @@
|
||||
},
|
||||
"include": [
|
||||
"src",
|
||||
".storybook",
|
||||
"vite.config.ts",
|
||||
"cypress.config.ts",
|
||||
"cypress",
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
"**/*.cy.tsx",
|
||||
"**/*.test.ts",
|
||||
"**/*.test.tsx",
|
||||
"**/*.stories.ts",
|
||||
"**/*.stories.tsx",
|
||||
"**/__tests__/**",
|
||||
"cypress/**/*",
|
||||
"dist",
|
||||
|
||||
Reference in New Issue
Block a user