From 0512cf9c3f86cb1ee80395ea5f3cdd21d0e8392f Mon Sep 17 00:00:00 2001 From: Tom Herold Date: Tue, 1 Jul 2025 15:32:20 +0200 Subject: [PATCH 01/30] WIP upgrade to react router v6 --- .../admin/auth/accept_invite_view.tsx | 6 +-- .../admin/auth/finish_reset_password_view.tsx | 6 +-- .../javascripts/admin/auth/login_view.tsx | 8 +-- .../admin/auth/registration_view.tsx | 12 ++--- .../admin/auth/start_reset_password_view.tsx | 6 +-- .../admin/auth/verify_email_view.tsx | 6 +-- .../admin/dataset/dataset_add_remote_view.tsx | 22 ++++---- .../admin/dataset/dataset_add_view.tsx | 25 ++++----- .../admin/dataset/dataset_url_import.tsx | 6 +-- .../admin/project/project_create_view.tsx | 6 +-- .../admin/scripts/script_create_view.tsx | 25 +++------ .../admin/task/task_create_form_view.tsx | 6 +-- .../admin/tasktype/task_type_create_view.tsx | 6 +-- .../admin/voxelytics/task_list_view.tsx | 12 ++--- frontend/javascripts/components/redirect.tsx | 8 +-- frontend/javascripts/navbar.tsx | 52 +++++++------------ 16 files changed, 91 insertions(+), 121 deletions(-) diff --git a/frontend/javascripts/admin/auth/accept_invite_view.tsx b/frontend/javascripts/admin/auth/accept_invite_view.tsx index 958c34a16d7..75584a96e70 100644 --- a/frontend/javascripts/admin/auth/accept_invite_view.tsx +++ b/frontend/javascripts/admin/auth/accept_invite_view.tsx @@ -7,7 +7,7 @@ import { useFetch } from "libs/react_helpers"; import Toast from "libs/toast"; import { location } from "libs/window"; import { useState } from "react"; -import { useHistory } from "react-router-dom"; +import { useNavigate } from "react-router-dom"; import type { APIUser } from "types/api_types"; const { Content } = Layout; @@ -19,7 +19,7 @@ export default function AcceptInviteView({ token: string; activeUser: APIUser | null | undefined; }) { - const history = useHistory(); + const navigate = useNavigate(); const [isAuthenticationModalOpen, setIsAuthenticationModalOpen] = useState(false); const [targetOrganization, exception] = useFetch( async () => { @@ -46,7 +46,7 @@ export default function AcceptInviteView({ targetOrganization != null ? targetOrganization.name || targetOrganization.id : "unknown"; const onSuccessfulJoin = (userJustRegistered: boolean = false) => { - history.push("/dashboard"); + navigate("/dashboard"); if (userJustRegistered) { // Since the user just registered, the organization is already active. diff --git a/frontend/javascripts/admin/auth/finish_reset_password_view.tsx b/frontend/javascripts/admin/auth/finish_reset_password_view.tsx index f8c88ec8d2d..4aa5af23276 100644 --- a/frontend/javascripts/admin/auth/finish_reset_password_view.tsx +++ b/frontend/javascripts/admin/auth/finish_reset_password_view.tsx @@ -3,7 +3,7 @@ import { Button, Card, Col, Form, Input, Row } from "antd"; import Request from "libs/request"; import Toast from "libs/toast"; import messages from "messages"; -import { useHistory } from "react-router-dom"; +import { useNavigate } from "react-router-dom"; const FormItem = Form.Item; const { Password } = Input; type Props = { @@ -12,7 +12,7 @@ type Props = { function FinishResetPasswordView(props: Props) { const [form] = Form.useForm(); - const history = useHistory(); + const navigate = useNavigate(); function onFinish(formValues: Record) { const data = formValues; @@ -27,7 +27,7 @@ function FinishResetPasswordView(props: Props) { data, }).then(() => { Toast.success(messages["auth.reset_pw_confirmation"]); - history.push("/auth/login"); + navigate("/auth/login"); }); } diff --git a/frontend/javascripts/admin/auth/login_view.tsx b/frontend/javascripts/admin/auth/login_view.tsx index 6f50060d055..ca010b7ab7a 100644 --- a/frontend/javascripts/admin/auth/login_view.tsx +++ b/frontend/javascripts/admin/auth/login_view.tsx @@ -1,7 +1,7 @@ import { Card, Col, Row } from "antd"; import * as Utils from "libs/utils"; import window from "libs/window"; -import { useHistory } from "react-router-dom"; +import { useNavigate } from "react-router-dom"; import LoginForm from "./login_form"; type Props = { @@ -9,14 +9,14 @@ type Props = { }; function LoginView({ redirect }: Props) { - const history = useHistory(); + const navigate = useNavigate(); const onLoggedIn = () => { if (!Utils.hasUrlParam("redirectPage")) { if (redirect) { // Use "redirect" prop for internal redirects, e.g. for SecuredRoutes - history.push(redirect); + navigate(redirect); } else { - history.push("/dashboard"); + navigate("/dashboard"); } } else { // Use "redirectPage" URL parameter to cause a full page reload and redirecting to external sites diff --git a/frontend/javascripts/admin/auth/registration_view.tsx b/frontend/javascripts/admin/auth/registration_view.tsx index 591e0ab619d..a48c461ccf9 100644 --- a/frontend/javascripts/admin/auth/registration_view.tsx +++ b/frontend/javascripts/admin/auth/registration_view.tsx @@ -6,11 +6,11 @@ import features from "features"; import Toast from "libs/toast"; import messages from "messages"; import { useEffect, useState } from "react"; -import { Link, useHistory } from "react-router-dom"; +import { Link, useNavigate } from "react-router-dom"; import type { APIOrganization } from "types/api_types"; function RegistrationViewGeneric() { - const history = useHistory(); + const navigate = useNavigate(); const [organization, setOrganization] = useState(null); const [isLoading, setIsLoading] = useState(true); @@ -55,10 +55,10 @@ function RegistrationViewGeneric() { targetOrganization={organization} onRegistered={(isUserLoggedIn?: boolean) => { if (isUserLoggedIn) { - history.goBack(); + navigate(-1); } else { Toast.success(messages["auth.account_created"]); - history.push("/auth/login"); + navigate("/auth/login"); } }} /> @@ -95,7 +95,7 @@ function RegistrationViewGeneric() { } function RegistrationViewWkOrg() { - const history = useHistory(); + const navigate = useNavigate(); return ( @@ -103,7 +103,7 @@ function RegistrationViewWkOrg() {

Sign Up

{ - history.push("/dashboard"); + navigate("/dashboard"); }} />

) => { Request.sendJSONReceiveJSON("/api/auth/startResetPassword", { data: formValues, }).then(() => { Toast.success(messages["auth.reset_email_notification"]); - history.push("/"); + navigate("/"); }); }; diff --git a/frontend/javascripts/admin/auth/verify_email_view.tsx b/frontend/javascripts/admin/auth/verify_email_view.tsx index 0979872f791..e0c13d23dd1 100644 --- a/frontend/javascripts/admin/auth/verify_email_view.tsx +++ b/frontend/javascripts/admin/auth/verify_email_view.tsx @@ -4,7 +4,7 @@ import { useFetch } from "libs/react_helpers"; import type { ServerErrorMessage } from "libs/request"; import Toast from "libs/toast"; import { useEffect } from "react"; -import { useHistory } from "react-router-dom"; +import { useNavigate } from "react-router-dom"; import { Store } from "viewer/singletons"; export const VERIFICATION_ERROR_TOAST_KEY = "verificationError"; @@ -45,7 +45,7 @@ export function showVerificationReminderToast() { } export default function VerifyEmailView({ token }: { token: string }) { - const history = useHistory(); + const navigate = useNavigate(); const [result, exception] = useFetch( async () => { try { @@ -80,7 +80,7 @@ export default function VerifyEmailView({ token }: { token: string }) { } if (result || exception) { - history.push("/"); + navigate("/"); } }, [result, exception]); return ( diff --git a/frontend/javascripts/admin/dataset/dataset_add_remote_view.tsx b/frontend/javascripts/admin/dataset/dataset_add_remote_view.tsx index 6a53102aaae..4a94419ced9 100644 --- a/frontend/javascripts/admin/dataset/dataset_add_remote_view.tsx +++ b/frontend/javascripts/admin/dataset/dataset_add_remote_view.tsx @@ -32,8 +32,6 @@ import * as Utils from "libs/utils"; import _ from "lodash"; import messages from "messages"; import React, { useEffect, useState } from "react"; -import { connect } from "react-redux"; -import { useHistory } from "react-router-dom"; import type { APIDataStore, APIUser } from "types/api_types"; import type { ArbitraryObject } from "types/globals"; import type { DataLayer, DatasourceConfiguration } from "types/schemas/datasource.types"; @@ -41,6 +39,8 @@ import { Unicode } from "viewer/constants"; import type { WebknossosState } from "viewer/store"; import { Hint } from "viewer/view/action-bar/download_modal_view"; import { dataPrivacyInfo } from "./dataset_upload_view"; +import { useWkSelector } from "libs/react_hooks"; +import { useNavigate } from "react-router-dom"; const FormItem = Form.Item; const RadioGroup = Radio.Group; @@ -179,8 +179,11 @@ export function GoogleAuthFormItem({ ); } -function DatasetAddRemoteView(props: Props) { - const { activeUser, onAdded, datastores, defaultDatasetUrl } = props; +function DatasetAddRemoteView(props: OwnProps) { + const { activeUser } = useWkSelector((state) => ({ + activeUser: state.activeUser, + })); + const { onAdded, datastores, defaultDatasetUrl } = props; const uploadableDatastores = datastores.filter((datastore) => datastore.allowsUpload); const hasOnlyOneDatastoreOrNone = uploadableDatastores.length <= 1; @@ -192,7 +195,7 @@ function DatasetAddRemoteView(props: Props) { const [targetFolderId, setTargetFolderId] = useState(null); const isDatasourceConfigStrFalsy = Form.useWatch("dataSourceJson", form) == null; const maybeDataLayers = Form.useWatch(["dataSource", "dataLayers"], form); - const history = useHistory(); + const navigate = useNavigate(); useEffect(() => { const params = new URLSearchParams(location.search); @@ -213,7 +216,7 @@ function DatasetAddRemoteView(props: Props) { .getFieldError("datasetName") .filter((error) => error === messages["dataset.name.already_taken"]); if (maybeDSNameError == null) return; - history.push( + navigate( `/datasets/${activeUser?.organization}/${form.getFieldValue(["dataSource", "id", "name"])}`, ); }; @@ -729,9 +732,4 @@ function AddRemoteLayer({ ); } -const mapStateToProps = (state: WebknossosState): StateProps => ({ - activeUser: state.activeUser, -}); - -const connector = connect(mapStateToProps); -export default connector(DatasetAddRemoteView); +export default DatasetAddRemoteView; diff --git a/frontend/javascripts/admin/dataset/dataset_add_view.tsx b/frontend/javascripts/admin/dataset/dataset_add_view.tsx index 6e7c13d2f70..8e56bae0254 100644 --- a/frontend/javascripts/admin/dataset/dataset_add_view.tsx +++ b/frontend/javascripts/admin/dataset/dataset_add_view.tsx @@ -8,7 +8,7 @@ import { useFetch } from "libs/react_helpers"; import { useWkSelector } from "libs/react_hooks"; import React, { useState } from "react"; import { connect } from "react-redux"; -import { useHistory } from "react-router-dom"; +import { useNavigate } from "react-router-dom"; import type { APIDataStore } from "types/api_types"; import { getReadableURLPart } from "viewer/model/accessors/dataset_accessor"; import { enforceActiveUser } from "viewer/model/accessors/user_accessor"; @@ -32,7 +32,7 @@ const addTypeToVerb: Record = { }; function DatasetAddView() { - const history = useHistory(); + const navigate = useNavigate(); const datastores = useFetch(getDatastores, [], []); const [datasetId, setDatasetId] = useState(""); const [uploadedDatasetName, setUploadedDatasetName] = useState(""); @@ -64,7 +64,7 @@ function DatasetAddView() { datasetId, uploadedDatasetName, setDatasetId, - history, + navigate, ); }; @@ -261,12 +261,7 @@ function VoxelyticsBanner() { ); } -const mapStateToProps = (state: WebknossosState) => ({ - activeUser: enforceActiveUser(state.activeUser), -}); - -const connector = connect(mapStateToProps); -export default connector(DatasetAddView); +export default DatasetAddView; const getPostUploadModal = ( datasetNeedsConversion: boolean, @@ -274,7 +269,7 @@ const getPostUploadModal = ( datasetId: string, uploadedDatasetName: string, setDatasetId: (arg0: string) => void, - history: ReturnType, + navigate: ReturnType, ) => { return ( {datasetNeedsConversion ? ( - - + ) : ( - + )} diff --git a/frontend/javascripts/admin/dataset/dataset_url_import.tsx b/frontend/javascripts/admin/dataset/dataset_url_import.tsx index 49f318f605a..244d01cd63c 100644 --- a/frontend/javascripts/admin/dataset/dataset_url_import.tsx +++ b/frontend/javascripts/admin/dataset/dataset_url_import.tsx @@ -3,15 +3,15 @@ import { getDatastores } from "admin/rest_api"; import { useFetch } from "libs/react_helpers"; import * as Utils from "libs/utils"; import _ from "lodash"; -import { useHistory } from "react-router-dom"; +import { useNavigate } from "react-router-dom"; export function DatasetURLImport() { - const history = useHistory(); + const navigate = useNavigate(); const datastores = useFetch(async () => await getDatastores(), null, []); const params = Utils.getUrlParamsObject(); const datasetUri = _.has(params, "url") ? params.url : null; const handleDatasetAdded = async (addedDatasetId: string): Promise => { - history.push(`/datasets/${addedDatasetId}/view`); + navigate(`/datasets/${addedDatasetId}/view`); }; return datastores != null ? ( diff --git a/frontend/javascripts/admin/project/project_create_view.tsx b/frontend/javascripts/admin/project/project_create_view.tsx index 8fdbb69f40b..06970ed1ea3 100644 --- a/frontend/javascripts/admin/project/project_create_view.tsx +++ b/frontend/javascripts/admin/project/project_create_view.tsx @@ -8,7 +8,7 @@ import { import { Button, Card, Checkbox, Form, Input, InputNumber, Select } from "antd"; import { useWkSelector } from "libs/react_hooks"; import { useEffect, useState } from "react"; -import { useHistory } from "react-router-dom"; +import { useNavigate } from "react-router-dom"; import type { APITeam, APIUser } from "types/api_types"; import { enforceActiveUser } from "viewer/model/accessors/user_accessor"; import { FormItemWithInfo } from "../../dashboard/dataset/helper_components"; @@ -23,7 +23,7 @@ function ProjectCreateView({ projectId }: Props) { const [users, setUsers] = useState([]); const [isFetchingData, setIsFetchingData] = useState(false); const [form] = Form.useForm(); - const history = useHistory(); + const navigate = useNavigate(); const activeUser = useWkSelector((state) => enforceActiveUser(state.activeUser)); useEffect(() => { fetchData(); @@ -59,7 +59,7 @@ function ProjectCreateView({ projectId }: Props) { await createProject(formValues); } - history.push("/projects"); + navigate("/projects"); }; const isEditMode = projectId != null; diff --git a/frontend/javascripts/admin/scripts/script_create_view.tsx b/frontend/javascripts/admin/scripts/script_create_view.tsx index 24ddbe8ac56..6021ef3d6db 100644 --- a/frontend/javascripts/admin/scripts/script_create_view.tsx +++ b/frontend/javascripts/admin/scripts/script_create_view.tsx @@ -1,23 +1,19 @@ import { createScript, getScript, getTeamManagerOrAdminUsers, updateScript } from "admin/rest_api"; import { Button, Card, Form, Input, Select } from "antd"; import { useEffect, useState } from "react"; -import { connect } from "react-redux"; -import { useHistory } from "react-router-dom"; +import { useNavigate } from "react-router-dom"; import type { APIUser } from "types/api_types"; import { enforceActiveUser } from "viewer/model/accessors/user_accessor"; -import type { WebknossosState } from "viewer/store"; +import { useWkSelector } from "libs/react_hooks"; const FormItem = Form.Item; -type OwnProps = { +type Props = { scriptId?: string | null | undefined; }; -type StateProps = { - activeUser: APIUser; -}; -type Props = OwnProps & StateProps; -function ScriptCreateView({ scriptId, activeUser }: Props) { - const history = useHistory(); +function ScriptCreateView({ scriptId }: Props) { + const navigate = useNavigate(); + const activeUser = useWkSelector((state) => enforceActiveUser(state.activeUser)); const [users, setUsers] = useState([]); const [isFetchingData, setIsFetchingData] = useState(false); const [form] = Form.useForm(); @@ -51,7 +47,7 @@ function ScriptCreateView({ scriptId, activeUser }: Props) { await createScript(formValues); } - history.push("/scripts"); + navigate("/scripts"); }; const titlePrefix = scriptId ? "Update" : "Create"; @@ -127,9 +123,4 @@ function ScriptCreateView({ scriptId, activeUser }: Props) { ); } -const mapStateToProps = (state: WebknossosState): StateProps => ({ - activeUser: enforceActiveUser(state.activeUser), -}); - -const connector = connect(mapStateToProps); -export default connector(ScriptCreateView); +export default ScriptCreateView; diff --git a/frontend/javascripts/admin/task/task_create_form_view.tsx b/frontend/javascripts/admin/task/task_create_form_view.tsx index 2c7dacc0cd2..b42443bf053 100644 --- a/frontend/javascripts/admin/task/task_create_form_view.tsx +++ b/frontend/javascripts/admin/task/task_create_form_view.tsx @@ -42,7 +42,7 @@ import { Vector3Input, Vector6Input } from "libs/vector_input"; import _ from "lodash"; import messages from "messages"; import React, { useEffect, useState } from "react"; -import { useHistory } from "react-router-dom"; +import { useNavigate } from "react-router-dom"; import type { APIDataset, APIProject, APIScript, APITask, APITaskType } from "types/api_types"; import type { Vector3, Vector6 } from "viewer/constants"; import type { BoundingBoxObject } from "viewer/store"; @@ -308,7 +308,7 @@ type FormValues = { }; function TaskCreateFormView({ taskId }: Props) { - const history = useHistory(); + const navigate = useNavigate(); const { modal } = App.useApp(); const [form] = Form.useForm(); @@ -381,7 +381,7 @@ function TaskCreateFormView({ taskId }: Props) { boundingBox, }; const confirmedTask = await updateTask(taskId, newTask); - history.push(`/tasks/${confirmedTask.id}`); + navigate(`/tasks/${confirmedTask.id}`); } else { setIsUploading(true); // or create a new one either from the form values or with an NML file diff --git a/frontend/javascripts/admin/tasktype/task_type_create_view.tsx b/frontend/javascripts/admin/tasktype/task_type_create_view.tsx index 2abd94f7dc9..87d45e5940d 100644 --- a/frontend/javascripts/admin/tasktype/task_type_create_view.tsx +++ b/frontend/javascripts/admin/tasktype/task_type_create_view.tsx @@ -9,7 +9,7 @@ import { useFetch } from "libs/react_helpers"; import { jsonStringify } from "libs/utils"; import _ from "lodash"; import { useEffect, useState } from "react"; -import { useHistory } from "react-router-dom"; +import { useNavigate } from "react-router-dom"; import { type APIAllowedMode, type APIMagRestrictions, @@ -74,7 +74,7 @@ function isMaximumMagnificationSmallerThenMinRule(value: number | undefined, min } function TaskTypeCreateView({ taskTypeId }: Props) { - const history = useHistory(); + const navigate = useNavigate(); const [useRecommendedConfiguration, setUseRecommendedConfiguration] = useState(false); const [isFetchingData, setIsFetchingData] = useState(true); const [form] = Form.useForm(); @@ -165,7 +165,7 @@ function TaskTypeCreateView({ taskTypeId }: Props) { await createTaskType(newTaskType); } - history.push("/taskTypes"); + navigate("/taskTypes"); } function onChangeUseRecommendedConfiguration(useRecommendedConfiguration: boolean) { diff --git a/frontend/javascripts/admin/voxelytics/task_list_view.tsx b/frontend/javascripts/admin/voxelytics/task_list_view.tsx index 075ae0f3796..faeb643f1ab 100644 --- a/frontend/javascripts/admin/voxelytics/task_list_view.tsx +++ b/frontend/javascripts/admin/voxelytics/task_list_view.tsx @@ -38,7 +38,7 @@ import { } from "libs/format_utils"; import { useSearchParams, useUpdateEvery, useWkSelector } from "libs/react_hooks"; import { notEmpty } from "libs/utils"; -import { Link, useHistory, useLocation, useParams } from "react-router-dom"; +import { Link, useLocation, useParams, useNavigate } from "react-router-dom"; import { VoxelyticsRunState, type VoxelyticsTaskConfig, @@ -264,7 +264,7 @@ export default function TaskListView({ const { modal } = App.useApp(); const [searchQuery, setSearchQuery] = useState(""); const { runId } = useSearchParams(); - const history = useHistory(); + const navigate = useNavigate(); // expandedTask = state of the collapsible list const [expandedTasks, setExpandedTasks] = useState>([]); @@ -433,7 +433,7 @@ export default function TaskListView({ onOk: async () => { try { await deleteWorkflow(report.workflow.hash); - history.push("/workflows"); + navigate("/workflows"); message.success("Workflow report deleted."); } catch (error) { console.error(error); @@ -678,10 +678,10 @@ export default function TaskListView({