Skip to content

Commit 88c4347

Browse files
authored
chore: display no access error (#189)
* chore: display no access error * chore: add storybook * chore: fix test
1 parent a3945b0 commit 88c4347

File tree

8 files changed

+169
-30
lines changed

8 files changed

+169
-30
lines changed

cypress/e2e/page/publish-page.cy.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ describe('Publish Page Test', () => {
2121
if (err.message.includes('No workspace or service found') ||
2222
err.message.includes('createThemeNoVars_default is not a function') ||
2323
err.message.includes('View not found') ||
24+
err.message.includes('Record not found') ||
25+
err.message.includes('Request failed') ||
2426
err.message.includes('Failed to execute \'writeText\' on \'Clipboard\': Document is not focused') ||
2527
err.name === 'NotAllowedError') {
2628
return false;
@@ -671,7 +673,9 @@ describe('Publish Page Test', () => {
671673
cy.on('uncaught:exception', (err: Error) => {
672674
if (err.message.includes('No workspace or service found') ||
673675
err.message.includes('createThemeNoVars_default is not a function') ||
674-
err.message.includes('View not found')) {
676+
err.message.includes('View not found') ||
677+
err.message.includes('Record not found') ||
678+
err.message.includes('Request failed')) {
675679
return false;
676680
}
677681
return true;
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import type { Meta, StoryObj } from '@storybook/react-vite';
2+
3+
import { withContextsMinimal } from '../../../../.storybook/decorators';
4+
5+
import { ErrorPage } from './ErrorPage';
6+
7+
const meta = {
8+
title: 'Landing Pages/ErrorPage',
9+
component: ErrorPage,
10+
parameters: {
11+
layout: 'fullscreen',
12+
},
13+
decorators: [withContextsMinimal],
14+
tags: ['autodocs'],
15+
} satisfies Meta<typeof ErrorPage>;
16+
17+
export default meta;
18+
type Story = StoryObj<typeof meta>;
19+
20+
export const Default: Story = {};
21+
22+
export const WithRetry: Story = {
23+
args: {
24+
onRetry: async () => {
25+
await new Promise((resolve) => setTimeout(resolve, 2000));
26+
},
27+
},
28+
};
29+
30+
export const WithErrorDetails: Story = {
31+
args: {
32+
error: {
33+
code: 1012,
34+
message: 'You do not have permission to access this workspace.',
35+
},
36+
onRetry: async () => {
37+
await new Promise((resolve) => setTimeout(resolve, 2000));
38+
},
39+
},
40+
};
41+
42+
export const WithNetworkError: Story = {
43+
args: {
44+
error: {
45+
code: -1,
46+
message: 'Network error. Please check your connection.',
47+
},
48+
onRetry: async () => {
49+
await new Promise((resolve) => setTimeout(resolve, 2000));
50+
},
51+
},
52+
};
53+
54+
export const WithLongErrorMessage: Story = {
55+
args: {
56+
error: {
57+
code: 500,
58+
message: 'Internal server error: The request could not be processed due to an unexpected condition. Please try again later or contact support if the problem persists. Request ID: abc123-def456-ghi789',
59+
},
60+
onRetry: async () => {
61+
await new Promise((resolve) => setTimeout(resolve, 2000));
62+
},
63+
},
64+
};
65+
66+
export const WithErrorMessageOnly: Story = {
67+
args: {
68+
error: {
69+
message: 'Something unexpected happened.',
70+
},
71+
},
72+
};

src/components/_shared/landing-page/ErrorPage.tsx

Lines changed: 55 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,72 @@
1-
import { useState } from 'react';
2-
import { Trans, useTranslation } from 'react-i18next';
1+
import { useCallback, useState } from 'react';
2+
import { useTranslation } from 'react-i18next';
3+
import { toast } from 'sonner';
34

45
import { ReactComponent as ErrorLogo } from '@/assets/icons/warning_logo.svg';
56
import LandingPage from '@/components/_shared/landing-page/LandingPage';
67
import { Progress } from '@/components/ui/progress';
78

8-
export function ErrorPage({ onRetry }: { onRetry?: () => Promise<void> }) {
9+
interface ErrorPageProps {
10+
onRetry?: () => Promise<void>;
11+
error?: {
12+
code?: number;
13+
message?: string;
14+
};
15+
}
16+
17+
export function ErrorPage({ onRetry, error }: ErrorPageProps) {
918
const { t } = useTranslation();
1019

1120
const [loading, setLoading] = useState(false);
1221

22+
const handleCopyError = useCallback(async () => {
23+
if (!error) return;
24+
25+
const errorText = error.code
26+
? `Error: ${error.message}\nCode: ${error.code}`
27+
: `Error: ${error.message}`;
28+
29+
try {
30+
await navigator.clipboard.writeText(errorText);
31+
toast.success('Error details copied to clipboard', { duration: 3000 });
32+
} catch (e) {
33+
console.error('Failed to copy:', e);
34+
toast.error('Failed to copy error details', { duration: 3000 });
35+
}
36+
}, [error]);
37+
1338
return (
1439
<LandingPage
1540
Logo={ErrorLogo}
1641
title={t('landingPage.error.title')}
1742
description={
18-
<Trans
19-
i18nKey={'landingPage.error.description'}
20-
components={{
21-
support: (
22-
<span
23-
onClick={() => window.open('mailto:[email protected]', '_blank')}
24-
className='cursor-pointer text-text-action'
25-
>
26-
27-
</span>
28-
),
29-
}}
30-
/>
43+
<>
44+
<div>
45+
{t('landingPage.error.descriptionShort', 'This might be due to a network issue or a temporary server error. Please check your internet connection or try again later.')}
46+
</div>
47+
<div className='mt-4'>
48+
{t('landingPage.error.contactSupport', 'If the problem persists, ')}
49+
{error?.message && (
50+
<>
51+
<span
52+
onClick={handleCopyError}
53+
className='cursor-pointer text-text-action hover:underline'
54+
>
55+
{t('landingPage.error.copyError', 'copy error')}
56+
</span>
57+
{' and '}
58+
</>
59+
)}
60+
{t('landingPage.error.contact', 'contact ')}
61+
<span
62+
onClick={() => window.open('mailto:[email protected]', '_blank')}
63+
className='cursor-pointer text-text-action hover:underline'
64+
>
65+
66+
</span>
67+
.
68+
</div>
69+
</>
3170
}
3271
primaryAction={
3372
onRetry

src/components/app/components/AppContextConsumer.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { Awareness } from 'y-protocols/awareness';
55
import { AIChatProvider } from '@/components/ai-chat/AIChatProvider';
66
import { AppOverlayProvider } from '@/components/app/app-overlay/AppOverlayProvider';
77
import { AppContext, useAppViewId, useCurrentWorkspaceId } from '@/components/app/app.hooks';
8+
import { RequestAccessError } from '@/components/app/hooks/useWorkspaceData';
89
import RequestAccess from '@/components/app/landing-pages/RequestAccess';
910
import { useCurrentUser } from '@/components/main/app.hooks';
1011

@@ -14,7 +15,7 @@ const ViewModal = React.lazy(() => import('@/components/app/ViewModal'));
1415

1516
interface AppContextConsumerProps {
1617
children: React.ReactNode;
17-
requestAccessOpened: boolean;
18+
requestAccessError: RequestAccessError | null;
1819
openModalViewId?: string;
1920
setOpenModalViewId: (id: string | undefined) => void;
2021
awarenessMap: Record<string, Awareness>;
@@ -23,15 +24,15 @@ interface AppContextConsumerProps {
2324
// Component that consumes all internal contexts and provides the unified AppContext
2425
// This maintains the original AppContext API while using the new layered architecture internally
2526
export const AppContextConsumer: React.FC<AppContextConsumerProps> = memo(
26-
({ children, requestAccessOpened, openModalViewId, setOpenModalViewId, awarenessMap }) => {
27+
({ children, requestAccessError, openModalViewId, setOpenModalViewId, awarenessMap }) => {
2728
// Merge all layer data into the complete AppContextType
2829
const allContextData = useAllContextData(awarenessMap);
2930

3031
return (
3132
<AppContext.Provider value={allContextData}>
3233
<AIChatProvider>
3334
<AppOverlayProvider>
34-
{requestAccessOpened ? <RequestAccess /> : children}
35+
{requestAccessError ? <RequestAccess error={requestAccessError} /> : children}
3536
{
3637
<Suspense>
3738
<ViewModal

src/components/app/hooks/useWorkspaceData.ts

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,21 @@ import { useNavigate } from 'react-router-dom';
44
import { validate as uuidValidate } from 'uuid';
55

66
import { APP_EVENTS } from '@/application/constants';
7+
import { invalidToken } from '@/application/session/token';
78
import { DatabaseRelations, MentionablePerson, UIVariant, View, ViewLayout } from '@/application/types';
89
import { findView, findViewByLayout } from '@/components/_shared/outline/utils';
910
import { createDeduplicatedNoArgsRequest } from '@/utils/deduplicateRequest';
1011

1112
import { useAuthInternal } from '../contexts/AuthInternalContext';
1213
import { useSyncInternal } from '../contexts/SyncInternalContext';
1314

14-
const USER_NO_ACCESS_CODE = [1024, 1012];
15+
const USER_NO_ACCESS_CODE = 1012;
16+
const USER_UNAUTHORIZED_CODE = 1024;
17+
18+
export interface RequestAccessError {
19+
code: number;
20+
message: string;
21+
}
1522

1623
// Hook for managing workspace data (outline, favorites, recent, trash)
1724
export function useWorkspaceData() {
@@ -25,7 +32,7 @@ export function useWorkspaceData() {
2532
const [recentViews, setRecentViews] = useState<View[]>();
2633
const [trashList, setTrashList] = useState<View[]>();
2734
const [workspaceDatabases, setWorkspaceDatabases] = useState<DatabaseRelations | undefined>(undefined);
28-
const [requestAccessOpened, setRequestAccessOpened] = useState(false);
35+
const [requestAccessError, setRequestAccessError] = useState<RequestAccessError | null>(null);
2936

3037
const mentionableUsersRef = useRef<MentionablePerson[]>([]);
3138

@@ -110,8 +117,17 @@ export function useWorkspaceData() {
110117
// eslint-disable-next-line @typescript-eslint/no-explicit-any
111118
} catch (e: any) {
112119
console.error('App outline not found');
113-
if (USER_NO_ACCESS_CODE.includes(e.code)) {
114-
setRequestAccessOpened(true);
120+
if (e.code === USER_UNAUTHORIZED_CODE) {
121+
invalidToken();
122+
navigate('/login');
123+
return;
124+
}
125+
126+
if (e.code === USER_NO_ACCESS_CODE) {
127+
setRequestAccessError({
128+
code: e.code,
129+
message: e.message,
130+
});
115131
return;
116132
}
117133
}
@@ -320,7 +336,7 @@ export function useWorkspaceData() {
320336
recentViews,
321337
trashList,
322338
workspaceDatabases,
323-
requestAccessOpened,
339+
requestAccessError,
324340
loadOutline,
325341
loadFavoriteViews,
326342
loadRecentViews,

src/components/app/landing-pages/RequestAccess.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import { ReactComponent as AppFlowyLogo } from '@/assets/icons/appflowy.svg';
2+
import { RequestAccessError } from '@/components/app/hooks/useWorkspaceData';
23
import { RequestAccessContent } from '@/components/app/share/RequestAccessContent';
34

4-
function RequestAccess() {
5+
interface RequestAccessProps {
6+
error?: RequestAccessError;
7+
}
8+
9+
function RequestAccess({ error }: RequestAccessProps) {
510
return (
611
<div className='flex h-screen w-screen flex-col bg-background-primary'>
712
<div className='absolute left-0 top-0 flex h-[60px] w-full items-center justify-between gap-[10px] p-4'>
@@ -15,7 +20,7 @@ function RequestAccess() {
1520
</span>
1621
</div>
1722
<div className='flex w-full flex-1 items-center justify-center'>
18-
<RequestAccessContent />
23+
<RequestAccessContent error={error} />
1924
</div>
2025
</div>
2126
);

src/components/app/layers/AppBusinessLayer.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ export const AppBusinessLayer: React.FC<AppBusinessLayerProps> = ({ children })
4747
recentViews,
4848
trashList,
4949
workspaceDatabases,
50-
requestAccessOpened,
50+
requestAccessError,
5151
loadOutline,
5252
loadFavoriteViews,
5353
loadRecentViews,
@@ -295,7 +295,7 @@ export const AppBusinessLayer: React.FC<AppBusinessLayerProps> = ({ children })
295295
return (
296296
<BusinessInternalContext.Provider value={businessContextValue}>
297297
<AppContextConsumer
298-
requestAccessOpened={requestAccessOpened}
298+
requestAccessError={requestAccessError}
299299
openModalViewId={openModalViewId}
300300
setOpenModalViewId={setOpenModalViewId}
301301
awarenessMap={awarenessMap}

src/components/app/share/RequestAccessContent.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { toast } from 'sonner';
66
import { ReactComponent as NoAccessLogo } from '@/assets/icons/no_access.svg';
77
import { ReactComponent as SuccessLogo } from '@/assets/icons/success_logo.svg';
88
import { useAppViewId, useCurrentWorkspaceId } from '@/components/app/app.hooks';
9+
import { RequestAccessError } from '@/components/app/hooks/useWorkspaceData';
910
import { useCurrentUser, useService } from '@/components/main/app.hooks';
1011
import { Button } from '@/components/ui/button';
1112
import { Progress } from '@/components/ui/progress';
@@ -15,9 +16,10 @@ const REPEAT_REQUEST_CODE = 1043;
1516
interface RequestAccessContentProps {
1617
viewId?: string;
1718
workspaceId?: string;
19+
error?: RequestAccessError;
1820
}
1921

20-
export function RequestAccessContent({ viewId: propViewId, workspaceId: propWorkspaceId }: RequestAccessContentProps) {
22+
export function RequestAccessContent({ viewId: propViewId, workspaceId: propWorkspaceId, error: _error }: RequestAccessContentProps) {
2123
const { t } = useTranslation();
2224
const service = useService();
2325
const currentWorkspaceId = useCurrentWorkspaceId();

0 commit comments

Comments
 (0)