refactor(core): use Route component

This commit is contained in:
Jimmfly
2025-05-16 11:25:23 +08:00
parent 565f61456f
commit f4afbb7c68
16 changed files with 744 additions and 437 deletions
+1
View File
@@ -16,6 +16,7 @@
"@affine/graphql": "workspace:*",
"@affine/i18n": "workspace:*",
"@affine/nbstore": "workspace:*",
"@affine/routes": "workspace:*",
"@affine/templates": "workspace:*",
"@affine/track": "workspace:*",
"@blocksuite/affine": "workspace:*",
@@ -3,6 +3,7 @@ import type { SettingTab } from '@affine/core/modules/dialogs/constant';
import { toDocSearchParams } from '@affine/core/modules/navigation';
import { getOpenUrlInDesktopAppLink } from '@affine/core/modules/open-in-app';
import { UserFriendlyError } from '@affine/error';
import { FACTORIES } from '@affine/routes';
import type { DocMode } from '@blocksuite/affine/model';
import { nanoid } from 'nanoid';
import { createContext, useCallback, useContext, useMemo } from 'react';
@@ -52,7 +53,7 @@ export function useNavigateHelper() {
pageId: string,
logic: RouteLogic = RouteLogic.PUSH
) => {
return navigate(`/workspace/${workspaceId}/${pageId}`, {
return navigate(FACTORIES.workspace.doc({ workspaceId, docId: pageId }), {
replace: logic === RouteLogic.REPLACE,
});
},
@@ -74,15 +75,18 @@ export function useNavigateHelper() {
refreshKey: nanoid(),
});
const query = search?.size ? `?${search.toString()}` : '';
return navigate(`/workspace/${workspaceId}/${pageId}${query}`, {
replace: logic === RouteLogic.REPLACE,
});
return navigate(
FACTORIES.workspace.doc({ workspaceId, docId: pageId }) + query,
{
replace: logic === RouteLogic.REPLACE,
}
);
},
[navigate]
);
const jumpToCollections = useCallback(
(workspaceId: string, logic: RouteLogic = RouteLogic.PUSH) => {
return navigate(`/workspace/${workspaceId}/collection`, {
return navigate(FACTORIES.workspace.collections({ workspaceId }), {
replace: logic === RouteLogic.REPLACE,
});
},
@@ -90,7 +94,7 @@ export function useNavigateHelper() {
);
const jumpToTags = useCallback(
(workspaceId: string, logic: RouteLogic = RouteLogic.PUSH) => {
return navigate(`/workspace/${workspaceId}/tag`, {
return navigate(FACTORIES.workspace.tags({ workspaceId }), {
replace: logic === RouteLogic.REPLACE,
});
},
@@ -102,7 +106,7 @@ export function useNavigateHelper() {
tagId: string,
logic: RouteLogic = RouteLogic.PUSH
) => {
return navigate(`/workspace/${workspaceId}/tag/${tagId}`, {
return navigate(FACTORIES.workspace.tags.tag({ workspaceId, tagId }), {
replace: logic === RouteLogic.REPLACE,
});
},
@@ -114,16 +118,22 @@ export function useNavigateHelper() {
collectionId: string,
logic: RouteLogic = RouteLogic.PUSH
) => {
return navigate(`/workspace/${workspaceId}/collection/${collectionId}`, {
replace: logic === RouteLogic.REPLACE,
});
return navigate(
FACTORIES.workspace.collections.collection({
workspaceId,
collectionId,
}),
{
replace: logic === RouteLogic.REPLACE,
}
);
},
[navigate]
);
const jumpToAll = useCallback(
(workspaceId: string, logic?: RouteLogic) => {
return navigate(`/workspace/${workspaceId}/all`, {
return navigate(FACTORIES.workspace.all({ workspaceId }), {
replace: logic === RouteLogic.REPLACE,
});
},
@@ -144,7 +154,7 @@ export function useNavigateHelper() {
const jumpTo404 = useCallback(
(logic: RouteLogic = RouteLogic.PUSH) => {
return navigate('/404', {
return navigate(FACTORIES.notFound(), {
replace: logic === RouteLogic.REPLACE,
});
},
@@ -152,7 +162,7 @@ export function useNavigateHelper() {
);
const jumpToExpired = useCallback(
(logic: RouteLogic = RouteLogic.PUSH) => {
return navigate('/expired', {
return navigate(FACTORIES.expired(), {
replace: logic === RouteLogic.REPLACE,
});
},
@@ -176,7 +186,7 @@ export function useNavigateHelper() {
}
return navigate(
'/sign-in' +
FACTORIES.signIn() +
(searchParams.toString() ? '?' + searchParams.toString() : ''),
{
replace: logic === RouteLogic.REPLACE,
@@ -196,7 +206,7 @@ export function useNavigateHelper() {
}
const encodedUrl = encodeURIComponent(deeplink);
return navigate(`/open-app/url?url=${encodedUrl}`);
return navigate(FACTORIES.openApp({ action: `url?url=${encodedUrl}` }));
},
[navigate]
);
@@ -204,7 +214,8 @@ export function useNavigateHelper() {
const jumpToImportTemplate = useCallback(
(name: string, snapshotUrl: string) => {
return navigate(
`/template/import?name=${encodeURIComponent(name)}&snapshotUrl=${encodeURIComponent(snapshotUrl)}`
FACTORIES.template.import() +
`?name=${encodeURIComponent(name)}&snapshotUrl=${encodeURIComponent(snapshotUrl)}`
);
},
[navigate]
@@ -221,7 +232,8 @@ export function useNavigateHelper() {
searchParams.set('tab', tab);
}
return navigate(
`/workspace/${workspaceId}/settings?${searchParams.toString()}`,
FACTORIES.workspace.settings({ workspaceId }) +
(searchParams.toString() ? `?${searchParams.toString()}` : ''),
{
replace: logic === RouteLogic.REPLACE,
}
@@ -14,9 +14,7 @@ import {
import { useI18n } from '@affine/i18n';
import { useLiveData, useService } from '@toeverything/infra';
import { useCallback } from 'react';
import type { LoaderFunction } from 'react-router';
import { redirect, useParams, useSearchParams } from 'react-router';
import { z } from 'zod';
import { useParams, useSearchParams } from 'react-router';
import { useMutation } from '../../../components/hooks/use-mutation';
import {
@@ -28,18 +26,6 @@ import { AppContainer } from '../../components/app-container';
import { ConfirmChangeEmail } from './confirm-change-email';
import { ConfirmVerifiedEmail } from './email-verified-email';
const authTypeSchema = z.enum([
'onboarding',
'setPassword',
'signIn',
'changePassword',
'signUp',
'changeEmail',
'confirm-change-email',
'subscription-redirect',
'verify-email',
]);
export const Component = () => {
const authService = useService(AuthService);
const account = useLiveData(authService.session.account$);
@@ -159,14 +145,3 @@ export const Component = () => {
}
return null;
};
export const loader: LoaderFunction = async args => {
if (!args.params.authType) {
return redirect('/404');
}
if (!authTypeSchema.safeParse(args.params.authType).success) {
return redirect('/404');
}
return null;
};
@@ -1,57 +1,19 @@
import { useAsyncNavigate } from '@affine/core/utils/use-async-navigate';
import { useService } from '@toeverything/infra';
import { useEffect, useRef } from 'react';
import { type LoaderFunction, redirect, useLoaderData } from 'react-router';
import { useLoaderData } from 'react-router';
import { AuthService } from '../../../modules/cloud';
import { supportedClient } from './common';
interface LoaderData {
export interface MagicLinkLoaderData {
token: string;
email: string;
redirectUri: string | null;
}
export const loader: LoaderFunction = ({ request }) => {
const url = new URL(request.url);
const params = url.searchParams;
const client = params.get('client');
const email = params.get('email');
const token = params.get('token');
const redirectUri = params.get('redirect_uri');
if (!email || !token) {
return redirect('/sign-in?error=Invalid magic link');
}
const payload: LoaderData = {
email,
token,
redirectUri,
};
if (!client || client === 'web') {
return payload;
}
const clientCheckResult = supportedClient.safeParse(client);
if (!clientCheckResult.success) {
return redirect('/sign-in?error=Invalid callback parameters');
}
const authParams = new URLSearchParams();
authParams.set('method', 'magic-link');
authParams.set('payload', JSON.stringify(payload));
return redirect(
`/open-app/url?url=${encodeURIComponent(`${client}://authentication?${authParams.toString()}`)}`
);
};
export const Component = () => {
// TODO(@eyhn): loading ui
const auth = useService(AuthService);
const data = useLoaderData() as LoaderData;
const data = useLoaderData() as MagicLinkLoaderData;
const nav = useAsyncNavigate();
@@ -1,62 +1,19 @@
import { useAsyncNavigate } from '@affine/core/utils/use-async-navigate';
import { useService } from '@toeverything/infra';
import { useEffect, useRef } from 'react';
import { type LoaderFunction, redirect, useLoaderData } from 'react-router';
import { useLoaderData } from 'react-router';
import { AuthService } from '../../../modules/cloud';
import { supportedClient } from './common';
interface LoaderData {
export interface OAuthCallbackLoaderData {
state: string;
code: string;
provider: string;
}
export const loader: LoaderFunction = async ({ request }) => {
const url = new URL(request.url);
const queries = url.searchParams;
const code = queries.get('code');
let stateStr = queries.get('state') ?? '{}';
if (!code || !stateStr) {
return redirect('/sign-in?error=Invalid oauth callback parameters');
}
try {
const { state, client, provider } = JSON.parse(stateStr);
stateStr = state;
const payload: LoaderData = {
state,
code,
provider,
};
if (!client || client === 'web') {
return payload;
}
const clientCheckResult = supportedClient.safeParse(client);
if (!clientCheckResult.success) {
return redirect('/sign-in?error=Invalid oauth callback parameters');
}
const authParams = new URLSearchParams();
authParams.set('method', 'oauth');
authParams.set('payload', JSON.stringify(payload));
authParams.set('server', location.origin);
return redirect(
`/open-app/url?url=${encodeURIComponent(`${client}://authentication?${authParams.toString()}`)}`
);
} catch {
return redirect('/sign-in?error=Invalid oauth callback parameters');
}
};
export const Component = () => {
const auth = useService(AuthService);
const data = useLoaderData() as LoaderData;
const data = useLoaderData() as OAuthCallbackLoaderData;
// loader data from useLoaderData is not reactive, so that we can safely
// assume the effect below is only triggered once
@@ -1,61 +1,19 @@
import { AuthService } from '@affine/core/modules/cloud';
import { useAsyncNavigate } from '@affine/core/utils';
import { OAuthProviderType } from '@affine/graphql';
import type { OAuthProviderType } from '@affine/graphql';
import { useService } from '@toeverything/infra';
import { useEffect } from 'react';
import { type LoaderFunction, redirect, useLoaderData } from 'react-router';
import { z } from 'zod';
import { useLoaderData } from 'react-router';
import { supportedClient } from './common';
const supportedProvider = z.nativeEnum(OAuthProviderType);
const oauthParameters = z.object({
provider: supportedProvider,
client: supportedClient,
redirectUri: z.string().optional().nullable(),
});
interface LoaderData {
interface OAuthLoginLoaderData {
provider: OAuthProviderType;
client: string;
redirectUri?: string;
}
export const loader: LoaderFunction = async ({ request }) => {
const url = new URL(request.url);
const searchParams = url.searchParams;
const provider = searchParams.get('provider');
const client = searchParams.get('client') ?? 'web';
const redirectUri = searchParams.get('redirect_uri');
// sign out first, web only
if (client === 'web') {
await fetch('/api/auth/sign-out');
}
const paramsParseResult = oauthParameters.safeParse({
provider,
client,
redirectUri,
});
if (paramsParseResult.success) {
return {
provider,
client,
redirectUri,
};
}
return redirect(
`/sign-in?error=${encodeURIComponent(`Invalid oauth parameters`)}`
);
};
export const Component = () => {
const auth = useService(AuthService);
const data = useLoaderData() as LoaderData;
const data = useLoaderData() as OAuthLoginLoaderData;
const nav = useAsyncNavigate();
@@ -1,24 +1,8 @@
import { DesktopApiService } from '@affine/core/modules/desktop-api';
import { useServiceOptional } from '@toeverything/infra';
import { useCallback } from 'react';
import { redirect } from 'react-router';
import { Onboarding } from '../../../components/affine/onboarding/onboarding';
import { appConfigStorage } from '../../../components/hooks/use-app-config-storage';
/**
* /onboarding page
*
* only for electron
*/
export const loader = () => {
if (!BUILD_CONFIG.isElectron && !appConfigStorage.get('onBoarding')) {
// onboarding is off, redirect to index
return redirect('/');
}
return null;
};
export const Component = () => {
const desktopApi = useServiceOptional(DesktopApiService);
@@ -1,53 +1,4 @@
import { DebugLogger } from '@affine/debug';
import { type LoaderFunction, Navigate, useLoaderData } from 'react-router';
const trustedDomain = [
'google.com',
'stripe.com',
'github.com',
'twitter.com',
'discord.gg',
'youtube.com',
't.me',
'reddit.com',
'affine.pro',
];
const logger = new DebugLogger('redirect_proxy');
/**
* /redirect-proxy page
*
* only for web
*/
export const loader: LoaderFunction = async ({ request }) => {
const url = new URL(request.url);
const searchParams = url.searchParams;
const redirectUri = searchParams.get('redirect_uri');
if (!redirectUri) {
return { allow: false };
}
try {
const target = new URL(redirectUri);
if (
target.hostname === window.location.hostname ||
trustedDomain.some(domain =>
new RegExp(`.?${domain}$`).test(target.hostname)
)
) {
location.href = redirectUri;
return { allow: true };
}
} catch (e) {
logger.error('Failed to parse redirect uri', e);
return { allow: false };
}
return { allow: true };
};
import { Navigate, useLoaderData } from 'react-router';
export const Component = () => {
const { allow } = useLoaderData() as { allow: boolean };
@@ -0,0 +1,219 @@
import { DebugLogger } from '@affine/debug';
import { OAuthProviderType } from '@affine/graphql';
import { FACTORIES } from '@affine/routes';
import type { LoaderFunction } from 'react-router';
import { redirect } from 'react-router';
import { z } from 'zod';
import { appConfigStorage } from '../components/hooks/use-app-config-storage';
import { supportedClient } from './pages/auth/common';
import type { MagicLinkLoaderData } from './pages/auth/magic-link';
import type { OAuthCallbackLoaderData } from './pages/auth/oauth-callback';
const trustedDomain = [
'google.com',
'stripe.com',
'github.com',
'twitter.com',
'discord.gg',
'youtube.com',
't.me',
'reddit.com',
'affine.pro',
];
const authTypeSchema = z.enum([
'onboarding',
'setPassword',
'signIn',
'changePassword',
'signUp',
'changeEmail',
'confirm-change-email',
'subscription-redirect',
'verify-email',
]);
const supportedProvider = z.nativeEnum(OAuthProviderType);
const oauthParameters = z.object({
provider: supportedProvider,
client: supportedClient,
redirectUri: z.string().optional().nullable(),
});
const redirectLogger = new DebugLogger('redirect_proxy');
/**
* /onboarding page
*
* only for electron
*/
export const onboardingLoader = async () => {
if (!BUILD_CONFIG.isElectron && !appConfigStorage.get('onBoarding')) {
// onboarding is off, redirect to index
return redirect('/');
}
return null;
};
/**
* /redirect-proxy page
*
* only for web
*/
export const redirectLoader: LoaderFunction = async ({ request }) => {
const url = new URL(request.url);
const searchParams = url.searchParams;
const redirectUri = searchParams.get('redirect_uri');
if (!redirectUri) {
return { allow: false };
}
try {
const target = new URL(redirectUri);
if (
target.hostname === window.location.hostname ||
trustedDomain.some(domain =>
new RegExp(`.?${domain}$`).test(target.hostname)
)
) {
location.href = redirectUri;
return { allow: true };
}
} catch (e) {
redirectLogger.error('Failed to parse redirect uri', e);
return { allow: false };
}
return { allow: true };
};
export const authLoader: LoaderFunction = async args => {
if (!args.params.authType) {
return redirect(FACTORIES.notFound());
}
if (!authTypeSchema.safeParse(args.params.authType).success) {
return redirect(FACTORIES.notFound());
}
return null;
};
export const magicLinkLoader: LoaderFunction = ({ request }) => {
const url = new URL(request.url);
const params = url.searchParams;
const client = params.get('client');
const email = params.get('email');
const token = params.get('token');
const redirectUri = params.get('redirect_uri');
if (!email || !token) {
return redirect(FACTORIES.signIn() + '?error=Invalid magic link');
}
const payload: MagicLinkLoaderData = {
email,
token,
redirectUri,
};
if (!client || client === 'web') {
return payload;
}
const clientCheckResult = supportedClient.safeParse(client);
if (!clientCheckResult.success) {
return redirect(FACTORIES.signIn() + '?error=Invalid callback parameters');
}
const authParams = new URLSearchParams();
authParams.set('method', 'magic-link');
authParams.set('payload', JSON.stringify(payload));
return redirect(
`${FACTORIES.openApp({ action: 'url' })}?url=${encodeURIComponent(`${client}://authentication?${authParams.toString()}`)}`
);
};
export const oauthLoginLoader: LoaderFunction = async ({ request }) => {
const url = new URL(request.url);
const searchParams = url.searchParams;
const provider = searchParams.get('provider');
const client = searchParams.get('client') ?? 'web';
const redirectUri = searchParams.get('redirect_uri');
// sign out first, web only
if (client === 'web') {
await fetch('/api/auth/sign-out');
}
const paramsParseResult = oauthParameters.safeParse({
provider,
client,
redirectUri,
});
if (paramsParseResult.success) {
return {
provider,
client,
redirectUri,
};
}
return redirect(
`${FACTORIES.signIn()}?error=${encodeURIComponent(`Invalid oauth parameters`)}`
);
};
export const oauthCallbackLoader: LoaderFunction = async ({ request }) => {
const url = new URL(request.url);
const queries = url.searchParams;
const code = queries.get('code');
let stateStr = queries.get('state') ?? '{}';
if (!code || !stateStr) {
return redirect(
`${FACTORIES.signIn()}?error=${encodeURIComponent(`Invalid oauth callback parameters`)}`
);
}
try {
const { state, client, provider } = JSON.parse(stateStr);
stateStr = state;
const payload: OAuthCallbackLoaderData = {
state,
code,
provider,
};
if (!client || client === 'web') {
return payload;
}
const clientCheckResult = supportedClient.safeParse(client);
if (!clientCheckResult.success) {
return redirect(
`${FACTORIES.signIn()}?error=${encodeURIComponent(`Invalid oauth callback parameters`)}`
);
}
const authParams = new URLSearchParams();
authParams.set('method', 'oauth');
authParams.set('payload', JSON.stringify(payload));
authParams.set('server', location.origin);
return redirect(
`${FACTORIES.openApp({ action: 'url' })}?url=${encodeURIComponent(`${client}://authentication?${authParams.toString()}`)}`
);
} catch {
return redirect(
`${FACTORIES.signIn()}?error=${encodeURIComponent(`Invalid oauth callback parameters`)}`
);
}
};
+204 -176
View File
@@ -1,16 +1,26 @@
import { wrapCreateBrowserRouterV7 } from '@sentry/react';
import { FACTORIES, lazy, RELATIVE_ROUTES } from '@affine/routes';
import { withSentryReactRouterV7Routing } from '@sentry/react';
import { useEffect, useState } from 'react';
import type { RouteObject } from 'react-router';
import type { Params } from 'react-router';
import {
createBrowserRouter as reactRouterCreateBrowserRouter,
redirect,
Route,
Routes as ReactRouterRoutes,
useNavigate,
} from 'react-router';
import { AffineErrorComponent } from '../components/affine/affine-error-boundary/affine-error-fallback';
import { NavigateContext } from '../components/hooks/use-navigate-helper';
import { AppContainer } from './components/app-container';
import { RootWrapper } from './pages/root';
import {
authLoader,
magicLinkLoader,
oauthCallbackLoader,
oauthLoginLoader,
onboardingLoader,
redirectLoader,
} from './router-loader';
export function RootRouter() {
const navigate = useNavigate();
const [ready, setReady] = useState(false);
@@ -27,176 +37,194 @@ export function RootRouter() {
)
);
}
export const topLevelRoutes = [
{
element: <RootRouter />,
errorElement: <AffineErrorComponent />,
children: [
{
path: '/',
lazy: async () => await import('./pages/index'),
},
{
path: '/workspace/:workspaceId/*',
lazy: async () => await import('./pages/workspace/index'),
},
{
path: '/share/:workspaceId/:pageId',
loader: ({ params }) => {
return redirect(`/workspace/${params.workspaceId}/${params.pageId}`);
},
},
{
path: '/404',
lazy: async () => await import('./pages/404'),
},
{
path: '/expired',
lazy: async () => await import('./pages/expired'),
},
{
path: '/invite/:inviteId',
lazy: async () => await import('./pages/invite'),
},
{
path: '/upgrade-success',
lazy: async () => await import('./pages/upgrade-success'),
},
{
path: '/upgrade-success/team',
lazy: async () => await import('./pages/upgrade-success/team'),
},
{
path: '/upgrade-success/self-hosted-team',
lazy: async () =>
await import('./pages/upgrade-success/self-host-team'),
},
{
path: '/ai-upgrade-success',
lazy: async () => await import('./pages/ai-upgrade-success'),
},
{
path: '/onboarding',
lazy: async () => await import('./pages/onboarding'),
},
{
path: '/redirect-proxy',
lazy: async () => await import('./pages/redirect'),
},
{
path: '/subscribe',
lazy: async () => await import('./pages/subscribe'),
},
{
path: '/upgrade-to-team',
lazy: async () => await import('./pages/upgrade-to-team'),
},
{
path: '/try-cloud',
loader: () => {
return redirect(
`/sign-in?redirect_uri=${encodeURIComponent('/?initCloud=true')}`
);
},
},
{
path: '/theme-editor',
lazy: async () => await import('./pages/theme-editor'),
},
{
path: '/clipper/import',
lazy: async () => await import('./pages/import-clipper'),
},
{
path: '/template/import',
lazy: async () => await import('./pages/import-template'),
},
{
path: '/template/preview',
loader: ({ request }) => {
const url = new URL(request.url);
const workspaceId = url.searchParams.get('workspaceId');
const docId = url.searchParams.get('docId');
const templateName = url.searchParams.get('name');
const templateMode = url.searchParams.get('mode');
const snapshotUrl = url.searchParams.get('snapshotUrl');
return redirect(
`/workspace/${workspaceId}/${docId}?${new URLSearchParams({
isTemplate: 'true',
templateName: templateName ?? '',
snapshotUrl: snapshotUrl ?? '',
mode: templateMode ?? 'page',
}).toString()}`
);
},
},
{
path: '/auth/:authType',
lazy: async () =>
await import(/* webpackChunkName: "auth" */ './pages/auth/auth'),
},
{
path: '/sign-In',
lazy: async () =>
await import(/* webpackChunkName: "auth" */ './pages/auth/sign-in'),
},
{
path: '/magic-link',
lazy: async () =>
await import(
/* webpackChunkName: "auth" */ './pages/auth/magic-link'
),
},
{
path: '/oauth/login',
lazy: async () =>
await import(
/* webpackChunkName: "auth" */ './pages/auth/oauth-login'
),
},
{
path: '/oauth/callback',
lazy: async () =>
await import(
/* webpackChunkName: "auth" */ './pages/auth/oauth-callback'
),
},
// deprecated, keep for old client compatibility
// TODO(@forehalo): remove
{
path: '/desktop-signin',
lazy: async () =>
await import(
/* webpackChunkName: "auth" */ './pages/auth/oauth-login'
),
},
// deprecated, keep for old client compatibility
// use '/sign-in'
// TODO(@forehalo): remove
{
path: '/signIn',
lazy: async () =>
await import(/* webpackChunkName: "auth" */ './pages/auth/sign-in'),
},
{
path: '/open-app/:action',
lazy: async () => await import('./pages/open-app'),
},
{
path: '*',
lazy: async () => await import('./pages/404'),
},
],
},
] satisfies [RouteObject, ...RouteObject[]];
const createBrowserRouter = wrapCreateBrowserRouterV7(
reactRouterCreateBrowserRouter
export const Index = lazy(async () => await import('./pages/index'));
export const Workspace = lazy(
async () => await import('./pages/workspace/index')
);
export const router = (
window.SENTRY_RELEASE ? createBrowserRouter : reactRouterCreateBrowserRouter
)(topLevelRoutes, {
basename: environment.subPath,
});
export const NotFound = lazy(async () => await import('./pages/404'));
export const Expired = lazy(async () => await import('./pages/expired'));
export const Invite = lazy(async () => await import('./pages/invite'));
export const UpgradeSuccess = lazy(
async () => await import('./pages/upgrade-success')
);
export const UpgradeSuccessTeam = lazy(
async () => await import('./pages/upgrade-success/team')
);
export const UpgradeSuccessSelfHostedTeam = lazy(
async () => await import('./pages/upgrade-success/self-host-team')
);
export const AIUpgradeSuccess = lazy(
async () => await import('./pages/ai-upgrade-success')
);
export const Subscribe = lazy(async () => await import('./pages/subscribe'));
export const UpgradeToTeam = lazy(
async () => await import('./pages/upgrade-to-team')
);
export const ThemeEditor = lazy(
async () => await import('./pages/theme-editor')
);
export const ImportClipper = lazy(
async () => await import('./pages/import-clipper')
);
export const ImportTemplate = lazy(
async () => await import('./pages/import-template')
);
export const OpenApp = lazy(async () => await import('./pages/open-app'));
export const Onboarding = lazy(async () => await import('./pages/onboarding'));
export const Redirect = lazy(async () => await import('./pages/redirect'));
export const Auth = lazy(
async () => await import(/* webpackChunkName: "auth" */ './pages/auth/auth')
);
export const SignIn = lazy(
async () =>
await import(/* webpackChunkName: "auth" */ './pages/auth/sign-in')
);
export const MagicLink = lazy(
async () =>
await import(/* webpackChunkName: "auth" */ './pages/auth/magic-link')
);
export const OAuthLogin = lazy(
async () =>
await import(/* webpackChunkName: "auth" */ './pages/auth/oauth-login')
);
export const OAuthCallback = lazy(
async () =>
await import(/* webpackChunkName: "auth" */ './pages/auth/oauth-callback')
);
// Define routes using JSX syntax for better type checking
export const routes = (
<Route
element={<RootRouter />}
errorElement={<AffineErrorComponent />}
hydrateFallbackElement={<AppContainer fallback />}
>
<Route path={RELATIVE_ROUTES.index}>
<Route index element={<Index />} />
<Route path={RELATIVE_ROUTES.workspace.index} element={<Workspace />} />
<Route
path={RELATIVE_ROUTES.share}
loader={async ({ params }: { params: Params<string> }) => {
return redirect(
FACTORIES.workspace.doc({
workspaceId: params.workspaceId ?? '',
docId: params.pageId ?? '',
})
);
}}
/>
<Route path={RELATIVE_ROUTES.notFound} element={<NotFound />} />
<Route path={RELATIVE_ROUTES.expired} element={<Expired />} />
<Route path={RELATIVE_ROUTES.invite} element={<Invite />} />
<Route path={RELATIVE_ROUTES.upgradeSuccess.index}>
<Route index element={<UpgradeSuccess />} />
<Route
path={RELATIVE_ROUTES.upgradeSuccess.team}
element={<UpgradeSuccessTeam />}
/>
<Route
path={RELATIVE_ROUTES.upgradeSuccess.selfHostTeam}
element={<UpgradeSuccessSelfHostedTeam />}
/>
</Route>
<Route
path={RELATIVE_ROUTES.aiUpgradeSuccess}
element={<AIUpgradeSuccess />}
/>
<Route
path={RELATIVE_ROUTES.onboarding}
element={<Onboarding />}
loader={onboardingLoader}
/>
<Route
path={RELATIVE_ROUTES.redirect}
element={<Redirect />}
loader={redirectLoader}
/>
<Route path={RELATIVE_ROUTES.subscribe} element={<Subscribe />} />
<Route path={RELATIVE_ROUTES.upgradeToTeam} element={<UpgradeToTeam />} />
<Route
path={RELATIVE_ROUTES.tryCloud}
loader={async () => {
return redirect(
FACTORIES.signIn() +
`?redirect_uri=${encodeURIComponent('/?initCloud=true')}`
);
}}
/>
<Route path={RELATIVE_ROUTES.themeEditor} element={<ThemeEditor />} />
<Route path="clipper/import" element={<ImportClipper />} />
<Route path={RELATIVE_ROUTES.template.index} element={<ImportTemplate />}>
<Route
path={RELATIVE_ROUTES.template.import}
element={<ImportTemplate />}
/>
<Route
path={RELATIVE_ROUTES.template.preview}
loader={async ({ request }: { request: Request }) => {
const url = new URL(request.url);
const workspaceId = url.searchParams.get('workspaceId');
const docId = url.searchParams.get('docId');
const templateName = url.searchParams.get('name');
const templateMode = url.searchParams.get('mode');
const snapshotUrl = url.searchParams.get('snapshotUrl');
return redirect(
FACTORIES.workspace.doc({
workspaceId: workspaceId ?? '',
docId: docId ?? '',
}) +
`?${new URLSearchParams({
isTemplate: 'true',
templateName: templateName ?? '',
snapshotUrl: snapshotUrl ?? '',
mode: templateMode ?? 'page',
}).toString()}`
);
}}
/>
</Route>
<Route
path={RELATIVE_ROUTES.auth}
element={<Auth />}
loader={authLoader}
/>
<Route path={RELATIVE_ROUTES.signIn} element={<SignIn />} />
<Route
path={RELATIVE_ROUTES.magicLink}
element={<MagicLink />}
loader={magicLinkLoader}
/>
<Route path={RELATIVE_ROUTES.oauth.index} element={<NotFound />}>
<Route
path={RELATIVE_ROUTES.oauth.login}
element={<OAuthLogin />}
loader={oauthLoginLoader}
/>
<Route
path={RELATIVE_ROUTES.oauth.callback}
element={<OAuthCallback />}
loader={oauthCallbackLoader}
/>
</Route>
<Route path={RELATIVE_ROUTES.openApp} element={<OpenApp />} />
{/* deprecated, keep for old client compatibility */}
{/* TODO(@forehalo): remove */}
<Route path="desktop-signin" element={<OAuthLogin />} />
{/* deprecated, keep for old client compatibility */}
{/* use '/sign-in' */}
{/* TODO(@forehalo): remove */}
<Route path="signIn" element={<SignIn />} />
<Route path="*" element={<NotFound />} />
</Route>
</Route>
);
// Apply Sentry wrapper to ReactRouterRoutes if needed
const Routes = window.SENTRY_RELEASE
? withSentryReactRouterV7Routing(ReactRouterRoutes)
: ReactRouterRoutes;
// Export Router component - will be wrapped by BrowserRouter in app.tsx
export const Router = () => <Routes>{routes}</Routes>;
+15 -12
View File
@@ -1,4 +1,5 @@
import { NavigateContext } from '@affine/core/components/hooks/use-navigate-helper';
import { ROUTES } from '@affine/routes';
import { wrapCreateBrowserRouterV7 } from '@sentry/react';
import { useEffect, useState } from 'react';
import type { RouteObject } from 'react-router';
@@ -34,58 +35,60 @@ export const topLevelRoutes = [
hydrateFallbackElement: <AppFallback />,
children: [
{
path: '/',
path: ROUTES.index,
lazy: async () => await import('./pages/index'),
},
{
path: '/workspace/:workspaceId/*',
path: `${ROUTES.workspace.index}/*`,
lazy: async () => await import('./pages/workspace/index'),
},
{
path: '/share/:workspaceId/:pageId',
path: ROUTES.share,
loader: async ({ params }) => {
return redirect(`/workspace/${params.workspaceId}/${params.pageId}`);
return redirect(
`/workspaces/${params.workspaceId}/docs/${params.pageId}`
);
},
},
{
path: '/404',
path: ROUTES.notFound,
lazy: async () => await import('./pages/404'),
},
{
path: '/auth/:authType',
path: ROUTES.auth,
lazy: async () => await import('./pages/auth'),
},
{
path: '/sign-in',
path: ROUTES.signIn,
lazy: async () => await import('./pages/sign-in'),
},
{
path: '/magic-link',
path: ROUTES.magicLink,
lazy: async () =>
await import(
/* webpackChunkName: "auth" */ '@affine/core/desktop/pages/auth/magic-link'
),
},
{
path: '/oauth/login',
path: ROUTES.oauth.login,
lazy: async () =>
await import(
/* webpackChunkName: "auth" */ '@affine/core/desktop/pages/auth/oauth-login'
),
},
{
path: '/oauth/callback',
path: ROUTES.oauth.callback,
lazy: async () =>
await import(
/* webpackChunkName: "auth" */ '@affine/core/desktop/pages/auth/oauth-callback'
),
},
{
path: '/redirect-proxy',
path: ROUTES.redirect,
lazy: async () => await import('@affine/core/desktop/pages/redirect'),
},
{
path: '/open-app/:action',
path: ROUTES.openApp,
lazy: async () => await import('@affine/core/desktop/pages/open-app'),
},
{