Compare commits

...

2 Commits

Author SHA1 Message Date
Jimmfly
f4afbb7c68 refactor(core): use Route component 2025-05-23 16:59:22 +08:00
Jimmfly
565f61456f feat(core): upgrade react-router from v6 to v7 2025-05-23 16:59:21 +08:00
91 changed files with 1647 additions and 1358 deletions

View File

@@ -51,7 +51,7 @@
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-hook-form": "^7.54.1", "react-hook-form": "^7.54.1",
"react-resizable-panels": "^3.0.0", "react-resizable-panels": "^3.0.0",
"react-router-dom": "^7.5.1", "react-router": "^7.6.0",
"sonner": "^2.0.0", "sonner": "^2.0.0",
"swr": "^2.2.5", "swr": "^2.2.5",
"vaul": "^1.1.1", "vaul": "^1.1.1",

View File

@@ -1,5 +1,5 @@
import { Toaster } from '@affine/admin/components/ui/sonner'; import { Toaster } from '@affine/admin/components/ui/sonner';
import { lazy, ROUTES } from '@affine/routes'; import { FACTORIES, lazy, RELATIVE_ROUTES } from '@affine/routes';
import { withSentryReactRouterV7Routing } from '@sentry/react'; import { withSentryReactRouterV7Routing } from '@sentry/react';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { import {
@@ -9,7 +9,7 @@ import {
Route, Route,
Routes as ReactRouterRoutes, Routes as ReactRouterRoutes,
useLocation, useLocation,
} from 'react-router-dom'; } from 'react-router';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { SWRConfig } from 'swr'; import { SWRConfig } from 'swr';
@@ -18,22 +18,24 @@ import { isAdmin, useCurrentUser, useServerConfig } from './modules/common';
import { Layout } from './modules/layout'; import { Layout } from './modules/layout';
export const Setup = lazy( export const Setup = lazy(
() => import(/* webpackChunkName: "setup" */ './modules/setup') async () => await import(/* webpackChunkName: "setup" */ './modules/setup')
); );
export const Accounts = lazy( export const Accounts = lazy(
() => import(/* webpackChunkName: "accounts" */ './modules/accounts') async () =>
await import(/* webpackChunkName: "accounts" */ './modules/accounts')
); );
export const AI = lazy( export const AI = lazy(
() => import(/* webpackChunkName: "ai" */ './modules/ai') async () => await import(/* webpackChunkName: "ai" */ './modules/ai')
); );
export const About = lazy( export const About = lazy(
() => import(/* webpackChunkName: "about" */ './modules/about') async () => await import(/* webpackChunkName: "about" */ './modules/about')
); );
export const Settings = lazy( export const Settings = lazy(
() => import(/* webpackChunkName: "settings" */ './modules/settings') async () =>
await import(/* webpackChunkName: "settings" */ './modules/settings')
); );
export const Auth = lazy( export const Auth = lazy(
() => import(/* webpackChunkName: "auth" */ './modules/auth') async () => await import(/* webpackChunkName: "auth" */ './modules/auth')
); );
const Routes = window.SENTRY_RELEASE const Routes = window.SENTRY_RELEASE
@@ -50,7 +52,7 @@ function AuthenticatedRoutes() {
}, [user]); }, [user]);
if (!user || !isAdmin(user)) { if (!user || !isAdmin(user)) {
return <Navigate to="/admin/auth" />; return <Navigate to={FACTORIES.admin.auth()} />;
} }
return ( return (
@@ -86,19 +88,21 @@ export const App = () => {
> >
<BrowserRouter basename={environment.subPath}> <BrowserRouter basename={environment.subPath}>
<Routes> <Routes>
<Route path={ROUTES.admin.index} element={<RootRoutes />}> <Route path={RELATIVE_ROUTES.admin.index}>
<Route path={ROUTES.admin.auth} element={<Auth />} /> <Route index element={<RootRoutes />} />
<Route path={ROUTES.admin.setup} element={<Setup />} /> <Route path={RELATIVE_ROUTES.admin.auth} element={<Auth />} />
<Route path={RELATIVE_ROUTES.admin.setup} element={<Setup />} />
<Route element={<AuthenticatedRoutes />}> <Route element={<AuthenticatedRoutes />}>
<Route path={ROUTES.admin.accounts} element={<Accounts />} />
<Route path={ROUTES.admin.ai} element={<AI />} />
<Route path={ROUTES.admin.about} element={<About />} />
<Route <Route
path={ROUTES.admin.settings.index} path={RELATIVE_ROUTES.admin.accounts}
element={<Settings />} element={<Accounts />}
> />
<Route path={RELATIVE_ROUTES.admin.ai} element={<AI />} />
<Route path={RELATIVE_ROUTES.admin.about} element={<About />} />
<Route path={RELATIVE_ROUTES.admin.settings.index}>
<Route index element={<Settings />} />
<Route <Route
path={ROUTES.admin.settings.module} path={RELATIVE_ROUTES.admin.settings.module}
element={<Settings />} element={<Settings />}
/> />
</Route> </Route>

View File

@@ -4,7 +4,7 @@ import { Label } from '@affine/admin/components/ui/label';
import { FeatureType, getUserFeaturesQuery } from '@affine/graphql'; import { FeatureType, getUserFeaturesQuery } from '@affine/graphql';
import type { FormEvent } from 'react'; import type { FormEvent } from 'react';
import { useCallback, useRef } from 'react'; import { useCallback, useRef } from 'react';
import { Navigate } from 'react-router-dom'; import { Navigate } from 'react-router';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { affineFetch } from '../../fetch-utils'; import { affineFetch } from '../../fetch-utils';

View File

@@ -1,5 +1,5 @@
import { useCallback } from 'react'; import { useCallback } from 'react';
import { NavLink } from 'react-router-dom'; import { NavLink } from 'react-router';
import { buttonVariants } from '../../components/ui/button'; import { buttonVariants } from '../../components/ui/button';
import { cn } from '../../utils'; import { cn } from '../../utils';

View File

@@ -1,7 +1,7 @@
import { buttonVariants } from '@affine/admin/components/ui/button'; import { buttonVariants } from '@affine/admin/components/ui/button';
import { cn } from '@affine/admin/utils'; import { cn } from '@affine/admin/utils';
import { cssVarV2 } from '@toeverything/theme/v2'; import { cssVarV2 } from '@toeverything/theme/v2';
import { NavLink } from 'react-router-dom'; import { NavLink } from 'react-router';
interface NavItemProps { interface NavItemProps {
icon: React.ReactNode; icon: React.ReactNode;

View File

@@ -2,7 +2,7 @@ import { buttonVariants } from '@affine/admin/components/ui/button';
import { cn } from '@affine/admin/utils'; import { cn } from '@affine/admin/utils';
import { AccountIcon, AiOutlineIcon, SelfhostIcon } from '@blocksuite/icons/rc'; import { AccountIcon, AiOutlineIcon, SelfhostIcon } from '@blocksuite/icons/rc';
import { cssVarV2 } from '@toeverything/theme/v2'; import { cssVarV2 } from '@toeverything/theme/v2';
import { NavLink } from 'react-router-dom'; import { NavLink } from 'react-router';
import { ServerVersion } from './server-version'; import { ServerVersion } from './server-version';
import { SettingsItem } from './settings-item'; import { SettingsItem } from './settings-item';

View File

@@ -10,7 +10,7 @@ import { SettingsIcon } from '@blocksuite/icons/rc';
import * as NavigationMenuPrimitive from '@radix-ui/react-navigation-menu'; import * as NavigationMenuPrimitive from '@radix-ui/react-navigation-menu';
import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area'; import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area';
import { cssVarV2 } from '@toeverything/theme/v2'; import { cssVarV2 } from '@toeverything/theme/v2';
import { NavLink } from 'react-router-dom'; import { NavLink } from 'react-router';
import { KNOWN_CONFIG_GROUPS, UNKNOWN_CONFIG_GROUPS } from '../settings/config'; import { KNOWN_CONFIG_GROUPS, UNKNOWN_CONFIG_GROUPS } from '../settings/config';
import { NormalSubItem } from './collapsible-item'; import { NormalSubItem } from './collapsible-item';

View File

@@ -8,7 +8,7 @@ import {
import { validateEmailAndPassword } from '@affine/admin/utils'; import { validateEmailAndPassword } from '@affine/admin/utils';
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks'; import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { affineFetch } from '../../fetch-utils'; import { affineFetch } from '../../fetch-utils';
@@ -149,7 +149,7 @@ export const Form = () => {
} }
if (current === count) { if (current === count) {
return navigate('/', { replace: true }); return await navigate('/', { replace: true });
} }
api?.scrollNext(); api?.scrollNext();
@@ -168,7 +168,7 @@ export const Form = () => {
const onPrevious = useAsyncCallback(async () => { const onPrevious = useAsyncCallback(async () => {
if (current === count) { if (current === count) {
if (serverConfig.initialized === true) { if (serverConfig.initialized === true) {
return navigate('/admin', { replace: true }); return await navigate('/admin', { replace: true });
} }
toast.error('Goto Admin Panel failed, please try again.'); toast.error('Goto Admin Panel failed, please try again.');
return; return;

View File

@@ -1,4 +1,4 @@
import { Navigate } from 'react-router-dom'; import { Navigate } from 'react-router';
import { useServerConfig } from '../common'; import { useServerConfig } from '../common';
import { Form } from './form'; import { Form } from './form';

View File

@@ -31,7 +31,7 @@
"next-themes": "^0.4.4", "next-themes": "^0.4.4",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-router-dom": "^6.28.0" "react-router": "^7.6.0"
}, },
"devDependencies": { "devDependencies": {
"@capacitor/cli": "^7.0.0", "@capacitor/cli": "^7.0.0",

View File

@@ -1,6 +1,5 @@
import { getStoreManager } from '@affine/core/blocksuite/manager/store'; import { getStoreManager } from '@affine/core/blocksuite/manager/store';
import { AffineContext } from '@affine/core/components/context'; import { AffineContext } from '@affine/core/components/context';
import { AppFallback } from '@affine/core/mobile/components/app-fallback';
import { configureMobileModules } from '@affine/core/mobile/modules'; import { configureMobileModules } from '@affine/core/mobile/modules';
import { VirtualKeyboardProvider } from '@affine/core/mobile/modules/virtual-keyboard'; import { VirtualKeyboardProvider } from '@affine/core/mobile/modules/virtual-keyboard';
import { router } from '@affine/core/mobile/router'; import { router } from '@affine/core/mobile/router';
@@ -46,7 +45,7 @@ import { OpClient } from '@toeverything/infra/op';
import { AsyncCall } from 'async-call-rpc'; import { AsyncCall } from 'async-call-rpc';
import { useTheme } from 'next-themes'; import { useTheme } from 'next-themes';
import { Suspense, useEffect } from 'react'; import { Suspense, useEffect } from 'react';
import { RouterProvider } from 'react-router-dom'; import { RouterProvider } from 'react-router/dom';
import { AffineTheme } from './plugins/affine-theme'; import { AffineTheme } from './plugins/affine-theme';
import { AIButton } from './plugins/ai-button'; import { AIButton } from './plugins/ai-button';
@@ -60,10 +59,6 @@ window.addEventListener('beforeunload', () => {
storeManagerClient.dispose(); storeManagerClient.dispose();
}); });
const future = {
v7_startTransition: true,
} as const;
const framework = new Framework(); const framework = new Framework();
configureCommonModules(framework); configureCommonModules(framework);
configureBrowserWorkbenchModule(framework); configureBrowserWorkbenchModule(framework);
@@ -342,11 +337,7 @@ export function App() {
<I18nProvider> <I18nProvider>
<AffineContext store={getCurrentStore()}> <AffineContext store={getCurrentStore()}>
<ThemeProvider /> <ThemeProvider />
<RouterProvider <RouterProvider router={router} />
fallbackElement={<AppFallback />}
router={router}
future={future}
/>
</AffineContext> </AffineContext>
</I18nProvider> </I18nProvider>
</FrameworkRoot> </FrameworkRoot>

View File

@@ -25,7 +25,7 @@
"next-themes": "^0.4.4", "next-themes": "^0.4.4",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-router-dom": "^6.28.0", "react-router": "^7.6.0",
"uuid": "^11.0.3", "uuid": "^11.0.3",
"webm-muxer": "^5.0.3" "webm-muxer": "^5.0.3"
}, },

View File

@@ -1,13 +1,12 @@
import { AffineContext } from '@affine/core/components/context'; import { AffineContext } from '@affine/core/components/context';
import { WindowsAppControls } from '@affine/core/components/pure/header/windows-app-controls'; import { WindowsAppControls } from '@affine/core/components/pure/header/windows-app-controls';
import { AppContainer } from '@affine/core/desktop/components/app-container'; import { Router } from '@affine/core/desktop/router';
import { router } from '@affine/core/desktop/router';
import { I18nProvider } from '@affine/core/modules/i18n'; import { I18nProvider } from '@affine/core/modules/i18n';
import createEmotionCache from '@affine/core/utils/create-emotion-cache'; import createEmotionCache from '@affine/core/utils/create-emotion-cache';
import { CacheProvider } from '@emotion/react'; import { CacheProvider } from '@emotion/react';
import { FrameworkRoot, getCurrentStore } from '@toeverything/infra'; import { FrameworkRoot, getCurrentStore } from '@toeverything/infra';
import { Suspense } from 'react'; import { Suspense } from 'react';
import { RouterProvider } from 'react-router-dom'; import { BrowserRouter } from 'react-router';
import { setupEffects } from './effects'; import { setupEffects } from './effects';
import { DesktopThemeSync } from './theme-sync'; import { DesktopThemeSync } from './theme-sync';
@@ -34,10 +33,6 @@ if (
const cache = createEmotionCache(); const cache = createEmotionCache();
const future = {
v7_startTransition: true,
} as const;
export function App() { export function App() {
return ( return (
<Suspense> <Suspense>
@@ -46,11 +41,9 @@ export function App() {
<I18nProvider> <I18nProvider>
<AffineContext store={getCurrentStore()}> <AffineContext store={getCurrentStore()}>
<DesktopThemeSync /> <DesktopThemeSync />
<RouterProvider <BrowserRouter basename={environment.subPath}>
fallbackElement={<AppContainer fallback />} <Router />
router={router} </BrowserRouter>
future={future}
/>
{environment.isWindows && ( {environment.isWindows && (
<div style={{ position: 'fixed', right: 0, top: 0, zIndex: 5 }}> <div style={{ position: 'fixed', right: 0, top: 0, zIndex: 5 }}>
<WindowsAppControls /> <WindowsAppControls />

View File

@@ -35,7 +35,7 @@
"next-themes": "^0.4.4", "next-themes": "^0.4.4",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-router-dom": "^6.28.0", "react-router": "^7.6.0",
"yjs": "^13.6.21" "yjs": "^13.6.21"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -1,6 +1,5 @@
import { getStoreManager } from '@affine/core/blocksuite/manager/store'; import { getStoreManager } from '@affine/core/blocksuite/manager/store';
import { AffineContext } from '@affine/core/components/context'; import { AffineContext } from '@affine/core/components/context';
import { AppFallback } from '@affine/core/mobile/components/app-fallback';
import { configureMobileModules } from '@affine/core/mobile/modules'; import { configureMobileModules } from '@affine/core/mobile/modules';
import { HapticProvider } from '@affine/core/mobile/modules/haptics'; import { HapticProvider } from '@affine/core/mobile/modules/haptics';
import { NavigationGestureProvider } from '@affine/core/mobile/modules/navigation-gesture'; import { NavigationGestureProvider } from '@affine/core/mobile/modules/navigation-gesture';
@@ -56,7 +55,7 @@ import { AsyncCall } from 'async-call-rpc';
import { AppTrackingTransparency } from 'capacitor-plugin-app-tracking-transparency'; import { AppTrackingTransparency } from 'capacitor-plugin-app-tracking-transparency';
import { useTheme } from 'next-themes'; import { useTheme } from 'next-themes';
import { Suspense, useEffect } from 'react'; import { Suspense, useEffect } from 'react';
import { RouterProvider } from 'react-router-dom'; import { RouterProvider } from 'react-router/dom';
import { BlocksuiteMenuConfigProvider } from './bs-menu-config'; import { BlocksuiteMenuConfigProvider } from './bs-menu-config';
import { ModalConfigProvider } from './modal-config'; import { ModalConfigProvider } from './modal-config';
@@ -72,10 +71,6 @@ window.addEventListener('beforeunload', () => {
storeManagerClient.dispose(); storeManagerClient.dispose();
}); });
const future = {
v7_startTransition: true,
} as const;
const framework = new Framework(); const framework = new Framework();
configureCommonModules(framework); configureCommonModules(framework);
configureBrowserWorkbenchModule(framework); configureBrowserWorkbenchModule(framework);
@@ -411,11 +406,7 @@ export function App() {
<KeyboardThemeProvider /> <KeyboardThemeProvider />
<ModalConfigProvider> <ModalConfigProvider>
<BlocksuiteMenuConfigProvider> <BlocksuiteMenuConfigProvider>
<RouterProvider <RouterProvider router={router} />
fallbackElement={<AppFallback />}
router={router}
future={future}
/>
</BlocksuiteMenuConfigProvider> </BlocksuiteMenuConfigProvider>
</ModalConfigProvider> </ModalConfigProvider>
</AffineContext> </AffineContext>

View File

@@ -20,7 +20,7 @@
"@toeverything/infra": "workspace:*", "@toeverything/infra": "workspace:*",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-router-dom": "^6.28.0" "react-router": "^7.6.0"
}, },
"devDependencies": { "devDependencies": {
"@types/react": "^19.0.1", "@types/react": "^19.0.1",

View File

@@ -1,5 +1,4 @@
import { AffineContext } from '@affine/core/components/context'; import { AffineContext } from '@affine/core/components/context';
import { AppFallback } from '@affine/core/mobile/components/app-fallback';
import { configureMobileModules } from '@affine/core/mobile/modules'; import { configureMobileModules } from '@affine/core/mobile/modules';
import { HapticProvider } from '@affine/core/mobile/modules/haptics'; import { HapticProvider } from '@affine/core/mobile/modules/haptics';
import { VirtualKeyboardProvider } from '@affine/core/mobile/modules/virtual-keyboard'; import { VirtualKeyboardProvider } from '@affine/core/mobile/modules/virtual-keyboard';
@@ -19,7 +18,7 @@ import { StoreManagerClient } from '@affine/nbstore/worker/client';
import { Framework, FrameworkRoot, getCurrentStore } from '@toeverything/infra'; import { Framework, FrameworkRoot, getCurrentStore } from '@toeverything/infra';
import { OpClient } from '@toeverything/infra/op'; import { OpClient } from '@toeverything/infra/op';
import { Suspense } from 'react'; import { Suspense } from 'react';
import { RouterProvider } from 'react-router-dom'; import { RouterProvider } from 'react-router/dom';
let storeManagerClient: StoreManagerClient; let storeManagerClient: StoreManagerClient;
@@ -35,10 +34,6 @@ window.addEventListener('beforeunload', () => {
storeManagerClient.dispose(); storeManagerClient.dispose();
}); });
const future = {
v7_startTransition: true,
} as const;
const framework = new Framework(); const framework = new Framework();
configureCommonModules(framework); configureCommonModules(framework);
configureBrowserWorkbenchModule(framework); configureBrowserWorkbenchModule(framework);
@@ -149,11 +144,7 @@ export function App() {
<FrameworkRoot framework={frameworkProvider}> <FrameworkRoot framework={frameworkProvider}>
<I18nProvider> <I18nProvider>
<AffineContext store={getCurrentStore()}> <AffineContext store={getCurrentStore()}>
<RouterProvider <RouterProvider router={router} />
fallbackElement={<AppFallback />}
router={router}
future={future}
/>
</AffineContext> </AffineContext>
</I18nProvider> </I18nProvider>
</FrameworkRoot> </FrameworkRoot>

View File

@@ -20,7 +20,7 @@
"@toeverything/infra": "workspace:*", "@toeverything/infra": "workspace:*",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-router-dom": "^6.28.0" "react-router": "^7.6.0"
}, },
"devDependencies": { "devDependencies": {
"@types/react": "^19.0.1", "@types/react": "^19.0.1",

View File

@@ -1,6 +1,5 @@
import { AffineContext } from '@affine/core/components/context'; import { AffineContext } from '@affine/core/components/context';
import { AppContainer } from '@affine/core/desktop/components/app-container'; import { Router } from '@affine/core/desktop/router';
import { router } from '@affine/core/desktop/router';
import { configureCommonModules } from '@affine/core/modules'; import { configureCommonModules } from '@affine/core/modules';
import { I18nProvider } from '@affine/core/modules/i18n'; import { I18nProvider } from '@affine/core/modules/i18n';
import { LifecycleService } from '@affine/core/modules/lifecycle'; import { LifecycleService } from '@affine/core/modules/lifecycle';
@@ -18,7 +17,7 @@ import { CacheProvider } from '@emotion/react';
import { Framework, FrameworkRoot, getCurrentStore } from '@toeverything/infra'; import { Framework, FrameworkRoot, getCurrentStore } from '@toeverything/infra';
import { OpClient } from '@toeverything/infra/op'; import { OpClient } from '@toeverything/infra/op';
import { Suspense } from 'react'; import { Suspense } from 'react';
import { RouterProvider } from 'react-router-dom'; import { BrowserRouter } from 'react-router';
const cache = createEmotionCache(); const cache = createEmotionCache();
@@ -42,10 +41,6 @@ window.addEventListener('beforeunload', () => {
storeManagerClient.dispose(); storeManagerClient.dispose();
}); });
const future = {
v7_startTransition: true,
} as const;
const framework = new Framework(); const framework = new Framework();
configureCommonModules(framework); configureCommonModules(framework);
configureBrowserWorkbenchModule(framework); configureBrowserWorkbenchModule(framework);
@@ -90,11 +85,9 @@ export function App() {
<CacheProvider value={cache}> <CacheProvider value={cache}>
<I18nProvider> <I18nProvider>
<AffineContext store={getCurrentStore()}> <AffineContext store={getCurrentStore()}>
<RouterProvider <BrowserRouter basename={environment.subPath}>
fallbackElement={<AppContainer fallback />} <Router />
router={router} </BrowserRouter>
future={future}
/>
</AffineContext> </AffineContext>
</I18nProvider> </I18nProvider>
</CacheProvider> </CacheProvider>

View File

@@ -61,7 +61,7 @@
"react": "19.1.0", "react": "19.1.0",
"react-dom": "19.1.0", "react-dom": "19.1.0",
"react-paginate": "^8.2.0", "react-paginate": "^8.2.0",
"react-router-dom": "^6.28.0", "react-router": "^7.6.0",
"react-transition-state": "^2.2.0", "react-transition-state": "^2.2.0",
"sonner": "^2.0.0", "sonner": "^2.0.0",
"swr": "^2.2.5", "swr": "^2.2.5",

View File

@@ -1,14 +1,16 @@
import { UserFriendlyError } from '@affine/error';
import { ArrowRightSmallIcon } from '@blocksuite/icons/rc'; import { ArrowRightSmallIcon } from '@blocksuite/icons/rc';
import clsx from 'clsx'; import clsx from 'clsx';
import { useMemo, useState } from 'react'; import { useCallback, useMemo, useState } from 'react';
import type { Location } from 'react-router-dom'; import type { Location } from 'react-router';
import { useLocation, useNavigate } from 'react-router-dom'; import { useLocation, useNavigate } from 'react-router';
import useSWR from 'swr'; import useSWR from 'swr';
import { Button } from '../../ui/button'; import { Button } from '../../ui/button';
import { Checkbox } from '../../ui/checkbox'; import { Checkbox } from '../../ui/checkbox';
import { Divider } from '../../ui/divider'; import { Divider } from '../../ui/divider';
import Input from '../../ui/input'; import Input from '../../ui/input';
import { notify } from '../../ui/notification';
import { ScrollableContainer } from '../../ui/scrollbar'; import { ScrollableContainer } from '../../ui/scrollbar';
import * as styles from './onboarding-page.css'; import * as styles from './onboarding-page.css';
import type { User } from './type'; import type { User } from './type';
@@ -118,6 +120,21 @@ export const OnboardingPage = ({
() => questions?.[questionIdx], () => questions?.[questionIdx],
[questionIdx, questions] [questionIdx, questions]
); );
const onClick = useCallback(() => {
if (callbackUrl) {
const result = navigate(callbackUrl);
if (result instanceof Promise) {
result.catch((err: Error) => {
const error = UserFriendlyError.fromAny(err);
console.error(error);
notify.error(error);
});
}
} else {
onOpenAffine();
}
}, [callbackUrl, navigate, onOpenAffine]);
const isMacosDesktop = BUILD_CONFIG.isElectron && environment.isMacOs; const isMacosDesktop = BUILD_CONFIG.isElectron && environment.isMacOs;
const isWindowsDesktop = BUILD_CONFIG.isElectron && environment.isWindows; const isWindowsDesktop = BUILD_CONFIG.isElectron && environment.isWindows;
@@ -263,13 +280,7 @@ export const OnboardingPage = ({
className={clsx(styles.button, styles.openAFFiNEButton)} className={clsx(styles.button, styles.openAFFiNEButton)}
variant="primary" variant="primary"
size="extraLarge" size="extraLarge"
onClick={() => { onClick={onClick}
if (callbackUrl) {
navigate(callbackUrl);
} else {
onOpenAffine();
}
}}
suffix={<ArrowRightSmallIcon />} suffix={<ArrowRightSmallIcon />}
> >
Get Started Get Started

View File

@@ -1,7 +1,7 @@
import { DialogTrigger } from '@radix-ui/react-dialog'; import { DialogTrigger } from '@radix-ui/react-dialog';
import { cssVar } from '@toeverything/theme'; import { cssVar } from '@toeverything/theme';
import { memo, useCallback } from 'react'; import { memo, useCallback } from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router';
import type { ButtonProps } from '../button'; import type { ButtonProps } from '../button';
import { Button } from '../button'; import { Button } from '../button';

View File

@@ -16,6 +16,7 @@
"@affine/graphql": "workspace:*", "@affine/graphql": "workspace:*",
"@affine/i18n": "workspace:*", "@affine/i18n": "workspace:*",
"@affine/nbstore": "workspace:*", "@affine/nbstore": "workspace:*",
"@affine/routes": "workspace:*",
"@affine/templates": "workspace:*", "@affine/templates": "workspace:*",
"@affine/track": "workspace:*", "@affine/track": "workspace:*",
"@blocksuite/affine": "workspace:*", "@blocksuite/affine": "workspace:*",
@@ -74,7 +75,7 @@
"react": "19.1.0", "react": "19.1.0",
"react-dom": "19.1.0", "react-dom": "19.1.0",
"react-error-boundary": "^6.0.0", "react-error-boundary": "^6.0.0",
"react-router-dom": "^6.28.0", "react-router": "^7.6.0",
"react-transition-state": "^2.2.0", "react-transition-state": "^2.2.0",
"react-virtuoso": "^4.12.3", "react-virtuoso": "^4.12.3",
"rxjs": "^7.8.1", "rxjs": "^7.8.1",

View File

@@ -4,7 +4,7 @@ import clsx from 'clsx';
import { Provider } from 'jotai/react'; import { Provider } from 'jotai/react';
import type { FC } from 'react'; import type { FC } from 'react';
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import { useRouteError } from 'react-router-dom'; import { useRouteError } from 'react-router';
import * as styles from './affine-error-fallback.css'; import * as styles from './affine-error-fallback.css';
import { ErrorDetail } from './error-basic/error-detail'; import { ErrorDetail } from './error-basic/error-detail';

View File

@@ -1,7 +1,7 @@
import { GlobalContextService } from '@affine/core/modules/global-context'; import { GlobalContextService } from '@affine/core/modules/global-context';
import { useLiveData, useServices } from '@toeverything/infra'; import { useLiveData, useServices } from '@toeverything/infra';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useLocation, useParams } from 'react-router-dom'; import { useLocation, useParams } from 'react-router';
export interface DumpInfoProps { export interface DumpInfoProps {
error: any; error: any;

View File

@@ -18,7 +18,7 @@ import {
useRef, useRef,
useState, useState,
} from 'react'; } from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router';
import * as styles from './styles.css'; import * as styles from './styles.css';

View File

@@ -8,7 +8,7 @@ import { useNavigateHelper } from '../use-navigate-helper';
export function useBlockSuiteMetaHelper() { export function useBlockSuiteMetaHelper() {
const workspace = useService(WorkspaceService).workspace; const workspace = useService(WorkspaceService).workspace;
const { openPage } = useNavigateHelper(); const { jumpToPage } = useNavigateHelper();
const docsService = useService(DocsService); const docsService = useService(DocsService);
const docRecordList = useService(DocsService).list; const docRecordList = useService(DocsService).list;
@@ -45,9 +45,9 @@ export function useBlockSuiteMetaHelper() {
async (pageId: string, openPageAfterDuplication: boolean = true) => { async (pageId: string, openPageAfterDuplication: boolean = true) => {
const newPageId = await docsService.duplicate(pageId); const newPageId = await docsService.duplicate(pageId);
openPageAfterDuplication && openPageAfterDuplication &&
openPage(workspace.docCollection.id, newPageId); jumpToPage(workspace.docCollection.id, newPageId);
}, },
[docsService, openPage, workspace.docCollection.id] [docsService, jumpToPage, workspace.docCollection.id]
); );
return { return {

View File

@@ -26,7 +26,7 @@ export const useSignOut = ({
}: ConfirmModalProps = {}) => { }: ConfirmModalProps = {}) => {
const t = useI18n(); const t = useI18n();
const { openConfirmModal } = useConfirmModal(); const { openConfirmModal } = useConfirmModal();
const { openPage } = useNavigateHelper(); const { jumpToAll } = useNavigateHelper();
const serverService = useService(ServerService); const serverService = useService(ServerService);
const authService = useService(AuthService); const authService = useService(AuthService);
@@ -56,14 +56,14 @@ export const useSignOut = ({
w => w.flavour !== serverService.server.id w => w.flavour !== serverService.server.id
); );
if (localWorkspace) { if (localWorkspace) {
openPage(localWorkspace.id, 'all'); jumpToAll(localWorkspace.id);
} }
} }
}, [ }, [
authService, authService,
currentWorkspaceFlavour, currentWorkspaceFlavour,
jumpToAll,
onConfirm, onConfirm,
openPage,
serverService.server.id, serverService.server.id,
workspaces, workspaces,
]); ]);

View File

@@ -1,10 +1,13 @@
import { notify } from '@affine/component';
import type { SettingTab } from '@affine/core/modules/dialogs/constant'; import type { SettingTab } from '@affine/core/modules/dialogs/constant';
import { toDocSearchParams } from '@affine/core/modules/navigation'; import { toDocSearchParams } from '@affine/core/modules/navigation';
import { getOpenUrlInDesktopAppLink } from '@affine/core/modules/open-in-app'; 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 type { DocMode } from '@blocksuite/affine/model';
import { nanoid } from 'nanoid'; import { nanoid } from 'nanoid';
import { createContext, useCallback, useContext, useMemo } from 'react'; import { createContext, useCallback, useContext, useMemo } from 'react';
import type { NavigateFunction, NavigateOptions } from 'react-router-dom'; import type { NavigateFunction, NavigateOptions, To } from 'react-router';
/** /**
* In workbench, we use nested react-router, so default `useNavigate` can't get correct navigate function in workbench. * In workbench, we use nested react-router, so default `useNavigate` can't get correct navigate function in workbench.
@@ -22,11 +25,27 @@ export enum RouteLogic {
* Use this for over workbench navigate, for navigate in workbench, use `WorkbenchService`. * Use this for over workbench navigate, for navigate in workbench, use `WorkbenchService`.
*/ */
export function useNavigateHelper() { export function useNavigateHelper() {
const navigate = useContext(NavigateContext); const navigateFunction = useContext(NavigateContext);
if (!navigate) { const navigate = useCallback(
throw new Error('useNavigateHelper must be used within a NavigateProvider'); (to: To, options?: NavigateOptions) => {
} if (!navigateFunction) {
throw new Error(
'useNavigateHelper must be used within a NavigateProvider'
);
}
const result = navigateFunction(to, options);
if (result instanceof Promise) {
result.catch((err: Error) => {
const error = UserFriendlyError.fromAny(err);
console.error(error);
notify.error(error);
});
}
return;
},
[navigateFunction]
);
const jumpToPage = useCallback( const jumpToPage = useCallback(
( (
@@ -34,7 +53,7 @@ export function useNavigateHelper() {
pageId: string, pageId: string,
logic: RouteLogic = RouteLogic.PUSH logic: RouteLogic = RouteLogic.PUSH
) => { ) => {
return navigate(`/workspace/${workspaceId}/${pageId}`, { return navigate(FACTORIES.workspace.doc({ workspaceId, docId: pageId }), {
replace: logic === RouteLogic.REPLACE, replace: logic === RouteLogic.REPLACE,
}); });
}, },
@@ -56,15 +75,18 @@ export function useNavigateHelper() {
refreshKey: nanoid(), refreshKey: nanoid(),
}); });
const query = search?.size ? `?${search.toString()}` : ''; const query = search?.size ? `?${search.toString()}` : '';
return navigate(`/workspace/${workspaceId}/${pageId}${query}`, { return navigate(
replace: logic === RouteLogic.REPLACE, FACTORIES.workspace.doc({ workspaceId, docId: pageId }) + query,
}); {
replace: logic === RouteLogic.REPLACE,
}
);
}, },
[navigate] [navigate]
); );
const jumpToCollections = useCallback( const jumpToCollections = useCallback(
(workspaceId: string, logic: RouteLogic = RouteLogic.PUSH) => { (workspaceId: string, logic: RouteLogic = RouteLogic.PUSH) => {
return navigate(`/workspace/${workspaceId}/collection`, { return navigate(FACTORIES.workspace.collections({ workspaceId }), {
replace: logic === RouteLogic.REPLACE, replace: logic === RouteLogic.REPLACE,
}); });
}, },
@@ -72,7 +94,7 @@ export function useNavigateHelper() {
); );
const jumpToTags = useCallback( const jumpToTags = useCallback(
(workspaceId: string, logic: RouteLogic = RouteLogic.PUSH) => { (workspaceId: string, logic: RouteLogic = RouteLogic.PUSH) => {
return navigate(`/workspace/${workspaceId}/tag`, { return navigate(FACTORIES.workspace.tags({ workspaceId }), {
replace: logic === RouteLogic.REPLACE, replace: logic === RouteLogic.REPLACE,
}); });
}, },
@@ -84,7 +106,7 @@ export function useNavigateHelper() {
tagId: string, tagId: string,
logic: RouteLogic = RouteLogic.PUSH logic: RouteLogic = RouteLogic.PUSH
) => { ) => {
return navigate(`/workspace/${workspaceId}/tag/${tagId}`, { return navigate(FACTORIES.workspace.tags.tag({ workspaceId, tagId }), {
replace: logic === RouteLogic.REPLACE, replace: logic === RouteLogic.REPLACE,
}); });
}, },
@@ -96,18 +118,26 @@ export function useNavigateHelper() {
collectionId: string, collectionId: string,
logic: RouteLogic = RouteLogic.PUSH logic: RouteLogic = RouteLogic.PUSH
) => { ) => {
return navigate(`/workspace/${workspaceId}/collection/${collectionId}`, { return navigate(
replace: logic === RouteLogic.REPLACE, FACTORIES.workspace.collections.collection({
}); workspaceId,
collectionId,
}),
{
replace: logic === RouteLogic.REPLACE,
}
);
}, },
[navigate] [navigate]
); );
const openPage = useCallback( const jumpToAll = useCallback(
(workspaceId: string, pageId: string, logic?: RouteLogic) => { (workspaceId: string, logic?: RouteLogic) => {
return jumpToPage(workspaceId, pageId, logic); return navigate(FACTORIES.workspace.all({ workspaceId }), {
replace: logic === RouteLogic.REPLACE,
});
}, },
[jumpToPage] [navigate]
); );
const jumpToIndex = useCallback( const jumpToIndex = useCallback(
@@ -124,7 +154,7 @@ export function useNavigateHelper() {
const jumpTo404 = useCallback( const jumpTo404 = useCallback(
(logic: RouteLogic = RouteLogic.PUSH) => { (logic: RouteLogic = RouteLogic.PUSH) => {
return navigate('/404', { return navigate(FACTORIES.notFound(), {
replace: logic === RouteLogic.REPLACE, replace: logic === RouteLogic.REPLACE,
}); });
}, },
@@ -132,7 +162,7 @@ export function useNavigateHelper() {
); );
const jumpToExpired = useCallback( const jumpToExpired = useCallback(
(logic: RouteLogic = RouteLogic.PUSH) => { (logic: RouteLogic = RouteLogic.PUSH) => {
return navigate('/expired', { return navigate(FACTORIES.expired(), {
replace: logic === RouteLogic.REPLACE, replace: logic === RouteLogic.REPLACE,
}); });
}, },
@@ -156,7 +186,7 @@ export function useNavigateHelper() {
} }
return navigate( return navigate(
'/sign-in' + FACTORIES.signIn() +
(searchParams.toString() ? '?' + searchParams.toString() : ''), (searchParams.toString() ? '?' + searchParams.toString() : ''),
{ {
replace: logic === RouteLogic.REPLACE, replace: logic === RouteLogic.REPLACE,
@@ -176,7 +206,7 @@ export function useNavigateHelper() {
} }
const encodedUrl = encodeURIComponent(deeplink); const encodedUrl = encodeURIComponent(deeplink);
return navigate(`/open-app/url?url=${encodedUrl}`); return navigate(FACTORIES.openApp({ action: `url?url=${encodedUrl}` }));
}, },
[navigate] [navigate]
); );
@@ -184,7 +214,8 @@ export function useNavigateHelper() {
const jumpToImportTemplate = useCallback( const jumpToImportTemplate = useCallback(
(name: string, snapshotUrl: string) => { (name: string, snapshotUrl: string) => {
return navigate( return navigate(
`/template/import?name=${encodeURIComponent(name)}&snapshotUrl=${encodeURIComponent(snapshotUrl)}` FACTORIES.template.import() +
`?name=${encodeURIComponent(name)}&snapshotUrl=${encodeURIComponent(snapshotUrl)}`
); );
}, },
[navigate] [navigate]
@@ -201,7 +232,8 @@ export function useNavigateHelper() {
searchParams.set('tab', tab); searchParams.set('tab', tab);
} }
return navigate( return navigate(
`/workspace/${workspaceId}/settings?${searchParams.toString()}`, FACTORIES.workspace.settings({ workspaceId }) +
(searchParams.toString() ? `?${searchParams.toString()}` : ''),
{ {
replace: logic === RouteLogic.REPLACE, replace: logic === RouteLogic.REPLACE,
} }
@@ -215,7 +247,7 @@ export function useNavigateHelper() {
jumpToPageBlock, jumpToPageBlock,
jumpToIndex, jumpToIndex,
jumpTo404, jumpTo404,
openPage, jumpToAll,
jumpToExpired, jumpToExpired,
jumpToSignIn, jumpToSignIn,
jumpToCollection, jumpToCollection,
@@ -231,7 +263,7 @@ export function useNavigateHelper() {
jumpToPageBlock, jumpToPageBlock,
jumpToIndex, jumpToIndex,
jumpTo404, jumpTo404,
openPage, jumpToAll,
jumpToExpired, jumpToExpired,
jumpToSignIn, jumpToSignIn,
jumpToCollection, jumpToCollection,

View File

@@ -25,7 +25,7 @@ import {
import { useLiveData, useService, useServices } from '@toeverything/infra'; import { useLiveData, useService, useServices } from '@toeverything/infra';
import clsx from 'clsx'; import clsx from 'clsx';
import { useCallback, useMemo, useState } from 'react'; import { useCallback, useMemo, useState } from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router';
import { usePageHelper } from '../../../blocksuite/block-suite-page-list/utils'; import { usePageHelper } from '../../../blocksuite/block-suite-page-list/utils';
import { import {

View File

@@ -1,7 +1,7 @@
import type { CollectionMeta } from '@affine/core/modules/collection'; import type { CollectionMeta } from '@affine/core/modules/collection';
import type { DocMeta, Workspace } from '@blocksuite/affine/store'; import type { DocMeta, Workspace } from '@blocksuite/affine/store';
import type { JSX, PropsWithChildren, ReactNode } from 'react'; import type { JSX, PropsWithChildren, ReactNode } from 'react';
import type { To } from 'react-router-dom'; import type { To } from 'react-router';
export type ListItem = export type ListItem =
| DocMeta | DocMeta

View File

@@ -160,7 +160,7 @@ const CloudWorkSpaceList = ({
if (currentWorkspaceFlavour === server.id) { if (currentWorkspaceFlavour === server.id) {
const otherWorkspace = workspaces.find(w => w.flavour !== server.id); const otherWorkspace = workspaces.find(w => w.flavour !== server.id);
if (otherWorkspace) { if (otherWorkspace) {
navigateHelper.openPage(otherWorkspace.id, 'all'); navigateHelper.jumpToAll(otherWorkspace.id);
} }
} }
}, [ }, [

View File

@@ -49,7 +49,7 @@ const Dialog = ({
? workspacesService.open({ metadata: workspaceMeta }) ? workspacesService.open({ metadata: workspaceMeta })
: { workspace: undefined }; : { workspace: undefined };
const { jumpToPage } = useNavigateHelper(); const { jumpToPage, jumpToAll } = useNavigateHelper();
const enableCloud = useCallback(async () => { const enableCloud = useCallback(async () => {
try { try {
@@ -61,7 +61,11 @@ const Dialog = ({
account.id, account.id,
selectedServer.id selectedServer.id
); );
jumpToPage(newId, openPageId || 'all'); if (openPageId) {
jumpToPage(newId, openPageId);
} else {
jumpToAll(newId);
}
close?.(); close?.();
} catch (e) { } catch (e) {
console.error(e); console.error(e);
@@ -74,9 +78,10 @@ const Dialog = ({
account, account,
workspacesService, workspacesService,
selectedServer.id, selectedServer.id,
jumpToPage,
openPageId, openPageId,
close, close,
jumpToPage,
jumpToAll,
t, t,
]); ]);

View File

@@ -57,7 +57,7 @@ const Dialog = ({
workspaces.find(w => w.flavour !== 'local') ?? workspaces.find(w => w.flavour !== 'local') ??
workspaces.at(0); workspaces.at(0);
const selectedWorkspaceName = useWorkspaceName(selectedWorkspace); const selectedWorkspaceName = useWorkspaceName(selectedWorkspace);
const { openPage, jumpToSignIn } = useNavigateHelper(); const { jumpToPage, jumpToSignIn } = useNavigateHelper();
const noWorkspace = workspaces.length === 0; const noWorkspace = workspaces.length === 0;
@@ -119,7 +119,7 @@ const Dialog = ({
templateDownloader.data$.value, templateDownloader.data$.value,
templateMode templateMode
); );
openPage(selectedWorkspace.id, docId); jumpToPage(selectedWorkspace.id, docId);
onClose?.(); onClose?.();
} catch (err) { } catch (err) {
setImportingError(err); setImportingError(err);
@@ -129,8 +129,8 @@ const Dialog = ({
} }
}, [ }, [
importTemplateService, importTemplateService,
jumpToPage,
onClose, onClose,
openPage,
selectedWorkspace, selectedWorkspace,
templateDownloader.data$.value, templateDownloader.data$.value,
templateMode, templateMode,
@@ -149,7 +149,7 @@ const Dialog = ({
'Workspace', 'Workspace',
templateDownloader.data$.value templateDownloader.data$.value
); );
openPage(workspaceId, docId); jumpToPage(workspaceId, docId);
onClose?.(); onClose?.();
} catch (err) { } catch (err) {
setImportingError(err); setImportingError(err);
@@ -158,8 +158,8 @@ const Dialog = ({
} }
}, [ }, [
importTemplateService, importTemplateService,
jumpToPage,
onClose, onClose,
openPage,
templateDownloader.data$.value, templateDownloader.data$.value,
]); ]);

View File

@@ -3,7 +3,7 @@ import { AuthPageContainer } from '@affine/component/auth-components';
import { useNavigateHelper } from '@affine/core/components/hooks/use-navigate-helper'; import { useNavigateHelper } from '@affine/core/components/hooks/use-navigate-helper';
import { Trans, useI18n } from '@affine/i18n'; import { Trans, useI18n } from '@affine/i18n';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { useSearchParams } from 'react-router-dom'; import { useSearchParams } from 'react-router';
import * as styles from './styles.css'; import * as styles from './styles.css';

View File

@@ -14,9 +14,7 @@ import {
import { useI18n } from '@affine/i18n'; import { useI18n } from '@affine/i18n';
import { useLiveData, useService } from '@toeverything/infra'; import { useLiveData, useService } from '@toeverything/infra';
import { useCallback } from 'react'; import { useCallback } from 'react';
import type { LoaderFunction } from 'react-router-dom'; import { useParams, useSearchParams } from 'react-router';
import { redirect, useParams, useSearchParams } from 'react-router-dom';
import { z } from 'zod';
import { useMutation } from '../../../components/hooks/use-mutation'; import { useMutation } from '../../../components/hooks/use-mutation';
import { import {
@@ -28,18 +26,6 @@ import { AppContainer } from '../../components/app-container';
import { ConfirmChangeEmail } from './confirm-change-email'; import { ConfirmChangeEmail } from './confirm-change-email';
import { ConfirmVerifiedEmail } from './email-verified-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 = () => { export const Component = () => {
const authService = useService(AuthService); const authService = useService(AuthService);
const account = useLiveData(authService.session.account$); const account = useLiveData(authService.session.account$);
@@ -159,14 +145,3 @@ export const Component = () => {
} }
return null; 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;
};

View File

@@ -7,7 +7,7 @@ import { changeEmailMutation } from '@affine/graphql';
import { useI18n } from '@affine/i18n'; import { useI18n } from '@affine/i18n';
import { useService } from '@toeverything/infra'; import { useService } from '@toeverything/infra';
import { type FC, useEffect, useState } from 'react'; import { type FC, useEffect, useState } from 'react';
import { useSearchParams } from 'react-router-dom'; import { useSearchParams } from 'react-router';
import { AppContainer } from '../../components/app-container'; import { AppContainer } from '../../components/app-container';

View File

@@ -7,7 +7,7 @@ import { verifyEmailMutation } from '@affine/graphql';
import { useI18n } from '@affine/i18n'; import { useI18n } from '@affine/i18n';
import { useService } from '@toeverything/infra'; import { useService } from '@toeverything/infra';
import { type FC, useEffect, useState } from 'react'; import { type FC, useEffect, useState } from 'react';
import { useSearchParams } from 'react-router-dom'; import { useSearchParams } from 'react-router';
import { AppContainer } from '../../components/app-container'; import { AppContainer } from '../../components/app-container';

View File

@@ -1,64 +1,22 @@
import { useAsyncNavigate } from '@affine/core/utils/use-async-navigate';
import { useService } from '@toeverything/infra'; import { useService } from '@toeverything/infra';
import { useEffect, useRef } from 'react'; import { useEffect, useRef } from 'react';
import { import { useLoaderData } from 'react-router';
type LoaderFunction,
redirect,
useLoaderData,
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
useNavigate,
} from 'react-router-dom';
import { AuthService } from '../../../modules/cloud'; import { AuthService } from '../../../modules/cloud';
import { supportedClient } from './common'; export interface MagicLinkLoaderData {
interface LoaderData {
token: string; token: string;
email: string; email: string;
redirectUri: string | null; 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 = () => { export const Component = () => {
// TODO(@eyhn): loading ui // TODO(@eyhn): loading ui
const auth = useService(AuthService); const auth = useService(AuthService);
const data = useLoaderData() as LoaderData; const data = useLoaderData() as MagicLinkLoaderData;
const nav = useAsyncNavigate();
const nav = useNavigate();
// loader data from useLoaderData is not reactive, so that we can safely // loader data from useLoaderData is not reactive, so that we can safely
// assume the effect below is only triggered once // assume the effect below is only triggered once
const triggeredRef = useRef(false); const triggeredRef = useRef(false);

View File

@@ -1,72 +1,25 @@
import { useAsyncNavigate } from '@affine/core/utils/use-async-navigate';
import { useService } from '@toeverything/infra'; import { useService } from '@toeverything/infra';
import { useEffect, useRef } from 'react'; import { useEffect, useRef } from 'react';
import { import { useLoaderData } from 'react-router';
type LoaderFunction,
redirect,
useLoaderData,
useNavigate,
} from 'react-router-dom';
import { AuthService } from '../../../modules/cloud'; import { AuthService } from '../../../modules/cloud';
import { supportedClient } from './common';
interface LoaderData { export interface OAuthCallbackLoaderData {
state: string; state: string;
code: string; code: string;
provider: 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 = () => { export const Component = () => {
const auth = useService(AuthService); 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 // loader data from useLoaderData is not reactive, so that we can safely
// assume the effect below is only triggered once // assume the effect below is only triggered once
const triggeredRef = useRef(false); const triggeredRef = useRef(false);
const nav = useNavigate(); const nav = useAsyncNavigate();
useEffect(() => { useEffect(() => {
if (triggeredRef.current) { if (triggeredRef.current) {

View File

@@ -1,68 +1,21 @@
import { AuthService } from '@affine/core/modules/cloud'; import { AuthService } from '@affine/core/modules/cloud';
import { OAuthProviderType } from '@affine/graphql'; import { useAsyncNavigate } from '@affine/core/utils';
import type { OAuthProviderType } from '@affine/graphql';
import { useService } from '@toeverything/infra'; import { useService } from '@toeverything/infra';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { import { useLoaderData } from 'react-router';
type LoaderFunction,
redirect,
useLoaderData,
// oxlint-disable-next-line @typescript-eslint/no-restricted-imports
useNavigate,
} from 'react-router-dom';
import { z } from 'zod';
import { supportedClient } from './common'; interface OAuthLoginLoaderData {
const supportedProvider = z.nativeEnum(OAuthProviderType);
const oauthParameters = z.object({
provider: supportedProvider,
client: supportedClient,
redirectUri: z.string().optional().nullable(),
});
interface LoaderData {
provider: OAuthProviderType; provider: OAuthProviderType;
client: string; client: string;
redirectUri?: 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 = () => { export const Component = () => {
const auth = useService(AuthService); const auth = useService(AuthService);
const data = useLoaderData() as LoaderData; const data = useLoaderData() as OAuthLoginLoaderData;
const nav = useNavigate(); const nav = useAsyncNavigate();
useEffect(() => { useEffect(() => {
auth auth

View File

@@ -4,9 +4,10 @@ import { SignInPageContainer } from '@affine/component/auth-components';
import { SignInPanel } from '@affine/core/components/sign-in'; import { SignInPanel } from '@affine/core/components/sign-in';
import { SignInBackgroundArts } from '@affine/core/components/sign-in/background-arts'; import { SignInBackgroundArts } from '@affine/core/components/sign-in/background-arts';
import type { AuthSessionStatus } from '@affine/core/modules/cloud/entities/session'; import type { AuthSessionStatus } from '@affine/core/modules/cloud/entities/session';
import { useAsyncNavigate } from '@affine/core/utils';
import { useI18n } from '@affine/i18n'; import { useI18n } from '@affine/i18n';
import { useCallback, useEffect } from 'react'; import { useCallback, useEffect } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom'; import { useSearchParams } from 'react-router';
import { import {
RouteLogic, RouteLogic,
@@ -19,7 +20,7 @@ export const SignIn = ({
redirectUrl?: string; redirectUrl?: string;
}) => { }) => {
const t = useI18n(); const t = useI18n();
const navigate = useNavigate(); const navigate = useAsyncNavigate();
const { jumpToIndex } = useNavigateHelper(); const { jumpToIndex } = useNavigateHelper();
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const redirectUrl = redirectUrlFromProps ?? searchParams.get('redirect_uri'); const redirectUrl = redirectUrlFromProps ?? searchParams.get('redirect_uri');

View File

@@ -2,7 +2,7 @@ import { GlobalDialogService } from '@affine/core/modules/dialogs';
import type { DocMode } from '@blocksuite/affine/model'; import type { DocMode } from '@blocksuite/affine/model';
import { useService } from '@toeverything/infra'; import { useService } from '@toeverything/infra';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useSearchParams } from 'react-router-dom'; import { useSearchParams } from 'react-router';
import { useNavigateHelper } from '../../../components/hooks/use-navigate-helper'; import { useNavigateHelper } from '../../../components/hooks/use-navigate-helper';

View File

@@ -17,12 +17,9 @@ import {
useRef, useRef,
useState, useState,
} from 'react'; } from 'react';
import { useSearchParams } from 'react-router-dom'; import { useSearchParams } from 'react-router';
import { import { useNavigateHelper } from '../../../components/hooks/use-navigate-helper';
RouteLogic,
useNavigateHelper,
} from '../../../components/hooks/use-navigate-helper';
import { WorkspaceNavigator } from '../../../components/workspace-selector'; import { WorkspaceNavigator } from '../../../components/workspace-selector';
import { AuthService } from '../../../modules/cloud'; import { AuthService } from '../../../modules/cloud';
import { AppContainer } from '../../components/app-container'; import { AppContainer } from '../../components/app-container';
@@ -55,7 +52,7 @@ export const Component = ({
const list = useLiveData(workspacesService.list.workspaces$); const list = useLiveData(workspacesService.list.workspaces$);
const listIsLoading = useLiveData(workspacesService.list.isRevalidating$); const listIsLoading = useLiveData(workspacesService.list.isRevalidating$);
const { openPage, jumpToPage } = useNavigateHelper(); const { jumpToAll, jumpToPage } = useNavigateHelper();
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const createOnceRef = useRef(false); const createOnceRef = useRef(false);
@@ -68,12 +65,14 @@ export const Component = ({
.then(({ meta, defaultDocId }) => { .then(({ meta, defaultDocId }) => {
if (defaultDocId) { if (defaultDocId) {
jumpToPage(meta.id, defaultDocId); jumpToPage(meta.id, defaultDocId);
} else if (defaultIndexRoute === 'all') {
jumpToAll(meta.id);
} else { } else {
openPage(meta.id, defaultIndexRoute); jumpToPage(meta.id, defaultIndexRoute);
} }
}) })
.catch(err => console.error('Failed to create cloud workspace', err)); .catch(err => console.error('Failed to create cloud workspace', err));
}, [defaultIndexRoute, jumpToPage, openPage, workspacesService]); }, [defaultIndexRoute, jumpToPage, jumpToAll, workspacesService]);
useLayoutEffect(() => { useLayoutEffect(() => {
if (!navigating) { if (!navigating) {
@@ -95,7 +94,11 @@ export const Component = ({
// open first cloud workspace // open first cloud workspace
const openWorkspace = const openWorkspace =
list.find(w => w.flavour === 'affine-cloud') ?? list[0]; list.find(w => w.flavour === 'affine-cloud') ?? list[0];
openPage(openWorkspace.id, defaultIndexRoute); if (defaultIndexRoute === 'all') {
jumpToAll(openWorkspace.id);
} else {
jumpToPage(openWorkspace.id, defaultIndexRoute);
}
} else { } else {
return; return;
} }
@@ -108,17 +111,22 @@ export const Component = ({
const lastId = localStorage.getItem('last_workspace_id'); const lastId = localStorage.getItem('last_workspace_id');
const openWorkspace = list.find(w => w.id === lastId) ?? list[0]; const openWorkspace = list.find(w => w.id === lastId) ?? list[0];
openPage(openWorkspace.id, defaultIndexRoute, RouteLogic.REPLACE); if (defaultIndexRoute === 'all') {
jumpToAll(openWorkspace.id);
} else {
jumpToPage(openWorkspace.id, defaultIndexRoute);
}
} }
}, [ }, [
createCloudWorkspace, createCloudWorkspace,
list, list,
openPage,
searchParams, searchParams,
listIsLoading, listIsLoading,
loggedIn, loggedIn,
navigating, navigating,
defaultIndexRoute, defaultIndexRoute,
jumpToAll,
jumpToPage,
]); ]);
const desktopApi = useServiceOptional(DesktopApiService); const desktopApi = useServiceOptional(DesktopApiService);
@@ -138,7 +146,7 @@ export const Component = ({
createdWorkspace.defaultPageId createdWorkspace.defaultPageId
); );
} else { } else {
openPage(createdWorkspace.meta.id, 'all'); jumpToAll(createdWorkspace.meta.id);
} }
} }
}) })
@@ -148,7 +156,7 @@ export const Component = ({
.finally(() => { .finally(() => {
setCreating(false); setCreating(false);
}); });
}, [jumpToPage, openPage, workspacesService]); }, [jumpToAll, jumpToPage, workspacesService]);
if (navigating || creating) { if (navigating || creating) {
return fallback ?? <AppContainer fallback />; return fallback ?? <AppContainer fallback />;

View File

@@ -12,7 +12,7 @@ import { UserFriendlyError } from '@affine/error';
import { WorkspaceMemberStatus } from '@affine/graphql'; import { WorkspaceMemberStatus } from '@affine/graphql';
import { useLiveData, useService } from '@toeverything/infra'; import { useLiveData, useService } from '@toeverything/infra';
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { Navigate, useParams } from 'react-router-dom'; import { Navigate, useParams } from 'react-router';
import { import {
RouteLogic, RouteLogic,

View File

@@ -1,24 +1,8 @@
import { DesktopApiService } from '@affine/core/modules/desktop-api'; import { DesktopApiService } from '@affine/core/modules/desktop-api';
import { useServiceOptional } from '@toeverything/infra'; import { useServiceOptional } from '@toeverything/infra';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { redirect } from 'react-router-dom';
import { Onboarding } from '../../../components/affine/onboarding/onboarding'; 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 = () => { export const Component = () => {
const desktopApi = useServiceOptional(DesktopApiService); const desktopApi = useServiceOptional(DesktopApiService);

View File

@@ -10,7 +10,7 @@ import type { GetCurrentUserQuery } from '@affine/graphql';
import { getCurrentUserQuery } from '@affine/graphql'; import { getCurrentUserQuery } from '@affine/graphql';
import { useService } from '@toeverything/infra'; import { useService } from '@toeverything/infra';
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { useParams, useSearchParams } from 'react-router-dom'; import { useParams, useSearchParams } from 'react-router';
import { AppContainer } from '../../components/app-container'; import { AppContainer } from '../../components/app-container';

View File

@@ -1,53 +1,4 @@
import { DebugLogger } from '@affine/debug'; import { Navigate, useLoaderData } from 'react-router';
import { type LoaderFunction, Navigate, useLoaderData } from 'react-router-dom';
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 };
};
export const Component = () => { export const Component = () => {
const { allow } = useLoaderData() as { allow: boolean }; const { allow } = useLoaderData() as { allow: boolean };

View File

@@ -2,7 +2,7 @@ import { NotificationCenter } from '@affine/component';
import { DefaultServerService } from '@affine/core/modules/cloud'; import { DefaultServerService } from '@affine/core/modules/cloud';
import { FrameworkScope, useService } from '@toeverything/infra'; import { FrameworkScope, useService } from '@toeverything/infra';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { Outlet } from 'react-router-dom'; import { Outlet } from 'react-router';
import { GlobalDialogs } from '../../dialogs'; import { GlobalDialogs } from '../../dialogs';
import { CustomThemeModifier } from './custom-theme'; import { CustomThemeModifier } from './custom-theme';

View File

@@ -10,7 +10,7 @@ import { track } from '@affine/track';
import { effect, fromPromise, useServices } from '@toeverything/infra'; import { effect, fromPromise, useServices } from '@toeverything/infra';
import { nanoid } from 'nanoid'; import { nanoid } from 'nanoid';
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { useSearchParams } from 'react-router-dom'; import { useSearchParams } from 'react-router';
import { switchMap } from 'rxjs'; import { switchMap } from 'rxjs';
import { generateSubscriptionCallbackLink } from '../../../components/hooks/affine/use-subscription-notify'; import { generateSubscriptionCallbackLink } from '../../../components/hooks/affine/use-subscription-notify';

View File

@@ -3,7 +3,7 @@ import { AuthPageContainer } from '@affine/component/auth-components';
import { useNavigateHelper } from '@affine/core/components/hooks/use-navigate-helper'; import { useNavigateHelper } from '@affine/core/components/hooks/use-navigate-helper';
import { Trans, useI18n } from '@affine/i18n'; import { Trans, useI18n } from '@affine/i18n';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { useSearchParams } from 'react-router-dom'; import { useSearchParams } from 'react-router';
import * as styles from './styles.css'; import * as styles from './styles.css';

View File

@@ -7,7 +7,7 @@ import { Trans, useI18n } from '@affine/i18n';
import { CopyIcon } from '@blocksuite/icons/rc'; import { CopyIcon } from '@blocksuite/icons/rc';
import { useLiveData, useService } from '@toeverything/infra'; import { useLiveData, useService } from '@toeverything/infra';
import { useCallback, useEffect } from 'react'; import { useCallback, useEffect } from 'react';
import { useSearchParams } from 'react-router-dom'; import { useSearchParams } from 'react-router';
import { PageNotFound } from '../../404'; import { PageNotFound } from '../../404';
import * as styles from './styles.css'; import * as styles from './styles.css';

View File

@@ -29,7 +29,7 @@ import { type I18nString, Trans, useI18n } from '@affine/i18n';
import { DoneIcon, NewPageIcon, SignOutIcon } from '@blocksuite/icons/rc'; import { DoneIcon, NewPageIcon, SignOutIcon } from '@blocksuite/icons/rc';
import { useLiveData, useService } from '@toeverything/infra'; import { useLiveData, useService } from '@toeverything/infra';
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
import { useSearchParams } from 'react-router-dom'; import { useSearchParams } from 'react-router';
import { Upgrade } from '../../dialogs/setting/general-setting/plans/plan-card'; import { Upgrade } from '../../dialogs/setting/general-setting/plans/plan-card';
import { PageNotFound } from '../404'; import { PageNotFound } from '../404';

View File

@@ -4,7 +4,7 @@ import { type Doc, DocsService } from '@affine/core/modules/doc';
import { type AttachmentBlockModel } from '@blocksuite/affine/model'; import { type AttachmentBlockModel } from '@blocksuite/affine/model';
import { FrameworkScope, useLiveData, useService } from '@toeverything/infra'; import { FrameworkScope, useLiveData, useService } from '@toeverything/infra';
import { type ReactElement, useLayoutEffect, useState } from 'react'; import { type ReactElement, useLayoutEffect, useState } from 'react';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router';
import { ViewIcon, ViewTitle } from '../../../../modules/workbench'; import { ViewIcon, ViewTitle } from '../../../../modules/workbench';
import { PageNotFound } from '../../404'; import { PageNotFound } from '../../404';

View File

@@ -18,7 +18,7 @@ import { useI18n } from '@affine/i18n';
import { ViewLayersIcon } from '@blocksuite/icons/rc'; import { ViewLayersIcon } from '@blocksuite/icons/rc';
import { useLiveData, useService, useServices } from '@toeverything/infra'; import { useLiveData, useService, useServices } from '@toeverything/infra';
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router';
import { useNavigateHelper } from '../../../../components/hooks/use-navigate-helper'; import { useNavigateHelper } from '../../../../components/hooks/use-navigate-helper';
import { import {

View File

@@ -50,7 +50,7 @@ import {
import clsx from 'clsx'; import clsx from 'clsx';
import { nanoid } from 'nanoid'; import { nanoid } from 'nanoid';
import { memo, useCallback, useEffect, useRef, useState } from 'react'; import { memo, useCallback, useEffect, useRef, useState } from 'react';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router';
import type { Subscription } from 'rxjs'; import type { Subscription } from 'rxjs';
import { PageNotFound } from '../../404'; import { PageNotFound } from '../../404';

View File

@@ -30,7 +30,7 @@ import {
useLocation, useLocation,
useParams, useParams,
useSearchParams, useSearchParams,
} from 'react-router-dom'; } from 'react-router';
import { map } from 'rxjs'; import { map } from 'rxjs';
import * as _Y from 'yjs'; import * as _Y from 'yjs';

View File

@@ -3,7 +3,7 @@ import type { SettingTab } from '@affine/core/modules/dialogs/constant';
import { WorkbenchService } from '@affine/core/modules/workbench'; import { WorkbenchService } from '@affine/core/modules/workbench';
import { useService } from '@toeverything/infra'; import { useService } from '@toeverything/infra';
import { useEffect, useRef } from 'react'; import { useEffect, useRef } from 'react';
import { useSearchParams } from 'react-router-dom'; import { useSearchParams } from 'react-router';
export const Component = () => { export const Component = () => {
const workbenchService = useService(WorkbenchService); const workbenchService = useService(WorkbenchService);

View File

@@ -27,7 +27,7 @@ import { Logo1Icon } from '@blocksuite/icons/rc';
import { FrameworkScope, useLiveData, useService } from '@toeverything/infra'; import { FrameworkScope, useLiveData, useService } from '@toeverything/infra';
import clsx from 'clsx'; import clsx from 'clsx';
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router';
import { PageNotFound } from '../../404'; import { PageNotFound } from '../../404';
import { ShareFooter } from './share-footer'; import { ShareFooter } from './share-footer';
@@ -186,7 +186,7 @@ const SharePageInner = ({
const t = useI18n(); const t = useI18n();
const pageTitle = useLiveData(page?.title$); const pageTitle = useLiveData(page?.title$);
const { jumpToPageBlock, openPage } = useNavigateHelper(); const { jumpToPageBlock, jumpToPage } = useNavigateHelper();
const onEditorLoad = useCallback( const onEditorLoad = useCallback(
(editorContainer: AffineEditorContainer) => { (editorContainer: AffineEditorContainer) => {
@@ -212,7 +212,7 @@ const SharePageInner = ({
return; return;
} }
return openPage(workspaceId, pageId); return jumpToPage(workspaceId, pageId);
}) })
); );
} }
@@ -221,7 +221,13 @@ const SharePageInner = ({
unbind(); unbind();
}; };
}, },
[editor, setActiveBlocksuiteEditor, jumpToPageBlock, openPage, workspaceId] [
setActiveBlocksuiteEditor,
editor,
jumpToPage,
workspaceId,
jumpToPageBlock,
]
); );
if (noPermission) { if (noPermission) {

View File

@@ -17,7 +17,7 @@ import {
} from '@affine/core/modules/workbench'; } from '@affine/core/modules/workbench';
import { useLiveData, useService } from '@toeverything/infra'; import { useLiveData, useService } from '@toeverything/infra';
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router';
import { PageNotFound } from '../../404'; import { PageNotFound } from '../../404';
import { AllDocSidebarTabs } from '../layouts/all-doc-sidebar-tabs'; import { AllDocSidebarTabs } from '../layouts/all-doc-sidebar-tabs';

View File

@@ -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`)}`
);
}
};

View File

@@ -1,16 +1,26 @@
import { wrapCreateBrowserRouterV6 } from '@sentry/react'; import { FACTORIES, lazy, RELATIVE_ROUTES } from '@affine/routes';
import { withSentryReactRouterV7Routing } from '@sentry/react';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import type { RouteObject } from 'react-router-dom'; import type { Params } from 'react-router';
import { import {
createBrowserRouter as reactRouterCreateBrowserRouter,
redirect, redirect,
Route,
Routes as ReactRouterRoutes,
useNavigate, useNavigate,
} from 'react-router-dom'; } from 'react-router';
import { AffineErrorComponent } from '../components/affine/affine-error-boundary/affine-error-fallback'; import { AffineErrorComponent } from '../components/affine/affine-error-boundary/affine-error-fallback';
import { NavigateContext } from '../components/hooks/use-navigate-helper'; import { NavigateContext } from '../components/hooks/use-navigate-helper';
import { AppContainer } from './components/app-container';
import { RootWrapper } from './pages/root'; import { RootWrapper } from './pages/root';
import {
authLoader,
magicLinkLoader,
oauthCallbackLoader,
oauthLoginLoader,
onboardingLoader,
redirectLoader,
} from './router-loader';
export function RootRouter() { export function RootRouter() {
const navigate = useNavigate(); const navigate = useNavigate();
const [ready, setReady] = useState(false); const [ready, setReady] = useState(false);
@@ -27,169 +37,194 @@ export function RootRouter() {
) )
); );
} }
export const Index = lazy(async () => await import('./pages/index'));
export const topLevelRoutes = [ export const Workspace = lazy(
{ async () => await import('./pages/workspace/index')
element: <RootRouter />,
errorElement: <AffineErrorComponent />,
children: [
{
path: '/',
lazy: () => import('./pages/index'),
},
{
path: '/workspace/:workspaceId/*',
lazy: () => import('./pages/workspace/index'),
},
{
path: '/share/:workspaceId/:pageId',
loader: ({ params }) => {
return redirect(`/workspace/${params.workspaceId}/${params.pageId}`);
},
},
{
path: '/404',
lazy: () => import('./pages/404'),
},
{
path: '/expired',
lazy: () => import('./pages/expired'),
},
{
path: '/invite/:inviteId',
lazy: () => import('./pages/invite'),
},
{
path: '/upgrade-success',
lazy: () => import('./pages/upgrade-success'),
},
{
path: '/upgrade-success/team',
lazy: () => import('./pages/upgrade-success/team'),
},
{
path: '/upgrade-success/self-hosted-team',
lazy: () => import('./pages/upgrade-success/self-host-team'),
},
{
path: '/ai-upgrade-success',
lazy: () => import('./pages/ai-upgrade-success'),
},
{
path: '/onboarding',
lazy: () => import('./pages/onboarding'),
},
{
path: '/redirect-proxy',
lazy: () => import('./pages/redirect'),
},
{
path: '/subscribe',
lazy: () => import('./pages/subscribe'),
},
{
path: '/upgrade-to-team',
lazy: () => import('./pages/upgrade-to-team'),
},
{
path: '/try-cloud',
loader: () => {
return redirect(
`/sign-in?redirect_uri=${encodeURIComponent('/?initCloud=true')}`
);
},
},
{
path: '/theme-editor',
lazy: () => import('./pages/theme-editor'),
},
{
path: '/clipper/import',
lazy: () => import('./pages/import-clipper'),
},
{
path: '/template/import',
lazy: () => 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: () => import(/* webpackChunkName: "auth" */ './pages/auth/auth'),
},
{
path: '/sign-In',
lazy: () =>
import(/* webpackChunkName: "auth" */ './pages/auth/sign-in'),
},
{
path: '/magic-link',
lazy: () =>
import(/* webpackChunkName: "auth" */ './pages/auth/magic-link'),
},
{
path: '/oauth/login',
lazy: () =>
import(/* webpackChunkName: "auth" */ './pages/auth/oauth-login'),
},
{
path: '/oauth/callback',
lazy: () =>
import(/* webpackChunkName: "auth" */ './pages/auth/oauth-callback'),
},
// deprecated, keep for old client compatibility
// TODO(@forehalo): remove
{
path: '/desktop-signin',
lazy: () =>
import(/* webpackChunkName: "auth" */ './pages/auth/oauth-login'),
},
// deprecated, keep for old client compatibility
// use '/sign-in'
// TODO(@forehalo): remove
{
path: '/signIn',
lazy: () =>
import(/* webpackChunkName: "auth" */ './pages/auth/sign-in'),
},
{
path: '/open-app/:action',
lazy: () => import('./pages/open-app'),
},
{
path: '*',
lazy: () => import('./pages/404'),
},
],
},
] satisfies [RouteObject, ...RouteObject[]];
const createBrowserRouter = wrapCreateBrowserRouterV6(
reactRouterCreateBrowserRouter
); );
export const router = ( export const NotFound = lazy(async () => await import('./pages/404'));
window.SENTRY_RELEASE ? createBrowserRouter : reactRouterCreateBrowserRouter export const Expired = lazy(async () => await import('./pages/expired'));
)(topLevelRoutes, { export const Invite = lazy(async () => await import('./pages/invite'));
basename: environment.subPath, export const UpgradeSuccess = lazy(
future: { async () => await import('./pages/upgrade-success')
v7_normalizeFormMethod: true, );
}, 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>;

View File

@@ -1,52 +1,52 @@
import type { RouteObject } from 'react-router-dom'; import type { RouteObject } from 'react-router';
export const workbenchRoutes = [ export const workbenchRoutes = [
{ {
path: '/all', path: '/all',
lazy: () => import('./pages/workspace/all-page/all-page'), lazy: async () => await import('./pages/workspace/all-page/all-page'),
}, },
{ {
path: '/all-old', path: '/all-old',
lazy: () => import('./pages/workspace/all-page-old/all-page'), lazy: async () => await import('./pages/workspace/all-page-old/all-page'),
}, },
{ {
path: '/collection', path: '/collection',
lazy: () => import('./pages/workspace/all-collection'), lazy: async () => await import('./pages/workspace/all-collection'),
}, },
{ {
path: '/collection/:collectionId', path: '/collection/:collectionId',
lazy: () => import('./pages/workspace/collection/index'), lazy: async () => await import('./pages/workspace/collection/index'),
}, },
{ {
path: '/tag', path: '/tag',
lazy: () => import('./pages/workspace/all-tag'), lazy: async () => await import('./pages/workspace/all-tag'),
}, },
{ {
path: '/tag/:tagId', path: '/tag/:tagId',
lazy: () => import('./pages/workspace/tag'), lazy: async () => await import('./pages/workspace/tag'),
}, },
{ {
path: '/trash', path: '/trash',
lazy: () => import('./pages/workspace/trash-page'), lazy: async () => await import('./pages/workspace/trash-page'),
}, },
{ {
path: '/:pageId', path: '/:pageId',
lazy: () => import('./pages/workspace/detail-page/detail-page'), lazy: async () => await import('./pages/workspace/detail-page/detail-page'),
}, },
{ {
path: '/:pageId/attachments/:attachmentId', path: '/:pageId/attachments/:attachmentId',
lazy: () => import('./pages/workspace/attachment/index'), lazy: async () => await import('./pages/workspace/attachment/index'),
}, },
{ {
path: '/journals', path: '/journals',
lazy: () => import('./pages/journals'), lazy: async () => await import('./pages/journals'),
}, },
{ {
path: '/settings', path: '/settings',
lazy: () => import('./pages/workspace/settings'), lazy: async () => await import('./pages/workspace/settings'),
}, },
{ {
path: '*', path: '*',
lazy: () => import('./pages/404'), lazy: async () => await import('./pages/404'),
}, },
] satisfies RouteObject[]; ] satisfies RouteObject[];

View File

@@ -216,7 +216,7 @@ const CloudWorkSpaceList = ({
if (currentWorkspaceFlavour === server.id) { if (currentWorkspaceFlavour === server.id) {
const otherWorkspace = workspaces.find(w => w.flavour !== server.id); const otherWorkspace = workspaces.find(w => w.flavour !== server.id);
if (otherWorkspace) { if (otherWorkspace) {
navigateHelper.openPage(otherWorkspace.id, 'all'); navigateHelper.jumpToAll(otherWorkspace.id);
} }
} }
}, [ }, [

View File

@@ -2,7 +2,7 @@ import { NotificationCenter } from '@affine/component';
import { DefaultServerService } from '@affine/core/modules/cloud'; import { DefaultServerService } from '@affine/core/modules/cloud';
import { FrameworkScope, useService } from '@toeverything/infra'; import { FrameworkScope, useService } from '@toeverything/infra';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { Outlet } from 'react-router-dom'; import { Outlet } from 'react-router';
import { GlobalDialogs } from '../../dialogs'; import { GlobalDialogs } from '../../dialogs';

View File

@@ -1,10 +1,13 @@
// eslint-disable-next-line @typescript-eslint/no-restricted-imports import { useAsyncNavigate } from '@affine/core/utils';
import { useNavigate } from 'react-router-dom'; import { useCallback } from 'react';
import { MobileSignInPanel } from '../components/sign-in'; import { MobileSignInPanel } from '../components/sign-in';
export const Component = () => { export const Component = () => {
const navigate = useNavigate(); const navigate = useAsyncNavigate();
const onClose = useCallback(() => {
navigate('/');
}, [navigate]);
return <MobileSignInPanel onClose={() => navigate('/')} />; return <MobileSignInPanel onClose={onClose} />;
}; };

View File

@@ -3,7 +3,7 @@ import { CollectionService } from '@affine/core/modules/collection';
import { GlobalContextService } from '@affine/core/modules/global-context'; import { GlobalContextService } from '@affine/core/modules/global-context';
import { useLiveData, useServices } from '@toeverything/infra'; import { useLiveData, useServices } from '@toeverything/infra';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router';
import { CollectionDetail } from '../../../views'; import { CollectionDetail } from '../../../views';

View File

@@ -37,7 +37,7 @@ import { cssVarV2 } from '@toeverything/theme/v2';
import clsx from 'clsx'; import clsx from 'clsx';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { useCallback, useEffect, useRef, useState } from 'react'; import { useCallback, useEffect, useRef, useState } from 'react';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router';
import { AppTabs } from '../../../components'; import { AppTabs } from '../../../components';
import { JournalConflictBlock } from './journal-conflict-block'; import { JournalConflictBlock } from './journal-conflict-block';
@@ -73,7 +73,7 @@ const DetailPageImpl = () => {
const mode = useLiveData(editor.mode$); const mode = useLiveData(editor.mode$);
const isInTrash = useLiveData(doc.meta$.map(meta => meta.trash)); const isInTrash = useLiveData(doc.meta$.map(meta => meta.trash));
const { openPage, jumpToPageBlock } = useNavigateHelper(); const { jumpToPage, jumpToPageBlock } = useNavigateHelper();
const scrollViewportRef = useRef<HTMLDivElement | null>(null); const scrollViewportRef = useRef<HTMLDivElement | null>(null);
const editorContainer = useLiveData(editor.editorContainer$); const editorContainer = useLiveData(editor.editorContainer$);
@@ -163,7 +163,7 @@ const DetailPageImpl = () => {
); );
} }
return openPage(docCollection.id, pageId); return jumpToPage(docCollection.id, pageId);
}) })
); );
} }
@@ -178,7 +178,7 @@ const DetailPageImpl = () => {
disposable.dispose(); disposable.dispose();
}; };
}, },
[docCollection.id, editor, jumpToPageBlock, openPage, server] [docCollection.id, editor, jumpToPage, jumpToPageBlock, server.baseUrl]
); );
const canEdit = useGuard('Doc_Update', doc.id); const canEdit = useGuard('Doc_Update', doc.id);

View File

@@ -17,7 +17,7 @@ import {
type RouteObject, type RouteObject,
useLocation, useLocation,
useParams, useParams,
} from 'react-router-dom'; } from 'react-router';
import { WorkspaceLayout } from './layout'; import { WorkspaceLayout } from './layout';
import { MobileWorkbenchRoot } from './workbench-root'; import { MobileWorkbenchRoot } from './workbench-root';
@@ -37,7 +37,11 @@ const MobileRouteContainer = ({ route }: { route: Route }) => {
}; };
const warpedRoutes = workbenchRoutes.map((originalRoute: RouteObject) => { const warpedRoutes = workbenchRoutes.map((originalRoute: RouteObject) => {
if (originalRoute.Component || !originalRoute.lazy) { if (
originalRoute.Component ||
!originalRoute.lazy ||
typeof originalRoute.lazy !== 'function'
) {
return originalRoute; return originalRoute;
} }

View File

@@ -4,7 +4,7 @@ import { GlobalContextService } from '@affine/core/modules/global-context';
import { TagService } from '@affine/core/modules/tag'; import { TagService } from '@affine/core/modules/tag';
import { useLiveData, useService } from '@toeverything/infra'; import { useLiveData, useService } from '@toeverything/infra';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router';
import { TagDetail } from '../../../views'; import { TagDetail } from '../../../views';

View File

@@ -5,7 +5,7 @@ import {
import { ViewRoot } from '@affine/core/modules/workbench/view/view-root'; import { ViewRoot } from '@affine/core/modules/workbench/view/view-root';
import { useLiveData, useService } from '@toeverything/infra'; import { useLiveData, useService } from '@toeverything/infra';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { type RouteObject, useLocation } from 'react-router-dom'; import { type RouteObject, useLocation } from 'react-router';
export const MobileWorkbenchRoot = ({ routes }: { routes: RouteObject[] }) => { export const MobileWorkbenchRoot = ({ routes }: { routes: RouteObject[] }) => {
const workbench = useService(WorkbenchService).workbench; const workbench = useService(WorkbenchService).workbench;

View File

@@ -1,13 +1,15 @@
import { NavigateContext } from '@affine/core/components/hooks/use-navigate-helper'; import { NavigateContext } from '@affine/core/components/hooks/use-navigate-helper';
import { wrapCreateBrowserRouterV6 } from '@sentry/react'; import { ROUTES } from '@affine/routes';
import { wrapCreateBrowserRouterV7 } from '@sentry/react';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import type { RouteObject } from 'react-router-dom'; import type { RouteObject } from 'react-router';
import { import {
createBrowserRouter as reactRouterCreateBrowserRouter, createBrowserRouter as reactRouterCreateBrowserRouter,
redirect, redirect,
useNavigate, useNavigate,
} from 'react-router-dom'; } from 'react-router';
import { AppFallback } from './components/app-fallback';
import { RootWrapper } from './pages/root'; import { RootWrapper } from './pages/root';
function RootRouter() { function RootRouter() {
@@ -30,77 +32,76 @@ function RootRouter() {
export const topLevelRoutes = [ export const topLevelRoutes = [
{ {
element: <RootRouter />, element: <RootRouter />,
hydrateFallbackElement: <AppFallback />,
children: [ children: [
{ {
path: '/', path: ROUTES.index,
lazy: () => import('./pages/index'), lazy: async () => await import('./pages/index'),
}, },
{ {
path: '/workspace/:workspaceId/*', path: `${ROUTES.workspace.index}/*`,
lazy: () => import('./pages/workspace/index'), lazy: async () => await import('./pages/workspace/index'),
}, },
{ {
path: '/share/:workspaceId/:pageId', path: ROUTES.share,
loader: ({ params }) => { loader: async ({ params }) => {
return redirect(`/workspace/${params.workspaceId}/${params.pageId}`); return redirect(
`/workspaces/${params.workspaceId}/docs/${params.pageId}`
);
}, },
}, },
{ {
path: '/404', path: ROUTES.notFound,
lazy: () => import('./pages/404'), lazy: async () => await import('./pages/404'),
}, },
{ {
path: '/auth/:authType', path: ROUTES.auth,
lazy: () => import('./pages/auth'), lazy: async () => await import('./pages/auth'),
}, },
{ {
path: '/sign-in', path: ROUTES.signIn,
lazy: () => import('./pages/sign-in'), lazy: async () => await import('./pages/sign-in'),
}, },
{ {
path: '/magic-link', path: ROUTES.magicLink,
lazy: () => lazy: async () =>
import( await import(
/* webpackChunkName: "auth" */ '@affine/core/desktop/pages/auth/magic-link' /* webpackChunkName: "auth" */ '@affine/core/desktop/pages/auth/magic-link'
), ),
}, },
{ {
path: '/oauth/login', path: ROUTES.oauth.login,
lazy: () => lazy: async () =>
import( await import(
/* webpackChunkName: "auth" */ '@affine/core/desktop/pages/auth/oauth-login' /* webpackChunkName: "auth" */ '@affine/core/desktop/pages/auth/oauth-login'
), ),
}, },
{ {
path: '/oauth/callback', path: ROUTES.oauth.callback,
lazy: () => lazy: async () =>
import( await import(
/* webpackChunkName: "auth" */ '@affine/core/desktop/pages/auth/oauth-callback' /* webpackChunkName: "auth" */ '@affine/core/desktop/pages/auth/oauth-callback'
), ),
}, },
{ {
path: '/redirect-proxy', path: ROUTES.redirect,
lazy: () => import('@affine/core/desktop/pages/redirect'), lazy: async () => await import('@affine/core/desktop/pages/redirect'),
}, },
{ {
path: '/open-app/:action', path: ROUTES.openApp,
lazy: () => import('@affine/core/desktop/pages/open-app'), lazy: async () => await import('@affine/core/desktop/pages/open-app'),
}, },
{ {
path: '*', path: '*',
lazy: () => import('./pages/404'), lazy: async () => await import('./pages/404'),
}, },
], ],
}, },
] satisfies [RouteObject, ...RouteObject[]]; ] satisfies [RouteObject, ...RouteObject[]];
const createBrowserRouter = wrapCreateBrowserRouterV6( const createBrowserRouter = wrapCreateBrowserRouterV7(
reactRouterCreateBrowserRouter reactRouterCreateBrowserRouter
); );
export const router = ( export const router = (
window.SENTRY_RELEASE ? createBrowserRouter : reactRouterCreateBrowserRouter window.SENTRY_RELEASE ? createBrowserRouter : reactRouterCreateBrowserRouter
)(topLevelRoutes, { )(topLevelRoutes);
future: {
v7_normalizeFormMethod: true,
},
});

View File

@@ -1,4 +1,4 @@
import type { RouteObject } from 'react-router-dom'; import type { RouteObject } from 'react-router';
import { Component as All } from './pages/workspace/all'; import { Component as All } from './pages/workspace/all';
import { Component as Collection } from './pages/workspace/collection'; import { Component as Collection } from './pages/workspace/collection';
@@ -43,14 +43,15 @@ export const workbenchRoutes = [
}, },
{ {
path: '/trash', path: '/trash',
lazy: () => import('./pages/workspace/trash'), lazy: async () => await import('./pages/workspace/trash'),
}, },
{ {
path: '/:pageId', path: '/:pageId',
lazy: () => import('./pages/workspace/detail/mobile-detail-page'), lazy: async () =>
await import('./pages/workspace/detail/mobile-detail-page'),
}, },
{ {
path: '*', path: '*',
lazy: () => import('./pages/404'), lazy: async () => await import('./pages/404'),
}, },
] satisfies [RouteObject, ...RouteObject[]]; ] satisfies [RouteObject, ...RouteObject[]];

View File

@@ -1,7 +1,7 @@
import { DualLinkIcon } from '@blocksuite/icons/rc'; import { DualLinkIcon } from '@blocksuite/icons/rc';
import { cssVarV2 } from '@toeverything/theme/v2'; import { cssVarV2 } from '@toeverything/theme/v2';
import type { ReactElement, SVGAttributes } from 'react'; import type { ReactElement, SVGAttributes } from 'react';
import type { To } from 'react-router-dom'; import type { To } from 'react-router';
import { MenuLinkItem } from './index'; import { MenuLinkItem } from './index';

View File

@@ -2,7 +2,7 @@ import { WorkbenchLink } from '@affine/core/modules/workbench';
import { ArrowDownSmallIcon } from '@blocksuite/icons/rc'; import { ArrowDownSmallIcon } from '@blocksuite/icons/rc';
import clsx from 'clsx'; import clsx from 'clsx';
import React, { type SVGAttributes } from 'react'; import React, { type SVGAttributes } from 'react';
import type { To } from 'react-router-dom'; import type { To } from 'react-router';
import * as styles from './index.css'; import * as styles from './index.css';

View File

@@ -1,8 +1,8 @@
import { useAsyncNavigate } from '@affine/core/utils';
import { useLiveData } from '@toeverything/infra'; import { useLiveData } from '@toeverything/infra';
import type { Location } from 'history'; import type { Location } from 'history';
import { useEffect } from 'react'; import { useEffect } from 'react';
// eslint-disable-next-line @typescript-eslint/no-restricted-imports import { useLocation } from 'react-router';
import { useLocation, useNavigate } from 'react-router-dom';
import type { Workbench } from '../entities/workbench'; import type { Workbench } from '../entities/workbench';
@@ -26,7 +26,7 @@ export function useBindWorkbenchToBrowserRouter(
workbench: Workbench, workbench: Workbench,
basename: string basename: string
) { ) {
const navigate = useNavigate(); const navigate = useAsyncNavigate();
const browserLocation = useLocation(); const browserLocation = useLocation();
const view = useLiveData(workbench.activeView$); const view = useLiveData(workbench.activeView$);

View File

@@ -1,7 +1,6 @@
import type { Location } from 'history'; import type { Location } from 'history';
import { useEffect } from 'react'; import { useEffect } from 'react';
// eslint-disable-next-line @typescript-eslint/no-restricted-imports import { useLocation } from 'react-router';
import { useLocation } from 'react-router-dom';
import type { Workbench } from '../entities/workbench'; import type { Workbench } from '../entities/workbench';

View File

@@ -3,7 +3,7 @@ import { AffineErrorBoundary } from '@affine/core/components/affine/affine-error
import { RightSidebarIcon } from '@blocksuite/icons/rc'; import { RightSidebarIcon } from '@blocksuite/icons/rc';
import { useLiveData, useService } from '@toeverything/infra'; import { useLiveData, useService } from '@toeverything/infra';
import { Suspense, useCallback } from 'react'; import { Suspense, useCallback } from 'react';
import { Outlet } from 'react-router-dom'; import { Outlet } from 'react-router';
import { AppSidebarService } from '../../app-sidebar'; import { AppSidebarService } from '../../app-sidebar';
import { SidebarSwitch } from '../../app-sidebar/views/sidebar-header'; import { SidebarSwitch } from '../../app-sidebar/views/sidebar-header';

View File

@@ -1,12 +1,12 @@
import { FrameworkScope, useLiveData } from '@toeverything/infra'; import { FrameworkScope, useLiveData } from '@toeverything/infra';
import { useLayoutEffect, useMemo } from 'react'; import { useLayoutEffect, useMemo } from 'react';
import type { RouteObject } from 'react-router-dom'; import type { RouteObject } from 'react-router';
import { import {
createMemoryRouter, createMemoryRouter,
RouterProvider, RouterProvider,
UNSAFE_LocationContext, UNSAFE_LocationContext,
UNSAFE_RouteContext, UNSAFE_RouteContext,
} from 'react-router-dom'; } from 'react-router';
import type { View } from '../entities/view'; import type { View } from '../entities/view';

View File

@@ -9,7 +9,7 @@ import {
} from '@toeverything/infra'; } from '@toeverything/infra';
import { useAtomValue } from 'jotai'; import { useAtomValue } from 'jotai';
import { memo, useCallback, useEffect, useRef, useState } from 'react'; import { memo, useCallback, useEffect, useRef, useState } from 'react';
import { type RouteObject, useLocation } from 'react-router-dom'; import { type RouteObject, useLocation } from 'react-router';
import type { View } from '../entities/view'; import type { View } from '../entities/view';
import { WorkbenchService } from '../services/workbench'; import { WorkbenchService } from '../services/workbench';

View File

@@ -5,3 +5,4 @@ export * from './extract-emoji-icon';
export * from './string2color'; export * from './string2color';
export * from './toast'; export * from './toast';
export * from './unflatten-object'; export * from './unflatten-object';
export * from './use-async-navigate';

View File

@@ -0,0 +1,25 @@
import { notify } from '@affine/component';
import { UserFriendlyError } from '@affine/error';
import { useCallback } from 'react';
import { type NavigateOptions, type To, useNavigate } from 'react-router';
export const useAsyncNavigate = () => {
const navigate = useNavigate();
const nav = useCallback(
(to: To, options?: NavigateOptions) => {
const result = navigate(to, options);
if (result instanceof Promise) {
result.catch((err: Error) => {
const error = UserFriendlyError.fromAny(err);
console.error(error);
notify.error(error);
});
}
return;
},
[navigate]
);
return nav;
};

View File

@@ -18,6 +18,6 @@
}, },
"peerDependencies": { "peerDependencies": {
"react": "^19.1.0", "react": "^19.1.0",
"react-router-dom": "^7.5.1" "react-router": "^7.5.2"
} }
} }

View File

@@ -2,6 +2,70 @@
"$schema": "./schema.json", "$schema": "./schema.json",
"route": "/", "route": "/",
"children": { "children": {
"workspace": {
"route": "workspaces/:workspaceId",
"children": {
"all": "all",
"trash": "trash",
"doc": {
"route": "docs/:docId",
"children": {
"attachment": "attachment/:attachmentId"
}
},
"journals": "journals",
"collections": {
"route": "collections",
"children": {
"collection": ":collectionId"
}
},
"tags": {
"route": "tags",
"children": {
"tag": ":tagId"
}
},
"settings": "settings"
}
},
"share": "share/:workspaceId/:pageId",
"expired": "expired",
"invite": "invite/:inviteId",
"payment": "payment/:plan/success",
"onboarding": "onboarding",
"redirect": "redirect",
"subscribe": "subscribe",
"upgradeToTeam": "upgrade-to-team",
"upgradeSuccess": {
"route": "upgrade-success",
"children": {
"team": "team",
"selfHostTeam": "self-host-team"
}
},
"aiUpgradeSuccess": "ai-upgrade-success",
"tryCloud": "try-cloud",
"themeEditor": "theme-editor",
"template": {
"route": "template",
"children": {
"import": "import",
"preview": "preview"
}
},
"auth": "auth/:authType",
"signIn": "sign-in",
"magicLink": "magic-link",
"oauth": {
"route": "oauth",
"children": {
"login": "login",
"callback": "callback"
}
},
"openApp": "open-app/:action",
"notFound": "404",
"admin": { "admin": {
"route": "admin", "route": "admin",
"children": { "children": {

View File

@@ -1,5 +1,29 @@
// #region Path Parameter Types // #region Path Parameter Types
export interface RouteParamsTypes { export interface RouteParamsTypes {
workspace: {
index: { workspaceId: string };
all: { workspaceId: string };
trash: { workspaceId: string };
doc: {
index: { workspaceId: string; docId: string };
attachment: { workspaceId: string; docId: string; attachmentId: string };
};
journals: { workspaceId: string };
collections: {
index: { workspaceId: string };
collection: { workspaceId: string; collectionId: string };
};
tags: {
index: { workspaceId: string };
tag: { workspaceId: string; tagId: string };
};
settings: { workspaceId: string };
};
share: { workspaceId: string; pageId: string };
invite: { inviteId: string };
payment: { plan: string };
auth: { authType: string };
openApp: { action: string };
admin: { settings: { module: { module: string } } }; admin: { settings: { module: { module: string } } };
} }
// #endregion // #endregion
@@ -7,6 +31,57 @@ export interface RouteParamsTypes {
// #region Absolute Paths // #region Absolute Paths
export const ROUTES = { export const ROUTES = {
index: '/', index: '/',
workspace: {
index: '/workspaces/:workspaceId',
all: '/workspaces/:workspaceId/all',
trash: '/workspaces/:workspaceId/trash',
doc: {
index: '/workspaces/:workspaceId/docs/:docId',
attachment:
'/workspaces/:workspaceId/docs/:docId/attachment/:attachmentId',
},
journals: '/workspaces/:workspaceId/journals',
collections: {
index: '/workspaces/:workspaceId/collections',
collection: '/workspaces/:workspaceId/collections/:collectionId',
},
tags: {
index: '/workspaces/:workspaceId/tags',
tag: '/workspaces/:workspaceId/tags/:tagId',
},
settings: '/workspaces/:workspaceId/settings',
},
share: '/share/:workspaceId/:pageId',
expired: '/expired',
invite: '/invite/:inviteId',
payment: '/payment/:plan/success',
onboarding: '/onboarding',
redirect: '/redirect',
subscribe: '/subscribe',
upgradeToTeam: '/upgrade-to-team',
upgradeSuccess: {
index: '/upgrade-success',
team: '/upgrade-success/team',
selfHostTeam: '/upgrade-success/self-host-team',
},
aiUpgradeSuccess: '/ai-upgrade-success',
tryCloud: '/try-cloud',
themeEditor: '/theme-editor',
template: {
index: '/template',
import: '/template/import',
preview: '/template/preview',
},
auth: '/auth/:authType',
signIn: '/sign-in',
magicLink: '/magic-link',
oauth: {
index: '/oauth',
login: '/oauth/login',
callback: '/oauth/callback',
},
openApp: '/open-app/:action',
notFound: '/404',
admin: { admin: {
index: '/admin', index: '/admin',
auth: '/admin/auth', auth: '/admin/auth',
@@ -23,6 +98,39 @@ export const ROUTES = {
// #region Relative Paths // #region Relative Paths
export const RELATIVE_ROUTES = { export const RELATIVE_ROUTES = {
index: '/', index: '/',
workspace: {
index: 'workspaces/:workspaceId',
all: 'all',
trash: 'trash',
doc: { index: 'docs/:docId', attachment: 'attachment/:attachmentId' },
journals: 'journals',
collections: { index: 'collections', collection: ':collectionId' },
tags: { index: 'tags', tag: ':tagId' },
settings: 'settings',
},
share: 'share/:workspaceId/:pageId',
expired: 'expired',
invite: 'invite/:inviteId',
payment: 'payment/:plan/success',
onboarding: 'onboarding',
redirect: 'redirect',
subscribe: 'subscribe',
upgradeToTeam: 'upgrade-to-team',
upgradeSuccess: {
index: 'upgrade-success',
team: 'team',
selfHostTeam: 'self-host-team',
},
aiUpgradeSuccess: 'ai-upgrade-success',
tryCloud: 'try-cloud',
themeEditor: 'theme-editor',
template: { index: 'template', import: 'import', preview: 'preview' },
auth: 'auth/:authType',
signIn: 'sign-in',
magicLink: 'magic-link',
oauth: { index: 'oauth', login: 'login', callback: 'callback' },
openApp: 'open-app/:action',
notFound: '404',
admin: { admin: {
index: 'admin', index: 'admin',
auth: 'auth', auth: 'auth',
@@ -38,6 +146,63 @@ export const RELATIVE_ROUTES = {
// #region Path Factories // #region Path Factories
const home = () => '/'; const home = () => '/';
const workspace = (params: { workspaceId: string }) =>
`/workspaces/${params.workspaceId}`;
workspace.all = (params: { workspaceId: string }) =>
`/workspaces/${params.workspaceId}/all`;
workspace.trash = (params: { workspaceId: string }) =>
`/workspaces/${params.workspaceId}/trash`;
const workspace_doc = (params: { workspaceId: string; docId: string }) =>
`/workspaces/${params.workspaceId}/docs/${params.docId}`;
workspace_doc.attachment = (params: {
workspaceId: string;
docId: string;
attachmentId: string;
}) =>
`/workspaces/${params.workspaceId}/docs/${params.docId}/attachment/${params.attachmentId}`;
workspace.doc = workspace_doc;
workspace.journals = (params: { workspaceId: string }) =>
`/workspaces/${params.workspaceId}/journals`;
const workspace_collections = (params: { workspaceId: string }) =>
`/workspaces/${params.workspaceId}/collections`;
workspace_collections.collection = (params: {
workspaceId: string;
collectionId: string;
}) => `/workspaces/${params.workspaceId}/collections/${params.collectionId}`;
workspace.collections = workspace_collections;
const workspace_tags = (params: { workspaceId: string }) =>
`/workspaces/${params.workspaceId}/tags`;
workspace_tags.tag = (params: { workspaceId: string; tagId: string }) =>
`/workspaces/${params.workspaceId}/tags/${params.tagId}`;
workspace.tags = workspace_tags;
workspace.settings = (params: { workspaceId: string }) =>
`/workspaces/${params.workspaceId}/settings`;
const share = (params: { workspaceId: string; pageId: string }) =>
`/share/${params.workspaceId}/${params.pageId}`;
const expired = () => '/expired';
const invite = (params: { inviteId: string }) => `/invite/${params.inviteId}`;
const payment = (params: { plan: string }) => `/payment/${params.plan}/success`;
const onboarding = () => '/onboarding';
const redirect = () => '/redirect';
const subscribe = () => '/subscribe';
const upgradeToTeam = () => '/upgrade-to-team';
const upgradeSuccess = () => '/upgrade-success';
upgradeSuccess.team = () => '/upgrade-success/team';
upgradeSuccess.selfHostTeam = () => '/upgrade-success/self-host-team';
const aiUpgradeSuccess = () => '/ai-upgrade-success';
const tryCloud = () => '/try-cloud';
const themeEditor = () => '/theme-editor';
const template = () => '/template';
template.import = () => '/template/import';
template.preview = () => '/template/preview';
const auth = (params: { authType: string }) => `/auth/${params.authType}`;
const signIn = () => '/sign-in';
const magicLink = () => '/magic-link';
const oauth = () => '/oauth';
oauth.login = () => '/oauth/login';
oauth.callback = () => '/oauth/callback';
const openApp = (params: { action: string }) => `/open-app/${params.action}`;
const notFound = () => '/404';
const admin = () => '/admin'; const admin = () => '/admin';
admin.auth = () => '/admin/auth'; admin.auth = () => '/admin/auth';
admin.setup = () => '/admin/setup'; admin.setup = () => '/admin/setup';
@@ -49,5 +214,28 @@ admin_settings.module = (params: { module: string }) =>
admin.settings = admin_settings; admin.settings = admin_settings;
admin.about = () => '/admin/about'; admin.about = () => '/admin/about';
admin.notFound = () => '/admin/404'; admin.notFound = () => '/admin/404';
export const FACTORIES = { admin, home }; export const FACTORIES = {
workspace,
share,
expired,
invite,
payment,
onboarding,
redirect,
subscribe,
upgradeToTeam,
upgradeSuccess,
aiUpgradeSuccess,
tryCloud,
themeEditor,
template,
auth,
signIn,
magicLink,
oauth,
openApp,
notFound,
admin,
home,
};
// #endregion // #endregion

View File

@@ -10,7 +10,7 @@
"@affine/debug": "workspace:*", "@affine/debug": "workspace:*",
"@sentry/react": "^9.2.0", "@sentry/react": "^9.2.0",
"mixpanel-browser": "^2.56.0", "mixpanel-browser": "^2.56.0",
"react-router-dom": "6.30.0" "react-router": "^7.6.0"
}, },
"devDependencies": { "devDependencies": {
"@types/mixpanel-browser": "^2.50.2", "@types/mixpanel-browser": "^2.50.2",

View File

@@ -5,7 +5,7 @@ import {
matchRoutes, matchRoutes,
useLocation, useLocation,
useNavigationType, useNavigationType,
} from 'react-router-dom'; } from 'react-router';
function createSentry() { function createSentry() {
let client: Sentry.BrowserClient | undefined; let client: Sentry.BrowserClient | undefined;
@@ -18,7 +18,7 @@ function createSentry() {
debug: BUILD_CONFIG.debug ?? false, debug: BUILD_CONFIG.debug ?? false,
environment: BUILD_CONFIG.appBuildType, environment: BUILD_CONFIG.appBuildType,
integrations: [ integrations: [
Sentry.reactRouterV6BrowserTracingIntegration({ Sentry.reactRouterV7BrowserTracingIntegration({
useEffect, useEffect,
useLocation, useLocation,
useNavigationType, useNavigationType,

View File

@@ -46,6 +46,7 @@ test('should not show hidden note in embed view page mode', async ({
await createLinkedPage(page, 'Test Page'); await createLinkedPage(page, 'Test Page');
const inlineLink = page.locator('affine-reference'); const inlineLink = page.locator('affine-reference');
await inlineLink.dblclick(); await inlineLink.dblclick();
await waitForEditorLoad(page);
// reference the previous page // reference the previous page
await page.keyboard.press('Enter'); await page.keyboard.press('Enter');

View File

@@ -255,6 +255,7 @@ test('create template doc from sidebar template entrance', async ({ page }) => {
test('should show starter-bar when doc is empty', async ({ page }) => { test('should show starter-bar when doc is empty', async ({ page }) => {
await openHomePage(page); await openHomePage(page);
await page.getByTestId('sidebar-new-page-button').click(); await page.getByTestId('sidebar-new-page-button').click();
await waitForEditorLoad(page);
await page.keyboard.press('ArrowDown'); await page.keyboard.press('ArrowDown');
const starterBar = page.getByTestId('starter-bar'); const starterBar = page.getByTestId('starter-bar');
await expect(starterBar).toBeVisible(); await expect(starterBar).toBeVisible();

View File

@@ -46,6 +46,7 @@ test('all tab', async ({ page }) => {
await expect(docsTab).toBeVisible(); await expect(docsTab).toBeVisible();
await docsTab.click(); await docsTab.click();
await page.waitForTimeout(300);
const todayDocs = page.getByTestId('doc-list-item'); const todayDocs = page.getByTestId('doc-list-item');
expect(await todayDocs.count()).toBeGreaterThan(0); expect(await todayDocs.count()).toBeGreaterThan(0);

1289
yarn.lock

File diff suppressed because it is too large Load Diff