Skip to content

Commit c66d4e9

Browse files
hotzenklotzcoderabbitai[bot]MichaelBuessemeyer
authored
Update React Router to v6 (#8739)
This PR updates React Router (RR) from v5 to v6 following their [upgrade guide](https://reactrouter.com/6.30.1/upgrading/v5). There is already react-router v7 but let's go step by step. v7 also seems to have the same API as v6 so we might update soon in a follow up PR (and double check compatibility with React v18/19). Noteworthy changes and high level concepts: - RR v6 offer various ways of setting up a router ([Docs](https://reactrouter.com/6.30.1/routers/picking-a-router)): - Declarative: Using JSX `<Route`> elements like we did so far - Data API: Create routes as TS code. This is the recommend method and what I used for the refactor. - Ideally the routes are constant and live outside a react component to avoid constant recreation of the router/routes - While you are meant to define routes as code, there is a "bridge"/helper, [`createRoutesFromElements`]( https://reactrouter.com/6.30.1/utils/create-routes-from-elements), that translates JSX `<Route>` objects into code. I used that to keep the diff and changes somewhat manageable. We can switch to a purely code based definition in the future. - File-Base/Framework: You can define routes through your hierarchy on the file system. (not used) - RR v6 `<Routes>` do not have a `render={() => ...}` prop anymore that would run any function. That makes it impossible to do any sort of conditional logic inside the router. I moved all these methods into new React wrapper components. - RR v6 uses new hooks: `useNavigation`, `useParams`(for accessing route parameters, e.g. `/tasks/:taskId`) - RR v6 allows [nested routing](https://reactrouter.com/6.30.1/start/overview#nested-routes). A common parent page (with nice layout) can be used to render/insert the child views with an `<Outlet />` component. I used it for the orga & account pages for rendering the various tabs into a comment parent. Also, the router's root element uses an `<Outlet>`. - RR v6 does not support class-based React component anymore. The `withRouter` HOC was removed. I build a new `withRouter` HOC as a workaround for our remaining React class components. - RR v6 removed an imperative API for blocking the navigation from one page to another. It was replace with a hook [`useBlocker`](https://reactrouter.com/6.30.1/hooks/use-blocker). Unfortunately, we mostly use the blocker in class based components. There is not easy workaround - so I had to remove some of the blocking stuff. (Dataset Setting are currently being refactored as functional components and will be able to use that once more. #8732 ) ### URL of deployed dev instance (used for testing): - https://___.webknossos.xyz ### Steps to test: My testing process: - Click through all the routes/views and make sure they still work - Make sure that routes with parameters still work, e.g. all the task, projects, task types stuff with ids and prefilled forms - Test a legacy route ### TODOs: - [x] Fix: navigation blockers - [ ] Decide: Should we rename the `SecuredRoute` component? It is not a `<Route>` any more... maybe `AuthRequired` or `RequireAuth` or something. - [x] Fix: `features` type definition. It needs too load earlier. - [x] Fix: dashboard header having too many items <img width="1650" alt="Screenshot 2025-07-03 at 09 37 11" src="https://github.com/user-attachments/assets/9bba76e3-2b3a-4bad-8be0-798685d0e4ac" /> ### Issues: - fixes # ------ (Please delete unneeded items, merge only when none are left open) - [x] Added changelog entry (create a `$PR_NUMBER.md` file in `unreleased_changes` or use `./tools/create-changelog-entry.py`) - [ ] Added migration guide entry if applicable (edit the same file as for the changelog) - [ ] Updated [documentation](../blob/master/docs) if applicable - [ ] Adapted [wk-libs python client](https://github.com/scalableminds/webknossos-libs/tree/master/webknossos/webknossos/client) if relevant API parts change - [ ] Removed dev-only changes like prints and application.conf edits - [ ] Considered [common edge cases](../blob/master/.github/common_edge_cases.md) - [ ] Needs datastore update after deployment --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: Michael Büßemeyer <39529669+MichaelBuessemeyer@users.noreply.github.com>
1 parent 0967358 commit c66d4e9

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+1272
-1466
lines changed

frontend/javascripts/admin/account/account_password_view.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { Alert, Button, Col, Form, Input, Row, Space } from "antd";
44
import Toast from "libs/toast";
55
import messages from "messages";
66
import { useState } from "react";
7-
import { useHistory } from "react-router-dom";
7+
import { useNavigate } from "react-router-dom";
88
import { logoutUserAction } from "viewer/model/actions/user_actions";
99
import Store from "viewer/store";
1010
import { SettingsCard } from "./helpers/settings_card";
@@ -15,7 +15,7 @@ const { Password } = Input;
1515
const MIN_PASSWORD_LENGTH = 8;
1616

1717
function AccountPasswordView() {
18-
const history = useHistory();
18+
const navigate = useNavigate();
1919
const [form] = Form.useForm();
2020
const [isResetPasswordVisible, setResetPasswordVisible] = useState(false);
2121

@@ -25,7 +25,7 @@ function AccountPasswordView() {
2525
Toast.success(messages["auth.reset_pw_confirmation"]);
2626
await logoutUser();
2727
Store.dispatch(logoutUserAction());
28-
history.push("/auth/login");
28+
navigate("/auth/login");
2929
})
3030
.catch((error) => {
3131
console.error("Password change failed:", error);

frontend/javascripts/admin/account/account_settings_view.tsx

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
import { SafetyOutlined, SettingOutlined, UserOutlined } from "@ant-design/icons";
22
import { Breadcrumb, Layout, Menu } from "antd";
33
import type { MenuItemGroupType } from "antd/es/menu/interface";
4-
import { Redirect, Route, Switch, useHistory, useLocation } from "react-router-dom";
5-
import AccountAuthTokenView from "./account_auth_token_view";
6-
import AccountPasswordView from "./account_password_view";
7-
import AccountProfileView from "./account_profile_view";
4+
import { Outlet, useLocation, useNavigate } from "react-router-dom";
85

96
const { Sider, Content } = Layout;
107

@@ -46,7 +43,7 @@ const MENU_ITEMS: MenuItemGroupType[] = [
4643

4744
function AccountSettingsView() {
4845
const location = useLocation();
49-
const history = useHistory();
46+
const navigate = useNavigate();
5047
const selectedKey = location.pathname.split("/").filter(Boolean).pop() || "profile";
5148

5249
const breadcrumbItems = [
@@ -68,17 +65,12 @@ function AccountSettingsView() {
6865
selectedKeys={[selectedKey]}
6966
style={{ height: "100%", padding: 24 }}
7067
items={MENU_ITEMS}
71-
onClick={({ key }) => history.push(`/account/${key}`)}
68+
onClick={({ key }) => navigate(`/account/${key}`)}
7269
/>
7370
</Sider>
7471
<Content style={{ padding: "32px", minHeight: 280, maxWidth: 1000 }}>
7572
<Breadcrumb style={{ marginBottom: "16px" }} items={breadcrumbItems} />
76-
<Switch>
77-
<Route path="/account/profile" component={AccountProfileView} />
78-
<Route path="/account/password" component={AccountPasswordView} />
79-
<Route path="/account/token" component={AccountAuthTokenView} />
80-
<Route path="/account" render={() => <Redirect to="/account/profile" />} />
81-
</Switch>
73+
<Outlet />
8274
</Content>
8375
</Layout>
8476
);

frontend/javascripts/admin/auth/accept_invite_view.tsx

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,19 @@ import { getOrganizationByInvite, joinOrganization, switchToOrganization } from
44
import { Button, Layout, Result, Spin } from "antd";
55
import { AsyncButton } from "components/async_clickables";
66
import { useFetch } from "libs/react_helpers";
7+
import { useWkSelector } from "libs/react_hooks";
78
import Toast from "libs/toast";
89
import { location } from "libs/window";
910
import { useState } from "react";
10-
import { useHistory } from "react-router-dom";
11-
import type { APIUser } from "types/api_types";
11+
import { useNavigate, useParams } from "react-router-dom";
1212

1313
const { Content } = Layout;
1414

15-
export default function AcceptInviteView({
16-
token,
17-
activeUser,
18-
}: {
19-
token: string;
20-
activeUser: APIUser | null | undefined;
21-
}) {
22-
const history = useHistory();
15+
export default function AcceptInviteView() {
16+
const activeUser = useWkSelector((state) => state.activeUser);
17+
const { token = "" } = useParams();
18+
const navigate = useNavigate();
19+
2320
const [isAuthenticationModalOpen, setIsAuthenticationModalOpen] = useState(false);
2421
const [targetOrganization, exception] = useFetch(
2522
async () => {
@@ -46,7 +43,7 @@ export default function AcceptInviteView({
4643
targetOrganization != null ? targetOrganization.name || targetOrganization.id : "unknown";
4744

4845
const onSuccessfulJoin = (userJustRegistered: boolean = false) => {
49-
history.push("/dashboard");
46+
navigate("/dashboard");
5047

5148
if (userJustRegistered) {
5249
// Since the user just registered, the organization is already active.

frontend/javascripts/admin/auth/finish_reset_password_view.tsx

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,32 +2,34 @@ import { LockOutlined } from "@ant-design/icons";
22
import { Button, Card, Col, Form, Input, Row } from "antd";
33
import Request from "libs/request";
44
import Toast from "libs/toast";
5+
import { getUrlParamsObjectFromString } from "libs/utils";
56
import messages from "messages";
6-
import { useHistory } from "react-router-dom";
7+
import { useLocation, useNavigate } from "react-router-dom";
8+
79
const FormItem = Form.Item;
810
const { Password } = Input;
9-
type Props = {
10-
resetToken: string;
11-
};
1211

13-
function FinishResetPasswordView(props: Props) {
12+
function FinishResetPasswordView() {
13+
const location = useLocation();
14+
const { token } = getUrlParamsObjectFromString(location.search);
15+
1416
const [form] = Form.useForm();
15-
const history = useHistory();
17+
const navigate = useNavigate();
1618

1719
function onFinish(formValues: Record<string, any>) {
1820
const data = formValues;
1921

20-
if (props.resetToken === "") {
22+
if (token == null) {
2123
Toast.error(messages["auth.reset_token_not_supplied"]);
2224
return;
2325
}
2426

25-
data.token = props.resetToken;
27+
data.token = token;
2628
Request.sendJSONReceiveJSON("/api/auth/resetPassword", {
2729
data,
2830
}).then(() => {
2931
Toast.success(messages["auth.reset_pw_confirmation"]);
30-
history.push("/auth/login");
32+
navigate("/auth/login");
3133
});
3234
}
3335

frontend/javascripts/admin/auth/login_view.tsx

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,30 @@
11
import { Card, Col, Row } from "antd";
2+
import { useWkSelector } from "libs/react_hooks";
23
import * as Utils from "libs/utils";
34
import window from "libs/window";
4-
import { useHistory } from "react-router-dom";
5+
import { useNavigate } from "react-router-dom";
56
import LoginForm from "./login_form";
67

78
type Props = {
89
redirect?: string;
910
};
1011

1112
function LoginView({ redirect }: Props) {
12-
const history = useHistory();
13+
const navigate = useNavigate();
14+
const isAuthenticated = useWkSelector((state) => state.activeUser != null);
15+
16+
if (isAuthenticated) {
17+
// If you're already logged in, redirect to the dashboard
18+
navigate("/");
19+
}
20+
1321
const onLoggedIn = () => {
1422
if (!Utils.hasUrlParam("redirectPage")) {
1523
if (redirect) {
1624
// Use "redirect" prop for internal redirects, e.g. for SecuredRoutes
17-
history.push(redirect);
25+
navigate(redirect);
1826
} else {
19-
history.push("/dashboard");
27+
navigate("/dashboard");
2028
}
2129
} else {
2230
// Use "redirectPage" URL parameter to cause a full page reload and redirecting to external sites

frontend/javascripts/admin/auth/registration_view.tsx

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@ import RegistrationFormWKOrg from "admin/auth/registration_form_wkorg";
33
import { getDefaultOrganization } from "admin/rest_api";
44
import { Card, Col, Row, Spin } from "antd";
55
import features from "features";
6+
import { useWkSelector } from "libs/react_hooks";
67
import Toast from "libs/toast";
78
import messages from "messages";
89
import { useEffect, useState } from "react";
9-
import { Link, useHistory } from "react-router-dom";
10+
import { Link, Navigate, useNavigate } from "react-router-dom";
1011
import type { APIOrganization } from "types/api_types";
1112

1213
function RegistrationViewGeneric() {
13-
const history = useHistory();
14+
const navigate = useNavigate();
1415
const [organization, setOrganization] = useState<APIOrganization | null>(null);
1516
const [isLoading, setIsLoading] = useState(true);
1617

@@ -55,10 +56,10 @@ function RegistrationViewGeneric() {
5556
targetOrganization={organization}
5657
onRegistered={(isUserLoggedIn?: boolean) => {
5758
if (isUserLoggedIn) {
58-
history.goBack();
59+
navigate(-1);
5960
} else {
6061
Toast.success(messages["auth.account_created"]);
61-
history.push("/auth/login");
62+
navigate("/auth/login");
6263
}
6364
}}
6465
/>
@@ -95,15 +96,15 @@ function RegistrationViewGeneric() {
9596
}
9697

9798
function RegistrationViewWkOrg() {
98-
const history = useHistory();
99+
const navigate = useNavigate();
99100
return (
100101
<Row justify="center" align="middle" className="login-view">
101102
<Col>
102103
<Card className="login-content drawing-signup" style={{ maxWidth: 1000 }}>
103104
<h3>Sign Up</h3>
104105
<RegistrationFormWKOrg
105106
onRegistered={() => {
106-
history.push("/dashboard");
107+
navigate("/dashboard");
107108
}}
108109
/>
109110
<p
@@ -120,6 +121,12 @@ function RegistrationViewWkOrg() {
120121
}
121122

122123
function RegistrationView() {
124+
// If you're already logged in, redirect to the dashboard
125+
const isAuthenticated = useWkSelector((state) => state.activeUser != null);
126+
127+
if (isAuthenticated) {
128+
return <Navigate to="/" />;
129+
}
123130
return features().isWkorgInstance ? <RegistrationViewWkOrg /> : <RegistrationViewGeneric />;
124131
}
125132

frontend/javascripts/admin/auth/start_reset_password_view.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,19 @@ import { Button, Card, Col, Form, Input, Row } from "antd";
33
import Request from "libs/request";
44
import Toast from "libs/toast";
55
import messages from "messages";
6-
import { Link, useHistory } from "react-router-dom";
6+
import { Link, useNavigate } from "react-router-dom";
77
const FormItem = Form.Item;
88

99
function StartResetPasswordView() {
1010
const [form] = Form.useForm();
11-
const history = useHistory();
11+
const navigate = useNavigate();
1212

1313
const onFinish = (formValues: Record<string, any>) => {
1414
Request.sendJSONReceiveJSON("/api/auth/startResetPassword", {
1515
data: formValues,
1616
}).then(() => {
1717
Toast.success(messages["auth.reset_email_notification"]);
18-
history.push("/");
18+
navigate("/");
1919
});
2020
};
2121

frontend/javascripts/admin/auth/verify_email_view.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { useFetch } from "libs/react_helpers";
44
import type { ServerErrorMessage } from "libs/request";
55
import Toast from "libs/toast";
66
import { useEffect } from "react";
7-
import { useHistory } from "react-router-dom";
7+
import { useNavigate, useParams } from "react-router-dom";
88
import { Store } from "viewer/singletons";
99

1010
export const VERIFICATION_ERROR_TOAST_KEY = "verificationError";
@@ -44,8 +44,9 @@ export function showVerificationReminderToast() {
4444
);
4545
}
4646

47-
export default function VerifyEmailView({ token }: { token: string }) {
48-
const history = useHistory();
47+
export default function VerifyEmailView() {
48+
const { token = "" } = useParams();
49+
const navigate = useNavigate();
4950
const [result, exception] = useFetch(
5051
async () => {
5152
try {
@@ -62,7 +63,7 @@ export default function VerifyEmailView({ token }: { token: string }) {
6263
Toast.close(VERIFICATION_ERROR_TOAST_KEY);
6364
}, []);
6465

65-
// biome-ignore lint/correctness/useExhaustiveDependencies: history.push is not needed as a dependency.
66+
// biome-ignore lint/correctness/useExhaustiveDependencies: navigate is not needed as a dependency.
6667
useEffect(() => {
6768
if (result) {
6869
Toast.success("Successfully verified your email.");
@@ -80,7 +81,7 @@ export default function VerifyEmailView({ token }: { token: string }) {
8081
}
8182

8283
if (result || exception) {
83-
history.push("/");
84+
navigate("/");
8485
}
8586
}, [result, exception]);
8687
return (

frontend/javascripts/admin/dataset/dataset_add_remote_view.tsx

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,20 +25,19 @@ import DatasetSettingsDataTab, {
2525
import { FormItemWithInfo, Hideable } from "dashboard/dataset/helper_components";
2626
import FolderSelection from "dashboard/folders/folder_selection";
2727
import { formatScale } from "libs/format_utils";
28+
import { useWkSelector } from "libs/react_hooks";
2829
import { readFileAsText } from "libs/read_file";
2930
import Toast from "libs/toast";
3031
import { jsonStringify } from "libs/utils";
3132
import * as Utils from "libs/utils";
3233
import _ from "lodash";
3334
import messages from "messages";
3435
import React, { useEffect, useState } from "react";
35-
import { useHistory } from "react-router-dom";
36+
import { useNavigate } from "react-router-dom";
3637
import type { APIDataStore } from "types/api_types";
3738
import type { ArbitraryObject } from "types/globals";
3839
import type { DataLayer, DatasourceConfiguration } from "types/schemas/datasource.types";
3940
import { Unicode } from "viewer/constants";
40-
41-
import { useWkSelector } from "libs/react_hooks";
4241
import { Hint } from "viewer/view/action-bar/download_modal_view";
4342
import { dataPrivacyInfo } from "./dataset_upload_view";
4443

@@ -189,7 +188,7 @@ function DatasetAddRemoteView(props: Props) {
189188
const [targetFolderId, setTargetFolderId] = useState<string | null>(null);
190189
const isDatasourceConfigStrFalsy = Form.useWatch("dataSourceJson", form) == null;
191190
const maybeDataLayers = Form.useWatch(["dataSource", "dataLayers"], form);
192-
const history = useHistory();
191+
const navigate = useNavigate();
193192

194193
useEffect(() => {
195194
const params = new URLSearchParams(location.search);
@@ -210,7 +209,7 @@ function DatasetAddRemoteView(props: Props) {
210209
.getFieldError("datasetName")
211210
.filter((error) => error === messages["dataset.name.already_taken"]);
212211
if (maybeDSNameError == null) return;
213-
history.push(
212+
navigate(
214213
`/datasets/${activeUser?.organization}/${form.getFieldValue(["dataSource", "id", "name"])}`,
215214
);
216215
};

0 commit comments

Comments
 (0)