From f717b38b82873bdc359c1387f45a9aab185a387c Mon Sep 17 00:00:00 2001 From: Ryo Okubo Date: Sun, 20 Oct 2024 00:39:29 +0900 Subject: [PATCH 1/4] Upgrade react-router to v6 --- frontend/src/App.tsx | 8 +- frontend/src/AppRouter.tsx | 199 +++++++-------- frontend/src/ErrorHandler.tsx | 18 +- frontend/src/components/common/ImportForm.tsx | 6 +- .../components/entity/EntityControlMenu.tsx | 8 +- .../entry/AdvancedSearchJoinModal.tsx | 6 +- .../components/entry/AdvancedSearchModal.tsx | 8 +- .../src/components/entry/AttributeValue.tsx | 2 +- .../src/components/entry/EntryControlMenu.tsx | 8 +- .../src/components/entry/EntryHistoryList.tsx | 8 +- frontend/src/components/entry/EntryList.tsx | 6 +- .../components/entry/RestorableEntryList.tsx | 10 +- .../entry/SearchResultsTableHead.tsx | 6 +- .../src/components/group/GroupControlMenu.tsx | 10 +- frontend/src/components/job/JobList.tsx | 8 +- frontend/src/components/role/RoleList.tsx | 8 +- .../src/components/user/UserControlMenu.tsx | 8 +- .../src/components/user/UserList.test.tsx | 39 +-- frontend/src/components/user/UserList.tsx | 6 +- .../components/user/UserPasswordFormModal.tsx | 10 +- frontend/src/hooks/usePage.tsx | 8 +- frontend/src/hooks/usePrompt.tsx | 19 ++ frontend/src/hooks/useSimpleSearch.tsx | 8 +- frontend/src/hooks/useTypedParams.tsx | 13 +- frontend/src/pages/ACLEditPage.tsx | 21 +- frontend/src/pages/DashboardPage.tsx | 6 +- frontend/src/pages/EntityEditPage.test.tsx | 5 +- frontend/src/pages/EntityEditPage.tsx | 23 +- frontend/src/pages/EntityListPage.tsx | 6 +- frontend/src/pages/EntryCopyPage.tsx | 19 +- frontend/src/pages/EntryDetailsPage.test.tsx | 2 +- frontend/src/pages/EntryDetailsPage.tsx | 8 +- frontend/src/pages/EntryEditPage.test.tsx | 2 +- frontend/src/pages/EntryEditPage.tsx | 23 +- .../src/pages/EntryHistoryListPage.test.tsx | 2 +- frontend/src/pages/GroupEditPage.tsx | 19 +- frontend/src/pages/LoginPage.tsx | 16 +- frontend/src/pages/RoleEditPage.tsx | 19 +- frontend/src/pages/TriggerEditPage.test.tsx | 2 +- frontend/src/pages/TriggerEditPage.tsx | 18 +- frontend/src/pages/TriggerListPage.tsx | 8 +- frontend/src/pages/UserEditPage.tsx | 20 +- package-lock.json | 235 ++++-------------- package.json | 3 +- 44 files changed, 391 insertions(+), 496 deletions(-) create mode 100644 frontend/src/hooks/usePrompt.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 19fa737d5..302bc1c81 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -4,6 +4,8 @@ import { createRoot } from "react-dom/client"; import { AironeSnackbarProvider } from "AironeSnackbarProvider"; import { AppRouter } from "AppRouter"; +import { CheckTermsService } from "CheckTermsService"; +import { ErrorHandler } from "ErrorHandler"; import { theme } from "Theme"; import "i18n/config"; @@ -11,7 +13,11 @@ const App: FC = () => { return ( - + + + + + ); diff --git a/frontend/src/AppRouter.tsx b/frontend/src/AppRouter.tsx index 237273f98..7d1ebdb0c 100644 --- a/frontend/src/AppRouter.tsx +++ b/frontend/src/AppRouter.tsx @@ -1,8 +1,12 @@ import React, { FC } from "react"; -import { RouteComponentProps } from "react-router"; -import { Route, BrowserRouter as Router, Switch } from "react-router-dom"; +import { + createBrowserRouter, + createRoutesFromElements, + Outlet, + Route, + RouterProvider, +} from "react-router-dom"; -import { ErrorHandler } from "./ErrorHandler"; import { ACLHistoryPage } from "./pages/ACLHistoryPage"; import { EntryCopyPage } from "./pages/EntryCopyPage"; import { EntryDetailsPage } from "./pages/EntryDetailsPage"; @@ -11,7 +15,6 @@ import { NotFoundErrorPage } from "./pages/NotFoundErrorPage"; import { RoleEditPage } from "./pages/RoleEditPage"; import { RoleListPage } from "./pages/RoleListPage"; -import { CheckTermsService } from "CheckTermsService"; import { aclHistoryPath, aclPath, @@ -68,110 +71,100 @@ interface Props { customRoutes?: { path: string; routePath: string; - component?: FC; - render?: ( - props: RouteComponentProps<{ [K: string]: string | undefined }> - ) => React.ReactNode; + component?: React.ReactNode; }[]; } export const AppRouter: FC = ({ customRoutes }) => { - return ( - - - - - - + const router = createBrowserRouter( + createRoutesFromElements( + + } /> +
- - {customRoutes && - customRoutes.map((r) => ( - - - - - - ))} + + + } + > + {customRoutes && + customRoutes.map((r) => ( + + {r.component && ( + + )} + + ))} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + } /> + } + /> + } /> + } + /> + } + /> + } + /> + } + /> + } + /> + } + /> + } + /> + } /> + } + /> + } /> + } /> + } + /> + } /> + } /> + } /> + } /> + } /> + } + /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + ) ); + + return ; }; diff --git a/frontend/src/ErrorHandler.tsx b/frontend/src/ErrorHandler.tsx index 23aac0100..8a268ee48 100644 --- a/frontend/src/ErrorHandler.tsx +++ b/frontend/src/ErrorHandler.tsx @@ -9,7 +9,7 @@ import { import { styled } from "@mui/material/styles"; import React, { FC, useCallback, useEffect, useState } from "react"; import { ErrorBoundary, FallbackProps } from "react-error-boundary"; -import { useHistory } from "react-router-dom"; +import { useLocation, useNavigate } from "react-router-dom"; import { useError } from "react-use"; import { ForbiddenErrorPage } from "./pages/ForbiddenErrorPage"; @@ -46,16 +46,16 @@ interface GenericErrorProps { } const GenericError: FC = ({ children }) => { - const history = useHistory(); + const navigate = useNavigate(); const [open, setOpen] = useState(true); const handleGoToTop = useCallback(() => { - history.replace(topPath()); - }, [history]); + navigate(topPath(), { replace: true }); + }, [navigate]); const handleReload = useCallback(() => { - history.go(0); - }, [history]); + navigate(0); + }, [navigate]); return ( setOpen(false)}> @@ -96,11 +96,11 @@ const GenericError: FC = ({ children }) => { }; const ErrorFallback: FC = ({ error, resetErrorBoundary }) => { - const history = useHistory(); + const location = useLocation(); - history.listen(() => { + useEffect(() => { resetErrorBoundary(); - }); + }, [location, resetErrorBoundary]); switch (error.name) { case ForbiddenError.errorName: diff --git a/frontend/src/components/common/ImportForm.tsx b/frontend/src/components/common/ImportForm.tsx index a3afdd3a4..a3ffc2b40 100644 --- a/frontend/src/components/common/ImportForm.tsx +++ b/frontend/src/components/common/ImportForm.tsx @@ -2,7 +2,7 @@ import { Box, Button, Input, Typography } from "@mui/material"; import Encoding from "encoding-japanese"; import { useSnackbar } from "notistack"; import React, { ChangeEvent, FC, useState } from "react"; -import { useHistory } from "react-router-dom"; +import { useNavigate } from "react-router-dom"; import { isResponseError, @@ -15,7 +15,7 @@ interface Props { } export const ImportForm: FC = ({ handleImport, handleCancel }) => { - const history = useHistory(); + const navigate = useNavigate(); const [file, setFile] = useState(); const [errorMessage, setErrorMessage] = useState(""); const { enqueueSnackbar } = useSnackbar(); @@ -43,7 +43,7 @@ export const ImportForm: FC = ({ handleImport, handleCancel }) => { try { await handleImport(fileReader.result); - history.go(0); + navigate(0); } catch (e) { if (e instanceof Error && isResponseError(e)) { const reportableError = await toReportableNonFieldErrors(e); diff --git a/frontend/src/components/entity/EntityControlMenu.tsx b/frontend/src/components/entity/EntityControlMenu.tsx index 2b57109d5..27a2924ac 100644 --- a/frontend/src/components/entity/EntityControlMenu.tsx +++ b/frontend/src/components/entity/EntityControlMenu.tsx @@ -8,7 +8,7 @@ import { } from "@mui/material"; import { useSnackbar } from "notistack"; import React, { FC } from "react"; -import { Link, useHistory } from "react-router-dom"; +import { Link, useNavigate } from "react-router-dom"; import { RateLimitedClickable } from "../common/RateLimitedClickable"; @@ -43,7 +43,7 @@ export const EntityControlMenu: FC = ({ setToggle, }) => { const { enqueueSnackbar } = useSnackbar(); - const history = useHistory(); + const navigate = useNavigate(); const handleDelete = async (entityId: number) => { await aironeApiClient @@ -53,8 +53,8 @@ export const EntityControlMenu: FC = ({ variant: "success", }); // A magic to reload the entity list with keeping snackbar - history.replace(topPath()); - history.replace(entitiesPath()); + navigate(topPath(), { replace: true }); + navigate(entitiesPath(), { replace: true }); setToggle && setToggle(); }) .catch(() => { diff --git a/frontend/src/components/entry/AdvancedSearchJoinModal.tsx b/frontend/src/components/entry/AdvancedSearchJoinModal.tsx index b70bd0cc7..8c2f359c5 100644 --- a/frontend/src/components/entry/AdvancedSearchJoinModal.tsx +++ b/frontend/src/components/entry/AdvancedSearchJoinModal.tsx @@ -4,7 +4,7 @@ import { } from "@dmm-com/airone-apiclient-typescript-fetch"; import { Autocomplete, Box, Button, TextField } from "@mui/material"; import React, { FC, useState } from "react"; -import { useHistory } from "react-router-dom"; +import { useNavigate } from "react-router-dom"; import { AironeModal } from "components/common/AironeModal"; import { useAsyncWithThrow } from "hooks/useAsyncWithThrow"; @@ -28,7 +28,7 @@ export const AdvancedSearchJoinModal: FC = ({ handleClose, refreshSearchResults, }) => { - const history = useHistory(); + const navigate = useNavigate(); // This is join attributes that have been already been selected before. const currentAttrInfo: AdvancedSearchJoinAttrInfo | undefined = joinAttrs.find((attr) => attr.name === targetAttrname); @@ -73,7 +73,7 @@ export const AdvancedSearchJoinModal: FC = ({ refreshSearchResults(); // Update Page URL parameters - history.push({ + navigate({ pathname: location.pathname, search: "?" + params.toString(), }); diff --git a/frontend/src/components/entry/AdvancedSearchModal.tsx b/frontend/src/components/entry/AdvancedSearchModal.tsx index d1f4c5029..7698779e5 100644 --- a/frontend/src/components/entry/AdvancedSearchModal.tsx +++ b/frontend/src/components/entry/AdvancedSearchModal.tsx @@ -4,7 +4,7 @@ import { } from "@dmm-com/airone-apiclient-typescript-fetch"; import { Box, Autocomplete, Checkbox, TextField, Button } from "@mui/material"; import React, { Dispatch, FC, useState, SetStateAction } from "react"; -import { useHistory } from "react-router-dom"; +import { useNavigate } from "react-router-dom"; import { formatAdvancedSearchParams } from "../../services/entry/AdvancedSearch"; import { AironeModal } from "../common/AironeModal"; @@ -24,7 +24,7 @@ export const AdvancedSearchModal: FC = ({ initialAttrNames, attrInfos, }) => { - const history = useHistory(); + const navigate = useNavigate(); const params = new URLSearchParams(location.search); const [selectedAttrNames, setSelectedAttrNames] = useState(initialAttrNames); @@ -53,11 +53,11 @@ export const AdvancedSearchModal: FC = ({ }); // Update Page URL parameters - history.push({ + navigate({ pathname: location.pathname, search: "?" + params.toString(), }); - history.go(0); + navigate(0); }; return ( diff --git a/frontend/src/components/entry/AttributeValue.tsx b/frontend/src/components/entry/AttributeValue.tsx index a39993933..f67785bb4 100644 --- a/frontend/src/components/entry/AttributeValue.tsx +++ b/frontend/src/components/entry/AttributeValue.tsx @@ -93,7 +93,7 @@ const ElemGroup: FC<{ attrValue: EntryAttributeValueGroup | undefined }> = ({ attrValue, }) => { return attrValue ? ( - + {attrValue.name} ) : ( diff --git a/frontend/src/components/entry/EntryControlMenu.tsx b/frontend/src/components/entry/EntryControlMenu.tsx index 74964a3d1..cbfa5e671 100644 --- a/frontend/src/components/entry/EntryControlMenu.tsx +++ b/frontend/src/components/entry/EntryControlMenu.tsx @@ -9,7 +9,7 @@ import { } from "@mui/material"; import { useSnackbar } from "notistack"; import React, { FC } from "react"; -import { Link, useHistory } from "react-router-dom"; +import { Link, useNavigate } from "react-router-dom"; import { entryEditPath, @@ -54,7 +54,7 @@ export const EntryControlMenu: FC = ({ customACLHistoryPath, }) => { const { enqueueSnackbar } = useSnackbar(); - const history = useHistory(); + const navigate = useNavigate(); const handleDelete = async (entryId: number) => { try { @@ -63,8 +63,8 @@ export const EntryControlMenu: FC = ({ variant: "success", }); setToggle && setToggle(); - history.replace(topPath()); - history.replace(entityEntriesPath(entityId)); + navigate(topPath(), { replace: true }); + navigate(entityEntriesPath(entityId), { replace: true }); } catch (e) { enqueueSnackbar("アイテムの削除が失敗しました", { variant: "error", diff --git a/frontend/src/components/entry/EntryHistoryList.tsx b/frontend/src/components/entry/EntryHistoryList.tsx index a4114bb4e..584e408d4 100644 --- a/frontend/src/components/entry/EntryHistoryList.tsx +++ b/frontend/src/components/entry/EntryHistoryList.tsx @@ -12,7 +12,7 @@ import { import { styled } from "@mui/material/styles"; import { useSnackbar } from "notistack"; import React, { FC, useCallback } from "react"; -import { useHistory } from "react-router-dom"; +import { useNavigate } from "react-router-dom"; import { AttributeValue } from "./AttributeValue"; @@ -56,7 +56,7 @@ export const EntryHistoryList: FC = ({ changePage, }) => { const { enqueueSnackbar } = useSnackbar(); - const history = useHistory(); + const navigate = useNavigate(); const handleRestore = useCallback(async (prevAttrValueId: number) => { try { @@ -64,8 +64,8 @@ export const EntryHistoryList: FC = ({ enqueueSnackbar(`変更の復旧が完了しました`, { variant: "success", }); - history.replace(topPath()); - history.replace(showEntryHistoryPath(entityId, entryId)); + navigate(topPath(), { replace: true }); + navigate(showEntryHistoryPath(entityId, entryId), { replace: true }); } catch (e) { enqueueSnackbar(`変更の復旧が失敗しました`, { variant: "error", diff --git a/frontend/src/components/entry/EntryList.tsx b/frontend/src/components/entry/EntryList.tsx index cb068b3fc..9efd49e8f 100644 --- a/frontend/src/components/entry/EntryList.tsx +++ b/frontend/src/components/entry/EntryList.tsx @@ -1,7 +1,7 @@ import AddIcon from "@mui/icons-material/Add"; import { Box, Button, Grid } from "@mui/material"; import React, { FC, useState } from "react"; -import { Link, useHistory, useLocation } from "react-router-dom"; +import { Link, useNavigate, useLocation } from "react-router-dom"; import { EntryListCard } from "./EntryListCard"; @@ -22,7 +22,7 @@ interface Props { export const EntryList: FC = ({ entityId, canCreateEntry = true }) => { const location = useLocation(); - const history = useHistory(); + const navigate = useNavigate(); const [page, changePage] = usePage(); @@ -40,7 +40,7 @@ export const EntryList: FC = ({ entityId, canCreateEntry = true }) => { changePage(1); setQuery(newQuery ?? ""); - history.push({ + navigate({ pathname: location.pathname, search: newQuery ? `?query=${newQuery}` : "", }); diff --git a/frontend/src/components/entry/RestorableEntryList.tsx b/frontend/src/components/entry/RestorableEntryList.tsx index e5cac77c1..9b89a4095 100644 --- a/frontend/src/components/entry/RestorableEntryList.tsx +++ b/frontend/src/components/entry/RestorableEntryList.tsx @@ -20,7 +20,7 @@ import { import { styled } from "@mui/material/styles"; import { useSnackbar } from "notistack"; import React, { FC, useState } from "react"; -import { useHistory, useLocation } from "react-router-dom"; +import { useNavigate, useLocation } from "react-router-dom"; import { EntryAttributes } from "./EntryAttributes"; @@ -101,7 +101,7 @@ interface Props { export const RestorableEntryList: FC = ({ entityId }) => { const location = useLocation(); - const history = useHistory(); + const navigate = useNavigate(); const { enqueueSnackbar } = useSnackbar(); const [page, changePage] = usePage(); @@ -129,7 +129,7 @@ export const RestorableEntryList: FC = ({ entityId }) => { changePage(1); setQuery(newQuery ?? ""); - history.push({ + navigate({ pathname: location.pathname, search: newQuery ? `?query=${newQuery}` : "", }); @@ -142,8 +142,8 @@ export const RestorableEntryList: FC = ({ entityId }) => { enqueueSnackbar("アイテムの復旧が完了しました", { variant: "success", }); - history.replace(topPath()); - history.replace(restoreEntryPath(entityId, keyword)); + navigate(topPath(), { replace: true }); + navigate(restoreEntryPath(entityId, keyword), { replace: true }); }) .catch(() => { enqueueSnackbar("アイテムの復旧が失敗しました", { diff --git a/frontend/src/components/entry/SearchResultsTableHead.tsx b/frontend/src/components/entry/SearchResultsTableHead.tsx index 286a9b3e9..c174e7175 100644 --- a/frontend/src/components/entry/SearchResultsTableHead.tsx +++ b/frontend/src/components/entry/SearchResultsTableHead.tsx @@ -22,7 +22,7 @@ import React, { useReducer, useState, } from "react"; -import { useHistory, useLocation } from "react-router-dom"; +import { useNavigate, useLocation } from "react-router-dom"; import { AdvancedSearchJoinModal } from "./AdvancedSearchJoinModal"; import { SearchResultControlMenu } from "./SearchResultControlMenu"; @@ -75,7 +75,7 @@ export const SearchResultsTableHead: FC = ({ refreshSearchResults, }) => { const location = useLocation(); - const history = useHistory(); + const navigate = useNavigate(); const [entryFilter, entryFilterDispatcher] = useReducer( ( @@ -170,7 +170,7 @@ export const SearchResultsTableHead: FC = ({ } // simply reload with the new params - history.push({ + navigate({ pathname: location.pathname, search: "?" + newParams.toString(), }); diff --git a/frontend/src/components/group/GroupControlMenu.tsx b/frontend/src/components/group/GroupControlMenu.tsx index 3e5274302..d02ab5fd9 100644 --- a/frontend/src/components/group/GroupControlMenu.tsx +++ b/frontend/src/components/group/GroupControlMenu.tsx @@ -9,7 +9,7 @@ import { } from "@mui/material"; import { useSnackbar } from "notistack"; import React, { FC, useCallback } from "react"; -import { Link, useHistory } from "react-router-dom"; +import { Link, useNavigate } from "react-router-dom"; import { groupPath, groupsPath, topPath } from "Routes"; import { Confirmable } from "components/common/Confirmable"; @@ -29,7 +29,7 @@ export const GroupControlMenu: FC = ({ setToggle, }) => { const { enqueueSnackbar } = useSnackbar(); - const history = useHistory(); + const navigate = useNavigate(); const handleDelete = useCallback(async () => { try { @@ -37,15 +37,15 @@ export const GroupControlMenu: FC = ({ enqueueSnackbar(`グループの削除が完了しました`, { variant: "success", }); - history.replace(topPath()); - history.replace(groupsPath()); + navigate(topPath(), { replace: true }); + navigate(groupsPath(), { replace: true }); setToggle && setToggle(); } catch (e) { enqueueSnackbar("グループの削除が失敗しました", { variant: "error", }); } - }, [history, enqueueSnackbar, groupId]); + }, [navigate, enqueueSnackbar, groupId]); return ( = ({ jobs }) => { - const history = useHistory(); + const navigate = useNavigate(); const [encodes, setEncodes] = useState<{ [key: number]: string; @@ -115,12 +115,12 @@ export const JobList: FC = ({ jobs }) => { const handleRerun = async (jobId: number) => { await aironeApiClient.rerunJob(jobId); - history.go(0); + navigate(0); }; const handleCancel = async (jobId: number) => { await aironeApiClient.cancelJob(jobId); - history.go(0); + navigate(0); }; return ( diff --git a/frontend/src/components/role/RoleList.tsx b/frontend/src/components/role/RoleList.tsx index 819209039..ccb32fd2d 100644 --- a/frontend/src/components/role/RoleList.tsx +++ b/frontend/src/components/role/RoleList.tsx @@ -18,7 +18,7 @@ import { OverridableComponent } from "@mui/material/OverridableComponent"; import { styled } from "@mui/material/styles"; import { useSnackbar } from "notistack"; import React, { FC, useState } from "react"; -import { Link, useHistory } from "react-router-dom"; +import { Link, useNavigate } from "react-router-dom"; import { useAsyncWithThrow } from "../../hooks/useAsyncWithThrow"; import { aironeApiClient } from "../../repository/AironeApiClient"; @@ -40,7 +40,7 @@ const StyledIconButton = styled(IconButton)(({ theme }) => ({ })) as OverridableComponent>; export const RoleList: FC = ({}) => { - const history = useHistory(); + const navigate = useNavigate(); const { enqueueSnackbar } = useSnackbar(); const [toggle, setToggle] = useState(false); @@ -54,8 +54,8 @@ export const RoleList: FC = ({}) => { enqueueSnackbar(`ロールの削除が完了しました`, { variant: "success", }); - history.replace(topPath()); - history.replace(rolesPath()); + navigate(topPath(), { replace: true }); + navigate(rolesPath(), { replace: true }); setToggle(!toggle); } catch (e) { enqueueSnackbar("ロールの削除が失敗しました", { diff --git a/frontend/src/components/user/UserControlMenu.tsx b/frontend/src/components/user/UserControlMenu.tsx index 06b56b5f1..b19463e89 100644 --- a/frontend/src/components/user/UserControlMenu.tsx +++ b/frontend/src/components/user/UserControlMenu.tsx @@ -10,7 +10,7 @@ import { } from "@mui/material"; import { useSnackbar } from "notistack"; import React, { FC, useState } from "react"; -import { useHistory } from "react-router-dom"; +import { useNavigate } from "react-router-dom"; import { UserPasswordFormModal } from "./UserPasswordFormModal"; @@ -33,7 +33,7 @@ export const UserControlMenu: FC = ({ setToggle, }) => { const { enqueueSnackbar } = useSnackbar(); - const history = useHistory(); + const navigate = useNavigate(); const [openModal, setOpenModal] = useState(false); @@ -51,8 +51,8 @@ export const UserControlMenu: FC = ({ enqueueSnackbar(`ユーザ(${user.username})の削除が完了しました`, { variant: "success", }); - history.replace(topPath()); - history.replace(usersPath()); + navigate(topPath(), { replace: true }); + navigate(usersPath(), { replace: true }); setToggle && setToggle(); } catch (e) { enqueueSnackbar("ユーザの削除が失敗しました", { diff --git a/frontend/src/components/user/UserList.test.tsx b/frontend/src/components/user/UserList.test.tsx index e925ca7a8..f4975425e 100644 --- a/frontend/src/components/user/UserList.test.tsx +++ b/frontend/src/components/user/UserList.test.tsx @@ -10,9 +10,8 @@ import { waitFor, waitForElementToBeRemoved, } from "@testing-library/react"; -import { createMemoryHistory } from "history"; import React from "react"; -import { Router } from "react-router-dom"; +import { createMemoryRouter, RouterProvider } from "react-router-dom"; import { TestWrapper, TestWrapperWithoutRoutes } from "TestWrapper"; import { UserList } from "components/user/UserList"; @@ -62,14 +61,16 @@ describe("UserList", () => { }); test("should navigate to user create page", async function () { - const history = createMemoryHistory(); + const router = createMemoryRouter([ + { + path: "/", + element: , + }, + ]); - render( - - - , - { wrapper: TestWrapperWithoutRoutes } - ); + render(, { + wrapper: TestWrapperWithoutRoutes, + }); await waitForElementToBeRemoved(screen.getByTestId("loading")); @@ -77,18 +78,20 @@ describe("UserList", () => { screen.getByRole("link", { name: "新規ユーザを登録" }).click(); }); - expect(history.location.pathname).toBe("/ui/users/new"); + expect(router.state.location.pathname).toBe("/ui/users/new"); }); test("should navigate to user details page", async function () { - const history = createMemoryHistory(); + const router = createMemoryRouter([ + { + path: "/", + element: , + }, + ]); - render( - - - , - { wrapper: TestWrapperWithoutRoutes } - ); + render(, { + wrapper: TestWrapperWithoutRoutes, + }); await waitForElementToBeRemoved(screen.getByTestId("loading")); @@ -96,7 +99,7 @@ describe("UserList", () => { screen.getByRole("link", { name: "user1" }).click(); }); - expect(history.location.pathname).toBe("/ui/users/1"); + expect(router.state.location.pathname).toBe("/ui/users/1"); }); test("should delete a user", async function () { diff --git a/frontend/src/components/user/UserList.tsx b/frontend/src/components/user/UserList.tsx index 283328a78..0cf8c557d 100644 --- a/frontend/src/components/user/UserList.tsx +++ b/frontend/src/components/user/UserList.tsx @@ -13,7 +13,7 @@ import { } from "@mui/material"; import { styled } from "@mui/material/styles"; import React, { FC, useMemo, useState } from "react"; -import { Link, useHistory, useLocation } from "react-router-dom"; +import { Link, useNavigate, useLocation } from "react-router-dom"; import { UserControlMenu } from "./UserControlMenu"; @@ -47,7 +47,7 @@ const UserName = styled(Typography)(({}) => ({ export const UserList: FC = ({}) => { const location = useLocation(); - const history = useHistory(); + const navigate = useNavigate(); const [page, changePage] = usePage(); const params = new URLSearchParams(location.search); const [query, setQuery] = useState(params.get("query") ?? ""); @@ -72,7 +72,7 @@ export const UserList: FC = ({}) => { changePage(1); setQuery(newQuery ?? ""); - history.push({ + navigate({ pathname: location.pathname, search: newQuery ? `?query=${newQuery}` : "", }); diff --git a/frontend/src/components/user/UserPasswordFormModal.tsx b/frontend/src/components/user/UserPasswordFormModal.tsx index 708c5d7f9..406fda701 100644 --- a/frontend/src/components/user/UserPasswordFormModal.tsx +++ b/frontend/src/components/user/UserPasswordFormModal.tsx @@ -2,7 +2,7 @@ import { Box, Button, TextField } from "@mui/material"; import { styled } from "@mui/material/styles"; import { useSnackbar } from "notistack"; import React, { FC, useMemo, useState } from "react"; -import { useHistory } from "react-router-dom"; +import { useNavigate } from "react-router-dom"; import { aironeApiClient } from "../../repository/AironeApiClient"; import { AironeModal } from "../common/AironeModal"; @@ -42,7 +42,7 @@ export const UserPasswordFormModal: FC = ({ onClose, }) => { const { enqueueSnackbar } = useSnackbar(); - const history = useHistory(); + const navigate = useNavigate(); const [oldPassword, setOldPassword] = useState(""); const [newPassword, setNewPassword] = useState(""); @@ -77,10 +77,10 @@ export const UserPasswordFormModal: FC = ({ } if (ServerContext.getInstance()?.user?.id == userId) { - history.replace(loginPath()); + navigate(loginPath(), { replace: true }); } else { - history.replace(topPath()); - history.replace(usersPath()); + navigate(topPath(), { replace: true }); + navigate(usersPath(), { replace: true }); } } catch (e) { enqueueSnackbar( diff --git a/frontend/src/hooks/usePage.tsx b/frontend/src/hooks/usePage.tsx index a10a437a9..892b9bcae 100644 --- a/frontend/src/hooks/usePage.tsx +++ b/frontend/src/hooks/usePage.tsx @@ -1,5 +1,5 @@ import { useCallback, useEffect, useState } from "react"; -import { useHistory, useLocation } from "react-router-dom"; +import { useNavigate, useLocation } from "react-router-dom"; /** * A hook provides page number to perform pagination. @@ -7,7 +7,7 @@ import { useHistory, useLocation } from "react-router-dom"; */ export const usePage = (): [number, (page: number) => void] => { const location = useLocation(); - const history = useHistory(); + const navigate = useNavigate(); const [page, setPage] = useState(1); @@ -18,12 +18,12 @@ export const usePage = (): [number, (page: number) => void] => { const params = new URLSearchParams(location.search); params.set("page", newPage.toString()); - history.push({ + navigate({ pathname: location.pathname, search: params.toString(), }); }, - [location.pathname, location.search] + [location.pathname, location.search, navigate] ); useEffect(() => { diff --git a/frontend/src/hooks/usePrompt.tsx b/frontend/src/hooks/usePrompt.tsx new file mode 100644 index 000000000..dac07c259 --- /dev/null +++ b/frontend/src/hooks/usePrompt.tsx @@ -0,0 +1,19 @@ +import { useEffect } from "react"; +import { useBlocker } from "react-router-dom"; + +export const usePrompt = (when: boolean, message: string) => { + const blocker = useBlocker( + ({ currentLocation, nextLocation }) => + when && currentLocation.pathname !== nextLocation.pathname + ); + + useEffect(() => { + if (blocker.state === "blocked") { + if (window.confirm(message)) { + blocker.proceed(); + } else { + blocker.reset(); + } + } + }, [blocker, message]); +}; diff --git a/frontend/src/hooks/useSimpleSearch.tsx b/frontend/src/hooks/useSimpleSearch.tsx index c30d42f6c..e7f00aaf6 100644 --- a/frontend/src/hooks/useSimpleSearch.tsx +++ b/frontend/src/hooks/useSimpleSearch.tsx @@ -1,5 +1,5 @@ import { useCallback, useMemo } from "react"; -import { useHistory, useLocation } from "react-router-dom"; +import { useNavigate, useLocation } from "react-router-dom"; import { topPath } from "../Routes"; @@ -7,7 +7,7 @@ type Query = string | undefined; export const useSimpleSearch = (): [Query, (query: Query) => void] => { const location = useLocation(); - const history = useHistory(); + const navigate = useNavigate(); const query = useMemo(() => { const params = new URLSearchParams(location.search); @@ -16,12 +16,12 @@ export const useSimpleSearch = (): [Query, (query: Query) => void] => { const submitQuery = useCallback( (query: Query) => { - history.push({ + navigate({ pathname: topPath(), search: query != null ? `simple_search_query=${query}` : undefined, }); }, - [history] + [navigate] ); return [query, submitQuery]; diff --git a/frontend/src/hooks/useTypedParams.tsx b/frontend/src/hooks/useTypedParams.tsx index da90e2d8f..5a62b82dc 100644 --- a/frontend/src/hooks/useTypedParams.tsx +++ b/frontend/src/hooks/useTypedParams.tsx @@ -1,7 +1,16 @@ import { useParams } from "react-router-dom"; export const useTypedParams = < - Params extends { [K in keyof Params]?: any } + Params extends { [K in keyof Params]: any } >() => { - return useParams(); + const params = useParams() as unknown as Partial; + + const allFieldsDefined = Object.keys(params).every( + (key) => params[key as keyof Params] !== undefined + ); + if (!allFieldsDefined) { + throw new Error("Some required URL parameters are missing"); + } + + return params as Required; }; diff --git a/frontend/src/pages/ACLEditPage.tsx b/frontend/src/pages/ACLEditPage.tsx index b76fc9b9d..40d62a1b3 100644 --- a/frontend/src/pages/ACLEditPage.tsx +++ b/frontend/src/pages/ACLEditPage.tsx @@ -8,7 +8,7 @@ import { Box, Container } from "@mui/material"; import { useSnackbar } from "notistack"; import React, { FC, useCallback, useEffect, useState } from "react"; import { FieldErrors, useForm } from "react-hook-form"; -import { Prompt, useHistory } from "react-router-dom"; +import { useNavigate } from "react-router-dom"; import { useAsyncWithThrow } from "../hooks/useAsyncWithThrow"; @@ -20,11 +20,12 @@ import { PageHeader } from "components/common/PageHeader"; import { SubmitButton } from "components/common/SubmitButton"; import { EntityBreadcrumbs } from "components/entity/EntityBreadcrumbs"; import { EntryBreadcrumbs } from "components/entry/EntryBreadcrumbs"; +import { usePrompt } from "hooks/usePrompt"; import { useTypedParams } from "hooks/useTypedParams"; import { aironeApiClient } from "repository/AironeApiClient"; export const ACLEditPage: FC = () => { - const history = useHistory(); + const navigate = useNavigate(); const { enqueueSnackbar } = useSnackbar(); const { objectId } = useTypedParams<{ objectId: number }>(); const [entity, setEntity] = useState(); @@ -42,6 +43,11 @@ export const ACLEditPage: FC = () => { mode: "onSubmit", }); + usePrompt( + isDirty && !isSubmitSuccessful, + "編集した内容は失われてしまいますが、このページを離れてもよろしいですか?" + ); + const acl = useAsyncWithThrow(async () => { return await aironeApiClient.getAcl(objectId); }); @@ -50,17 +56,17 @@ export const ACLEditPage: FC = () => { switch (acl.value?.objtype) { case ACLObjtypeEnum.Entity: if (entity?.id) { - history.replace(entityEntriesPath(entity?.id)); + navigate(entityEntriesPath(entity?.id)); } break; case ACLObjtypeEnum.EntityAttr: if (entity?.id) { - history.replace(editEntityPath(entity?.id)); + navigate(editEntityPath(entity?.id)); } break; case ACLObjtypeEnum.Entry: if (entry?.id) { - history.replace(entryDetailsPath(entry?.schema.id, entry?.id)); + navigate(entryDetailsPath(entry?.schema.id, entry?.id)); } break; } @@ -169,11 +175,6 @@ export const ACLEditPage: FC = () => { )} - - ); }; diff --git a/frontend/src/pages/DashboardPage.tsx b/frontend/src/pages/DashboardPage.tsx index 676b59848..f2aaed085 100644 --- a/frontend/src/pages/DashboardPage.tsx +++ b/frontend/src/pages/DashboardPage.tsx @@ -2,7 +2,7 @@ import { Box, Container, Typography, TypographyTypeMap } from "@mui/material"; import { OverridableComponent } from "@mui/material/OverridableComponent"; import { styled } from "@mui/material/styles"; import React, { FC } from "react"; -import { Link, useHistory, useLocation } from "react-router-dom"; +import { Link, useNavigate, useLocation } from "react-router-dom"; import { useAsyncWithThrow } from "../hooks/useAsyncWithThrow"; @@ -38,7 +38,7 @@ const ResultEntityForEntry = styled(Typography)(({}) => ({ })); export const DashboardPage: FC = () => { - const history = useHistory(); + const navigate = useNavigate(); const location = useLocation(); const [query, submitQuery] = useSimpleSearch(); @@ -55,7 +55,7 @@ export const DashboardPage: FC = () => { // If there is only one search result, move to entry details page. if (!entries.loading && entries.value?.length === 1) { - history.push( + navigate( entryDetailsPath(entries.value[0].schema?.id ?? 0, entries.value[0].id) ); } diff --git a/frontend/src/pages/EntityEditPage.test.tsx b/frontend/src/pages/EntityEditPage.test.tsx index 7e3926745..8e407ee59 100644 --- a/frontend/src/pages/EntityEditPage.test.tsx +++ b/frontend/src/pages/EntityEditPage.test.tsx @@ -87,7 +87,10 @@ describe("EditEntityPage", () => { // wait async calls and get rendered fragment const result = render( - + } + /> , { wrapper: TestWrapperWithoutRoutes, diff --git a/frontend/src/pages/EntityEditPage.tsx b/frontend/src/pages/EntityEditPage.tsx index 69d4ae0f9..fd1bcd7a9 100644 --- a/frontend/src/pages/EntityEditPage.tsx +++ b/frontend/src/pages/EntityEditPage.tsx @@ -3,7 +3,7 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { Box } from "@mui/material"; import React, { FC, useEffect } from "react"; import { useForm } from "react-hook-form"; -import { Prompt, useHistory } from "react-router-dom"; +import { useNavigate } from "react-router-dom"; import { entitiesPath, entityEntriesPath } from "Routes"; import { Loading } from "components/common/Loading"; @@ -14,6 +14,7 @@ import { EntityForm } from "components/entity/EntityForm"; import { Schema, schema } from "components/entity/entityForm/EntityFormSchema"; import { useAsyncWithThrow } from "hooks/useAsyncWithThrow"; import { useFormNotification } from "hooks/useFormNotification"; +import { usePrompt } from "hooks/usePrompt"; import { useTypedParams } from "hooks/useTypedParams"; import { aironeApiClient } from "repository/AironeApiClient"; import { @@ -27,7 +28,7 @@ export const EntityEditPage: FC = () => { const willCreate = entityId === undefined; - const history = useHistory(); + const navigate = useNavigate(); const { enqueueSubmitResult } = useFormNotification("モデル", willCreate); const { @@ -42,6 +43,11 @@ export const EntityEditPage: FC = () => { mode: "onBlur", }); + usePrompt( + isDirty && !isSubmitSuccessful, + "編集した内容は失われてしまいますが、このページを離れてもよろしいですか?" + ); + const entity = useAsyncWithThrow(async () => { if (entityId !== undefined) { return await aironeApiClient.getEntity(entityId); @@ -57,9 +63,9 @@ export const EntityEditPage: FC = () => { const handleCancel = () => { if (entityId !== undefined) { - history.replace(entityEntriesPath(entityId)); + navigate(entityEntriesPath(entityId)); } else { - history.replace(entitiesPath()); + navigate(entitiesPath()); } }; @@ -172,9 +178,9 @@ export const EntityEditPage: FC = () => { useEffect(() => { if (isSubmitSuccessful) { if (entityId === undefined) { - history.replace(entitiesPath()); + navigate(entitiesPath()); } else { - history.replace(entityEntriesPath(entityId)); + navigate(entityEntriesPath(entityId)); } } }, [isSubmitSuccessful]); @@ -211,11 +217,6 @@ export const EntityEditPage: FC = () => { control={control} setValue={setValue} /> - - ); }; diff --git a/frontend/src/pages/EntityListPage.tsx b/frontend/src/pages/EntityListPage.tsx index 06eeba130..3bdca1069 100644 --- a/frontend/src/pages/EntityListPage.tsx +++ b/frontend/src/pages/EntityListPage.tsx @@ -1,6 +1,6 @@ import { Box, Button, Container, Typography } from "@mui/material"; import React, { FC, useCallback, useState } from "react"; -import { Link, useHistory, useLocation } from "react-router-dom"; +import { Link, useNavigate, useLocation } from "react-router-dom"; import { useAsyncWithThrow } from "../hooks/useAsyncWithThrow"; @@ -15,7 +15,7 @@ import { aironeApiClient } from "repository/AironeApiClient"; export const EntityListPage: FC = () => { const location = useLocation(); - const history = useHistory(); + const navigate = useNavigate(); const [page, changePage] = usePage(); @@ -33,7 +33,7 @@ export const EntityListPage: FC = () => { changePage(1); setQuery(newQuery ?? ""); - history.push({ + navigate({ pathname: location.pathname, search: newQuery ? `?query=${newQuery}` : "", }); diff --git a/frontend/src/pages/EntryCopyPage.tsx b/frontend/src/pages/EntryCopyPage.tsx index 72389e259..28c4a6730 100644 --- a/frontend/src/pages/EntryCopyPage.tsx +++ b/frontend/src/pages/EntryCopyPage.tsx @@ -1,7 +1,7 @@ import { Box, Container } from "@mui/material"; import { useSnackbar } from "notistack"; import React, { FC, useState } from "react"; -import { Prompt, useHistory } from "react-router-dom"; +import { useNavigate } from "react-router-dom"; import { useAsyncWithThrow } from "../hooks/useAsyncWithThrow"; import { useTypedParams } from "../hooks/useTypedParams"; @@ -15,6 +15,7 @@ import { CopyFormProps, } from "components/entry/CopyForm"; import { EntryBreadcrumbs } from "components/entry/EntryBreadcrumbs"; +import { usePrompt } from "hooks/usePrompt"; import { aironeApiClient } from "repository/AironeApiClient"; interface Props { @@ -22,7 +23,7 @@ interface Props { } export const EntryCopyPage: FC = ({ CopyForm = DefaultCopyForm }) => { - const history = useHistory(); + const navigate = useNavigate(); const { enqueueSnackbar } = useSnackbar(); const { entityId, entryId } = useTypedParams<{ entityId: number; @@ -35,6 +36,11 @@ export const EntryCopyPage: FC = ({ CopyForm = DefaultCopyForm }) => { const [submitted, setSubmitted] = useState(false); const [edited, setEdited] = useState(false); + usePrompt( + edited && !submitted, + "編集した内容は失われてしまいますが、このページを離れてもよろしいですか?" + ); + const entry = useAsyncWithThrow(async () => { return await aironeApiClient.getEntry(entryId); }, [entryId]); @@ -57,7 +63,7 @@ export const EntryCopyPage: FC = ({ CopyForm = DefaultCopyForm }) => { variant: "success", }); setTimeout(() => { - history.replace(entityEntriesPath(entityId)); + navigate(entityEntriesPath(entityId)); }, 0.1); } catch { enqueueSnackbar("アイテムコピーのジョブ登録が失敗しました", { @@ -67,7 +73,7 @@ export const EntryCopyPage: FC = ({ CopyForm = DefaultCopyForm }) => { }; const handleCancel = () => { - history.replace( + navigate( entryDetailsPath(entry.value?.schema?.id ?? 0, entry.value?.id ?? 0) ); }; @@ -98,11 +104,6 @@ export const EntryCopyPage: FC = ({ CopyForm = DefaultCopyForm }) => { /> )} - - ); }; diff --git a/frontend/src/pages/EntryDetailsPage.test.tsx b/frontend/src/pages/EntryDetailsPage.test.tsx index fc071bce7..a43986574 100644 --- a/frontend/src/pages/EntryDetailsPage.test.tsx +++ b/frontend/src/pages/EntryDetailsPage.test.tsx @@ -64,7 +64,7 @@ test("should match snapshot", async () => { } /> , { diff --git a/frontend/src/pages/EntryDetailsPage.tsx b/frontend/src/pages/EntryDetailsPage.tsx index 81ca360c5..cfed90d9a 100644 --- a/frontend/src/pages/EntryDetailsPage.tsx +++ b/frontend/src/pages/EntryDetailsPage.tsx @@ -3,7 +3,7 @@ import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown"; import { Box, Chip, Grid, IconButton, Stack, Typography } from "@mui/material"; import { styled } from "@mui/material/styles"; import React, { FC, useEffect, useState } from "react"; -import { useHistory } from "react-router-dom"; +import { useNavigate } from "react-router-dom"; import { useAsyncWithThrow } from "../hooks/useAsyncWithThrow"; import { useTypedParams } from "../hooks/useTypedParams"; @@ -74,7 +74,7 @@ export const EntryDetailsPage: FC = ({ entityId: number; entryId: number; }>(); - const history = useHistory(); + const navigate = useNavigate(); const [entryAnchorEl, setEntryAnchorEl] = useState( null @@ -87,12 +87,12 @@ export const EntryDetailsPage: FC = ({ useEffect(() => { // When user specifies invalid entityId, redirect to the page that is correct entityId if (!entry.loading && entry.value?.schema?.id != entityId) { - history.replace(entryDetailsPath(entry.value?.schema?.id ?? 0, entryId)); + navigate(entryDetailsPath(entry.value?.schema?.id ?? 0, entryId)); } // If it'd been deleted, show restore-entry page instead if (!entry.loading && entry.value?.isActive === false) { - history.replace( + navigate( restoreEntryPath(entry.value?.schema?.id ?? "", entry.value?.name ?? "") ); } diff --git a/frontend/src/pages/EntryEditPage.test.tsx b/frontend/src/pages/EntryEditPage.test.tsx index 74d749b1f..8802b4a96 100644 --- a/frontend/src/pages/EntryEditPage.test.tsx +++ b/frontend/src/pages/EntryEditPage.test.tsx @@ -66,7 +66,7 @@ describe("EntryEditPage", () => { } /> , { diff --git a/frontend/src/pages/EntryEditPage.tsx b/frontend/src/pages/EntryEditPage.tsx index b800f824a..4402e87d2 100644 --- a/frontend/src/pages/EntryEditPage.tsx +++ b/frontend/src/pages/EntryEditPage.tsx @@ -2,7 +2,7 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { Box } from "@mui/material"; import React, { FC, useEffect, useState } from "react"; import { useForm } from "react-hook-form"; -import { Prompt, useHistory } from "react-router-dom"; +import { useNavigate } from "react-router-dom"; import { useAsyncWithThrow } from "../hooks/useAsyncWithThrow"; @@ -18,6 +18,7 @@ import { } from "components/entry/EntryForm"; import { Schema, schema } from "components/entry/entryForm/EntryFormSchema"; import { useFormNotification } from "hooks/useFormNotification"; +import { usePrompt } from "hooks/usePrompt"; import { useTypedParams } from "hooks/useTypedParams"; import { aironeApiClient } from "repository/AironeApiClient"; import { @@ -45,7 +46,7 @@ export const EntryEditPage: FC = ({ const willCreate = entryId == null; - const history = useHistory(); + const navigate = useNavigate(); const { enqueueSubmitResult } = useFormNotification("アイテム", willCreate); const [initialized, setInitialized] = useState(false); @@ -62,6 +63,11 @@ export const EntryEditPage: FC = ({ mode: "onBlur", }); + usePrompt( + isDirty && !isSubmitSuccessful, + "編集した内容は失われてしまいますが、このページを離れてもよろしいですか?" + ); + const entity = useAsyncWithThrow(async () => { return await aironeApiClient.getEntity(entityId); }); @@ -104,9 +110,9 @@ export const EntryEditPage: FC = ({ useEffect(() => { if (isSubmitSuccessful) { if (willCreate) { - history.replace(entityEntriesPath(entityId)); + navigate(entityEntriesPath(entityId)); } else { - history.replace(entryDetailsPath(entityId, entryId)); + navigate(entryDetailsPath(entityId, entryId)); } } }, [isSubmitSuccessful]); @@ -140,9 +146,9 @@ export const EntryEditPage: FC = ({ const handleCancel = () => { if (willCreate) { - history.replace(entityEntriesPath(entityId)); + navigate(entityEntriesPath(entityId)); } else { - history.replace(entryDetailsPath(entityId, entryId)); + navigate(entryDetailsPath(entityId, entryId)); } }; @@ -194,11 +200,6 @@ export const EntryEditPage: FC = ({ setValue={setValue} /> )} - - ); }; diff --git a/frontend/src/pages/EntryHistoryListPage.test.tsx b/frontend/src/pages/EntryHistoryListPage.test.tsx index fbe5927c5..2b9ae480f 100644 --- a/frontend/src/pages/EntryHistoryListPage.test.tsx +++ b/frontend/src/pages/EntryHistoryListPage.test.tsx @@ -55,7 +55,7 @@ test("should match snapshot", async () => { } /> , { diff --git a/frontend/src/pages/GroupEditPage.tsx b/frontend/src/pages/GroupEditPage.tsx index 30b1df0bc..f405ffb3e 100644 --- a/frontend/src/pages/GroupEditPage.tsx +++ b/frontend/src/pages/GroupEditPage.tsx @@ -2,7 +2,7 @@ import { zodResolver } from "@hookform/resolvers/zod/dist/zod"; import { Box, Container, Typography } from "@mui/material"; import React, { FC, useEffect } from "react"; import { useForm } from "react-hook-form"; -import { Link, Prompt, useHistory } from "react-router-dom"; +import { Link, useNavigate } from "react-router-dom"; import { useAsyncWithThrow } from "../hooks/useAsyncWithThrow"; @@ -13,6 +13,7 @@ import { SubmitButton } from "components/common/SubmitButton"; import { GroupForm } from "components/group/GroupForm"; import { schema, Schema } from "components/group/groupForm/GroupFormSchema"; import { useFormNotification } from "hooks/useFormNotification"; +import { usePrompt } from "hooks/usePrompt"; import { useTypedParams } from "hooks/useTypedParams"; import { aironeApiClient } from "repository/AironeApiClient"; import { @@ -26,7 +27,7 @@ export const GroupEditPage: FC = () => { const { groupId } = useTypedParams<{ groupId?: number }>(); const willCreate = groupId == null; - const history = useHistory(); + const navigate = useNavigate(); const { enqueueSubmitResult } = useFormNotification("グループ", willCreate); const { @@ -41,6 +42,11 @@ export const GroupEditPage: FC = () => { mode: "onBlur", }); + usePrompt( + isDirty && !isSubmitSuccessful, + "編集した内容は失われてしまいますが、このページを離れてもよろしいですか?" + ); + const group = useAsyncWithThrow(async () => { return groupId != null ? await aironeApiClient.getGroup(groupId) @@ -82,11 +88,11 @@ export const GroupEditPage: FC = () => { }; useEffect(() => { - isSubmitSuccessful && history.replace(groupsPath()); + isSubmitSuccessful && navigate(groupsPath()); }, [isSubmitSuccessful]); const handleCancel = async () => { - history.goBack(); + navigate(-1); }; if (ServerContext.getInstance()?.user?.isSuperuser !== true) { @@ -125,11 +131,6 @@ export const GroupEditPage: FC = () => { groupId={Number(groupId)} /> - - ); }; diff --git a/frontend/src/pages/LoginPage.tsx b/frontend/src/pages/LoginPage.tsx index 8166704e2..4eb95dda6 100644 --- a/frontend/src/pages/LoginPage.tsx +++ b/frontend/src/pages/LoginPage.tsx @@ -19,7 +19,7 @@ import { Typography, } from "@mui/material"; import React, { FC, useCallback, useEffect, useState } from "react"; -import { useHistory } from "react-router-dom"; +import { useNavigate, useLocation } from "react-router-dom"; import { PasswordResetConfirmModal } from "../components/user/PasswordResetConfirmModal"; import { PasswordResetModal } from "../components/user/PasswordResetModal"; @@ -29,7 +29,8 @@ import { ServerContext } from "services/ServerContext"; export const LoginPage: FC = () => { const serverContext = ServerContext.getInstance(); - const history = useHistory(); + const navigate = useNavigate(); + const location = useLocation(); const [showPassword, setShowPassword] = useState(false); const [isAlert, setIsAlert] = useState(false); @@ -56,10 +57,13 @@ export const LoginPage: FC = () => { if (_token != null) { setToken(_token); params.delete("token"); - history.replace({ - pathname: location.pathname, - search: "?" + params.toString(), - }); + navigate( + { + pathname: location.pathname, + search: "?" + params.toString(), + }, + { replace: true } + ); } if (_uidb64 != null && _token != null) { diff --git a/frontend/src/pages/RoleEditPage.tsx b/frontend/src/pages/RoleEditPage.tsx index 943b886b2..d4dcd0aa1 100644 --- a/frontend/src/pages/RoleEditPage.tsx +++ b/frontend/src/pages/RoleEditPage.tsx @@ -3,7 +3,7 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { Box, Container, Typography } from "@mui/material"; import React, { FC, useCallback, useEffect } from "react"; import { useForm } from "react-hook-form"; -import { Link, Prompt, useHistory } from "react-router-dom"; +import { Link, useNavigate } from "react-router-dom"; import { useAsyncWithThrow } from "../hooks/useAsyncWithThrow"; @@ -15,6 +15,7 @@ import { SubmitButton } from "components/common/SubmitButton"; import { RoleForm } from "components/role/RoleForm"; import { Schema, schema } from "components/role/roleForm/RoleFormSchema"; import { useFormNotification } from "hooks/useFormNotification"; +import { usePrompt } from "hooks/usePrompt"; import { useTypedParams } from "hooks/useTypedParams"; import { aironeApiClient } from "repository/AironeApiClient"; import { @@ -26,7 +27,7 @@ export const RoleEditPage: FC = () => { const { roleId } = useTypedParams<{ roleId?: number }>(); const willCreate = roleId == null; - const history = useHistory(); + const navigate = useNavigate(); const { enqueueSubmitResult } = useFormNotification("ロール", willCreate); const { @@ -41,6 +42,11 @@ export const RoleEditPage: FC = () => { mode: "onBlur", }); + usePrompt( + isDirty && !isSubmitSuccessful, + "編集した内容は失われてしまいますが、このページを離れてもよろしいですか?" + ); + const role = useAsyncWithThrow(async () => { return roleId != null ? await aironeApiClient.getRole(roleId) : undefined; }, [roleId]); @@ -50,7 +56,7 @@ export const RoleEditPage: FC = () => { }, [role.loading]); useEffect(() => { - isSubmitSuccessful && history.push(rolesPath()); + isSubmitSuccessful && navigate(rolesPath()); }, [isSubmitSuccessful]); const handleSubmitOnValid = useCallback( @@ -89,7 +95,7 @@ export const RoleEditPage: FC = () => { ); const handleCancel = async () => { - history.goBack(); + navigate(-1); }; if (role.loading) { @@ -130,11 +136,6 @@ export const RoleEditPage: FC = () => { - - ); }; diff --git a/frontend/src/pages/TriggerEditPage.test.tsx b/frontend/src/pages/TriggerEditPage.test.tsx index cb3e164cd..3a2fd730a 100644 --- a/frontend/src/pages/TriggerEditPage.test.tsx +++ b/frontend/src/pages/TriggerEditPage.test.tsx @@ -86,7 +86,7 @@ describe("EditTriggerPage", () => { } /> , { diff --git a/frontend/src/pages/TriggerEditPage.tsx b/frontend/src/pages/TriggerEditPage.tsx index 5034a9c20..65ebb8807 100644 --- a/frontend/src/pages/TriggerEditPage.tsx +++ b/frontend/src/pages/TriggerEditPage.tsx @@ -18,7 +18,7 @@ import { import { styled } from "@mui/material/styles"; import React, { FC, useCallback, useEffect, useState } from "react"; import { Controller, useForm } from "react-hook-form"; -import { Link, Prompt, useHistory } from "react-router-dom"; +import { Link, useNavigate } from "react-router-dom"; import type { TriggerActionUpdate } from "@dmm-com/airone-apiclient-typescript-fetch/src/autogenerated/models/TriggerActionUpdate"; @@ -31,6 +31,7 @@ import { Conditions } from "components/trigger/Conditions"; import { Schema, schema } from "components/trigger/TriggerFormSchema"; import { useAsyncWithThrow } from "hooks/useAsyncWithThrow"; import { useFormNotification } from "hooks/useFormNotification"; +import { usePrompt } from "hooks/usePrompt"; import { useTypedParams } from "hooks/useTypedParams"; import { aironeApiClient } from "repository/AironeApiClient"; @@ -66,7 +67,7 @@ export const TriggerEditPage: FC = () => { const { triggerId } = useTypedParams<{ triggerId: number }>(); const willCreate = triggerId === undefined; - const history = useHistory(); + const navigate = useNavigate(); const { enqueueSubmitResult } = useFormNotification("トリガー", willCreate); const actionTrigger = useAsyncWithThrow(async () => { @@ -95,6 +96,11 @@ export const TriggerEditPage: FC = () => { }, }); + usePrompt( + isDirty && !isSubmitSuccessful, + "編集した内容は失われてしまいますが、このページを離れてもよろしいですか?" + ); + const entities = useAsyncWithThrow(async () => { const entities = await aironeApiClient.getEntities(); return entities.results; @@ -251,7 +257,7 @@ export const TriggerEditPage: FC = () => { ); const handleCancel = async () => { - history.goBack(); + navigate(-1); }; useEffect(() => { @@ -283,7 +289,7 @@ export const TriggerEditPage: FC = () => { useEffect(() => { if (isSubmitSuccessful) { - history.replace(triggersPath()); + navigate(triggersPath()); } }, [isSubmitSuccessful]); @@ -425,10 +431,6 @@ export const TriggerEditPage: FC = () => { )} - ); }; diff --git a/frontend/src/pages/TriggerListPage.tsx b/frontend/src/pages/TriggerListPage.tsx index 54066a67c..bfaf8d091 100644 --- a/frontend/src/pages/TriggerListPage.tsx +++ b/frontend/src/pages/TriggerListPage.tsx @@ -29,7 +29,7 @@ import { OverridableComponent } from "@mui/material/OverridableComponent"; import { styled } from "@mui/material/styles"; import { useSnackbar } from "notistack"; import React, { FC, useState } from "react"; -import { Link, useHistory } from "react-router-dom"; +import { Link, useNavigate } from "react-router-dom"; import { useAsync } from "react-use"; import { @@ -207,7 +207,7 @@ const TriggerAction: FC<{ }; export const TriggerListPage: FC = () => { - const history = useHistory(); + const navigate = useNavigate(); const [toggle, setToggle] = useState(false); const { enqueueSnackbar } = useSnackbar(); @@ -221,8 +221,8 @@ export const TriggerListPage: FC = () => { enqueueSnackbar(`トリガーの削除が完了しました`, { variant: "success", }); - history.replace(topPath()); - history.replace(triggersPath()); + navigate(topPath()); + navigate(triggersPath()); setToggle(!toggle); } catch (e) { enqueueSnackbar("トリガーの削除が失敗しました", { diff --git a/frontend/src/pages/UserEditPage.tsx b/frontend/src/pages/UserEditPage.tsx index 243749909..146206226 100644 --- a/frontend/src/pages/UserEditPage.tsx +++ b/frontend/src/pages/UserEditPage.tsx @@ -3,8 +3,7 @@ import { Box, Typography, Button, Container } from "@mui/material"; import { useSnackbar } from "notistack"; import React, { FC, useEffect, useMemo, useState } from "react"; import { useForm } from "react-hook-form"; -import { Link, Prompt } from "react-router-dom"; -import { useHistory } from "react-router-dom"; +import { Link, useNavigate } from "react-router-dom"; import { useToggle } from "react-use"; import { topPath, usersPath } from "Routes"; @@ -17,6 +16,7 @@ import { UserPasswordFormModal } from "components/user/UserPasswordFormModal"; import { schema, Schema } from "components/user/userForm/UserFormSchema"; import { useAsyncWithThrow } from "hooks/useAsyncWithThrow"; import { useFormNotification } from "hooks/useFormNotification"; +import { usePrompt } from "hooks/usePrompt"; import { useTypedParams } from "hooks/useTypedParams"; import { aironeApiClient } from "repository/AironeApiClient"; import { @@ -29,7 +29,7 @@ export const UserEditPage: FC = () => { const { userId } = useTypedParams<{ userId?: number }>(); const willCreate = userId == null; - const history = useHistory(); + const navigate = useNavigate(); const { enqueueSnackbar } = useSnackbar(); const { enqueueSubmitResult } = useFormNotification("ユーザ", willCreate); const [shouldRefresh, toggleShouldRefresh] = useToggle(false); @@ -45,6 +45,11 @@ export const UserEditPage: FC = () => { mode: "onBlur", }); + usePrompt( + isDirty && !isSubmitSuccessful, + "編集した内容は失われてしまいますが、このページを離れてもよろしいですか?" + ); + const user = useAsyncWithThrow(async () => { if (userId) { return await aironeApiClient.getUser(userId); @@ -56,7 +61,7 @@ export const UserEditPage: FC = () => { }, [user.value]); useEffect(() => { - isSubmitSuccessful && history.push(usersPath()); + isSubmitSuccessful && navigate(usersPath()); }, [isSubmitSuccessful]); // These state variables and handlers are used for password reset feature @@ -118,7 +123,7 @@ export const UserEditPage: FC = () => { }; const handleCancel = () => { - history.replace(usersPath()); + navigate(usersPath()); }; const handleRefreshToken = async () => { @@ -208,11 +213,6 @@ export const UserEditPage: FC = () => { /> )} - - ); }; diff --git a/package-lock.json b/package-lock.json index ae4750ca9..5a608341c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,7 +21,6 @@ "@react-oauth/google": "^0.12.1", "@types/encoding-japanese": "^2.2.1", "@types/react-beautiful-dnd": "^13.1.4", - "@types/react-router-dom": "^5.3.3", "@typescript-eslint/eslint-plugin": "^6.19.0", "@typescript-eslint/parser": "^6.19.0", "encoding-japanese": "^2.2.0", @@ -37,7 +36,7 @@ "react-force-graph-2d": "^1.25.6", "react-hook-form": "^7.33.1", "react-infinite-scroller": "^1.2.6", - "react-router-dom": "^5.2.0", + "react-router-dom": "^6.27.0", "react-scroll": "^1.8.5", "react-use": "^17.2.4", "recharts": "^2.1.16", @@ -4282,6 +4281,14 @@ "react-dom": ">=16.8.0" } }, + "node_modules/@remix-run/router": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.20.0.tgz", + "integrity": "sha512-mUnk8rPJBI9loFDZ+YzPGdeniYK+FTmRD1TMCz7ev2SNIozyKKpnGgsxO34u6Z4z/t0ITuu7voi/AshfsGsgFg==", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -4649,11 +4656,6 @@ "@types/node": "*" } }, - "node_modules/@types/history": { - "version": "4.7.11", - "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.11.tgz", - "integrity": "sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==" - }, "node_modules/@types/hoist-non-react-statics": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", @@ -4995,25 +4997,6 @@ "redux": "^4.0.0" } }, - "node_modules/@types/react-router": { - "version": "5.1.20", - "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.20.tgz", - "integrity": "sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==", - "dependencies": { - "@types/history": "^4.7.11", - "@types/react": "*" - } - }, - "node_modules/@types/react-router-dom": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.3.3.tgz", - "integrity": "sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==", - "dependencies": { - "@types/history": "^4.7.11", - "@types/react": "*", - "@types/react-router": "*" - } - }, "node_modules/@types/react-scroll": { "version": "1.8.7", "resolved": "https://registry.npmjs.org/@types/react-scroll/-/react-scroll-1.8.7.tgz", @@ -9111,19 +9094,6 @@ "integrity": "sha512-EWGTfnTqAO2L/j5HZgoM/3z82L7necsJ0pO9Tp0X1wil3PDLrkypTBRgVO2ExehEEvUycejZD3FuRaXpZZc3kw==", "dev": true }, - "node_modules/history": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz", - "integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==", - "dependencies": { - "@babel/runtime": "^7.1.2", - "loose-envify": "^1.2.0", - "resolve-pathname": "^3.0.0", - "tiny-invariant": "^1.0.2", - "tiny-warning": "^1.0.0", - "value-equal": "^1.0.1" - } - }, "node_modules/hoist-non-react-statics": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", @@ -9938,11 +9908,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==" - }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -14601,14 +14566,6 @@ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, - "node_modules/path-to-regexp": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.9.0.tgz", - "integrity": "sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g==", - "dependencies": { - "isarray": "0.0.1" - } - }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -15055,46 +15012,35 @@ } }, "node_modules/react-router": { - "version": "5.3.4", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.4.tgz", - "integrity": "sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA==", - "dependencies": { - "@babel/runtime": "^7.12.13", - "history": "^4.9.0", - "hoist-non-react-statics": "^3.1.0", - "loose-envify": "^1.3.1", - "path-to-regexp": "^1.7.0", - "prop-types": "^15.6.2", - "react-is": "^16.6.0", - "tiny-invariant": "^1.0.2", - "tiny-warning": "^1.0.0" + "version": "6.27.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.27.0.tgz", + "integrity": "sha512-YA+HGZXz4jaAkVoYBE98VQl+nVzI+cVI2Oj/06F5ZM+0u3TgedN9Y9kmMRo2mnkSK2nCpNQn0DVob4HCsY/WLw==", + "dependencies": { + "@remix-run/router": "1.20.0" + }, + "engines": { + "node": ">=14.0.0" }, "peerDependencies": { - "react": ">=15" + "react": ">=16.8" } }, "node_modules/react-router-dom": { - "version": "5.3.4", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.4.tgz", - "integrity": "sha512-m4EqFMHv/Ih4kpcBCONHbkT68KoAeHN4p3lAGoNryfHi0dMy0kCzEZakiKRsvg5wHZ/JLrLW8o8KomWiz/qbYQ==", + "version": "6.27.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.27.0.tgz", + "integrity": "sha512-+bvtFWMC0DgAFrfKXKG9Fc+BcXWRUO1aJIihbB79xaeq0v5UzfvnM5houGUm1Y461WVRcgAQ+Clh5rdb1eCx4g==", "dependencies": { - "@babel/runtime": "^7.12.13", - "history": "^4.9.0", - "loose-envify": "^1.3.1", - "prop-types": "^15.6.2", - "react-router": "5.3.4", - "tiny-invariant": "^1.0.2", - "tiny-warning": "^1.0.0" + "@remix-run/router": "1.20.0", + "react-router": "6.27.0" + }, + "engines": { + "node": ">=14.0.0" }, "peerDependencies": { - "react": ">=15" + "react": ">=16.8", + "react-dom": ">=16.8" } }, - "node_modules/react-router/node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" - }, "node_modules/react-scroll": { "version": "1.8.9", "resolved": "https://registry.npmjs.org/react-scroll/-/react-scroll-1.8.9.tgz", @@ -15471,11 +15417,6 @@ "node": ">=4" } }, - "node_modules/resolve-pathname": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz", - "integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==" - }, "node_modules/resolve.exports": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", @@ -16315,11 +16256,6 @@ "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.1.tgz", "integrity": "sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==" }, - "node_modules/tiny-warning": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", - "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==" - }, "node_modules/tinycolor2": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", @@ -17020,11 +16956,6 @@ "node": ">=10.12.0" } }, - "node_modules/value-equal": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz", - "integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==" - }, "node_modules/victory-vendor": { "version": "36.6.11", "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.6.11.tgz", @@ -20615,6 +20546,11 @@ "integrity": "sha512-qagsy22t+7UdkYAiT5ZhfM4StXi9PPNvw0zuwNmabrWyMKddczMtBIOARflbaIj+wHiQjnMAsZmzsUYuXeyoSg==", "requires": {} }, + "@remix-run/router": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.20.0.tgz", + "integrity": "sha512-mUnk8rPJBI9loFDZ+YzPGdeniYK+FTmRD1TMCz7ev2SNIozyKKpnGgsxO34u6Z4z/t0ITuu7voi/AshfsGsgFg==" + }, "@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -20929,11 +20865,6 @@ "@types/node": "*" } }, - "@types/history": { - "version": "4.7.11", - "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.11.tgz", - "integrity": "sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==" - }, "@types/hoist-non-react-statics": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", @@ -21228,25 +21159,6 @@ "redux": "^4.0.0" } }, - "@types/react-router": { - "version": "5.1.20", - "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.20.tgz", - "integrity": "sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==", - "requires": { - "@types/history": "^4.7.11", - "@types/react": "*" - } - }, - "@types/react-router-dom": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.3.3.tgz", - "integrity": "sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==", - "requires": { - "@types/history": "^4.7.11", - "@types/react": "*", - "@types/react-router": "*" - } - }, "@types/react-scroll": { "version": "1.8.7", "resolved": "https://registry.npmjs.org/@types/react-scroll/-/react-scroll-1.8.7.tgz", @@ -24292,19 +24204,6 @@ "integrity": "sha512-EWGTfnTqAO2L/j5HZgoM/3z82L7necsJ0pO9Tp0X1wil3PDLrkypTBRgVO2ExehEEvUycejZD3FuRaXpZZc3kw==", "dev": true }, - "history": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz", - "integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==", - "requires": { - "@babel/runtime": "^7.1.2", - "loose-envify": "^1.2.0", - "resolve-pathname": "^3.0.0", - "tiny-invariant": "^1.0.2", - "tiny-warning": "^1.0.0", - "value-equal": "^1.0.1" - } - }, "hoist-non-react-statics": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", @@ -24872,11 +24771,6 @@ "get-intrinsic": "^1.1.1" } }, - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==" - }, "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -28401,14 +28295,6 @@ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, - "path-to-regexp": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.9.0.tgz", - "integrity": "sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g==", - "requires": { - "isarray": "0.0.1" - } - }, "path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -28714,40 +28600,20 @@ } }, "react-router": { - "version": "5.3.4", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.4.tgz", - "integrity": "sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA==", - "requires": { - "@babel/runtime": "^7.12.13", - "history": "^4.9.0", - "hoist-non-react-statics": "^3.1.0", - "loose-envify": "^1.3.1", - "path-to-regexp": "^1.7.0", - "prop-types": "^15.6.2", - "react-is": "^16.6.0", - "tiny-invariant": "^1.0.2", - "tiny-warning": "^1.0.0" - }, - "dependencies": { - "react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" - } + "version": "6.27.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.27.0.tgz", + "integrity": "sha512-YA+HGZXz4jaAkVoYBE98VQl+nVzI+cVI2Oj/06F5ZM+0u3TgedN9Y9kmMRo2mnkSK2nCpNQn0DVob4HCsY/WLw==", + "requires": { + "@remix-run/router": "1.20.0" } }, "react-router-dom": { - "version": "5.3.4", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.4.tgz", - "integrity": "sha512-m4EqFMHv/Ih4kpcBCONHbkT68KoAeHN4p3lAGoNryfHi0dMy0kCzEZakiKRsvg5wHZ/JLrLW8o8KomWiz/qbYQ==", + "version": "6.27.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.27.0.tgz", + "integrity": "sha512-+bvtFWMC0DgAFrfKXKG9Fc+BcXWRUO1aJIihbB79xaeq0v5UzfvnM5houGUm1Y461WVRcgAQ+Clh5rdb1eCx4g==", "requires": { - "@babel/runtime": "^7.12.13", - "history": "^4.9.0", - "loose-envify": "^1.3.1", - "prop-types": "^15.6.2", - "react-router": "5.3.4", - "tiny-invariant": "^1.0.2", - "tiny-warning": "^1.0.0" + "@remix-run/router": "1.20.0", + "react-router": "6.27.0" } }, "react-scroll": { @@ -29048,11 +28914,6 @@ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==" }, - "resolve-pathname": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz", - "integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==" - }, "resolve.exports": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", @@ -29662,11 +29523,6 @@ "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.1.tgz", "integrity": "sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==" }, - "tiny-warning": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", - "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==" - }, "tinycolor2": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", @@ -30157,11 +30013,6 @@ "convert-source-map": "^1.6.0" } }, - "value-equal": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz", - "integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==" - }, "victory-vendor": { "version": "36.6.11", "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.6.11.tgz", diff --git a/package.json b/package.json index 0758063b7..9e92b859e 100644 --- a/package.json +++ b/package.json @@ -92,7 +92,6 @@ "@react-oauth/google": "^0.12.1", "@types/encoding-japanese": "^2.2.1", "@types/react-beautiful-dnd": "^13.1.4", - "@types/react-router-dom": "^5.3.3", "@typescript-eslint/eslint-plugin": "^6.19.0", "@typescript-eslint/parser": "^6.19.0", "encoding-japanese": "^2.2.0", @@ -108,7 +107,7 @@ "react-force-graph-2d": "^1.25.6", "react-hook-form": "^7.33.1", "react-infinite-scroller": "^1.2.6", - "react-router-dom": "^5.2.0", + "react-router-dom": "^6.27.0", "react-scroll": "^1.8.5", "react-use": "^17.2.4", "recharts": "^2.1.16", From eda0195c8dd8577c68b1310c73deec2c564b723f Mon Sep 17 00:00:00 2001 From: Ryo Okubo Date: Sat, 26 Oct 2024 12:38:50 +0900 Subject: [PATCH 2/4] Use replace appropriately --- frontend/src/pages/ACLEditPage.tsx | 8 +++++--- frontend/src/pages/EntityEditPage.tsx | 8 ++++---- frontend/src/pages/EntryCopyPage.tsx | 5 +++-- frontend/src/pages/EntryDetailsPage.tsx | 10 ++++++++-- frontend/src/pages/EntryEditPage.tsx | 8 ++++---- frontend/src/pages/GroupEditPage.tsx | 2 +- frontend/src/pages/TriggerEditPage.tsx | 2 +- frontend/src/pages/TriggerListPage.tsx | 4 ++-- 8 files changed, 28 insertions(+), 19 deletions(-) diff --git a/frontend/src/pages/ACLEditPage.tsx b/frontend/src/pages/ACLEditPage.tsx index 40d62a1b3..1ffb9b161 100644 --- a/frontend/src/pages/ACLEditPage.tsx +++ b/frontend/src/pages/ACLEditPage.tsx @@ -56,17 +56,19 @@ export const ACLEditPage: FC = () => { switch (acl.value?.objtype) { case ACLObjtypeEnum.Entity: if (entity?.id) { - navigate(entityEntriesPath(entity?.id)); + navigate(entityEntriesPath(entity?.id), { replace: true }); } break; case ACLObjtypeEnum.EntityAttr: if (entity?.id) { - navigate(editEntityPath(entity?.id)); + navigate(editEntityPath(entity?.id), { replace: true }); } break; case ACLObjtypeEnum.Entry: if (entry?.id) { - navigate(entryDetailsPath(entry?.schema.id, entry?.id)); + navigate(entryDetailsPath(entry?.schema.id, entry?.id), { + replace: true, + }); } break; } diff --git a/frontend/src/pages/EntityEditPage.tsx b/frontend/src/pages/EntityEditPage.tsx index fd1bcd7a9..fb937f1b0 100644 --- a/frontend/src/pages/EntityEditPage.tsx +++ b/frontend/src/pages/EntityEditPage.tsx @@ -63,9 +63,9 @@ export const EntityEditPage: FC = () => { const handleCancel = () => { if (entityId !== undefined) { - navigate(entityEntriesPath(entityId)); + navigate(entityEntriesPath(entityId), { replace: true }); } else { - navigate(entitiesPath()); + navigate(entitiesPath(), { replace: true }); } }; @@ -178,9 +178,9 @@ export const EntityEditPage: FC = () => { useEffect(() => { if (isSubmitSuccessful) { if (entityId === undefined) { - navigate(entitiesPath()); + navigate(entitiesPath(), { replace: true }); } else { - navigate(entityEntriesPath(entityId)); + navigate(entityEntriesPath(entityId), { replace: true }); } } }, [isSubmitSuccessful]); diff --git a/frontend/src/pages/EntryCopyPage.tsx b/frontend/src/pages/EntryCopyPage.tsx index 28c4a6730..0cc5c1872 100644 --- a/frontend/src/pages/EntryCopyPage.tsx +++ b/frontend/src/pages/EntryCopyPage.tsx @@ -63,7 +63,7 @@ export const EntryCopyPage: FC = ({ CopyForm = DefaultCopyForm }) => { variant: "success", }); setTimeout(() => { - navigate(entityEntriesPath(entityId)); + navigate(entityEntriesPath(entityId), { replace: true }); }, 0.1); } catch { enqueueSnackbar("アイテムコピーのジョブ登録が失敗しました", { @@ -74,7 +74,8 @@ export const EntryCopyPage: FC = ({ CopyForm = DefaultCopyForm }) => { const handleCancel = () => { navigate( - entryDetailsPath(entry.value?.schema?.id ?? 0, entry.value?.id ?? 0) + entryDetailsPath(entry.value?.schema?.id ?? 0, entry.value?.id ?? 0), + { replace: true } ); }; diff --git a/frontend/src/pages/EntryDetailsPage.tsx b/frontend/src/pages/EntryDetailsPage.tsx index cfed90d9a..8569b4249 100644 --- a/frontend/src/pages/EntryDetailsPage.tsx +++ b/frontend/src/pages/EntryDetailsPage.tsx @@ -87,13 +87,19 @@ export const EntryDetailsPage: FC = ({ useEffect(() => { // When user specifies invalid entityId, redirect to the page that is correct entityId if (!entry.loading && entry.value?.schema?.id != entityId) { - navigate(entryDetailsPath(entry.value?.schema?.id ?? 0, entryId)); + navigate(entryDetailsPath(entry.value?.schema?.id ?? 0, entryId), { + replace: true, + }); } // If it'd been deleted, show restore-entry page instead if (!entry.loading && entry.value?.isActive === false) { navigate( - restoreEntryPath(entry.value?.schema?.id ?? "", entry.value?.name ?? "") + restoreEntryPath( + entry.value?.schema?.id ?? "", + entry.value?.name ?? "" + ), + { replace: true } ); } }, [entry.loading]); diff --git a/frontend/src/pages/EntryEditPage.tsx b/frontend/src/pages/EntryEditPage.tsx index 4402e87d2..8ea2ec41f 100644 --- a/frontend/src/pages/EntryEditPage.tsx +++ b/frontend/src/pages/EntryEditPage.tsx @@ -110,9 +110,9 @@ export const EntryEditPage: FC = ({ useEffect(() => { if (isSubmitSuccessful) { if (willCreate) { - navigate(entityEntriesPath(entityId)); + navigate(entityEntriesPath(entityId), { replace: true }); } else { - navigate(entryDetailsPath(entityId, entryId)); + navigate(entryDetailsPath(entityId, entryId), { replace: true }); } } }, [isSubmitSuccessful]); @@ -146,9 +146,9 @@ export const EntryEditPage: FC = ({ const handleCancel = () => { if (willCreate) { - navigate(entityEntriesPath(entityId)); + navigate(entityEntriesPath(entityId), { replace: true }); } else { - navigate(entryDetailsPath(entityId, entryId)); + navigate(entryDetailsPath(entityId, entryId), { replace: true }); } }; diff --git a/frontend/src/pages/GroupEditPage.tsx b/frontend/src/pages/GroupEditPage.tsx index f405ffb3e..cc6184ea7 100644 --- a/frontend/src/pages/GroupEditPage.tsx +++ b/frontend/src/pages/GroupEditPage.tsx @@ -88,7 +88,7 @@ export const GroupEditPage: FC = () => { }; useEffect(() => { - isSubmitSuccessful && navigate(groupsPath()); + isSubmitSuccessful && navigate(groupsPath(), { replace: true }); }, [isSubmitSuccessful]); const handleCancel = async () => { diff --git a/frontend/src/pages/TriggerEditPage.tsx b/frontend/src/pages/TriggerEditPage.tsx index 65ebb8807..87a486bf7 100644 --- a/frontend/src/pages/TriggerEditPage.tsx +++ b/frontend/src/pages/TriggerEditPage.tsx @@ -289,7 +289,7 @@ export const TriggerEditPage: FC = () => { useEffect(() => { if (isSubmitSuccessful) { - navigate(triggersPath()); + navigate(triggersPath(), { replace: true }); } }, [isSubmitSuccessful]); diff --git a/frontend/src/pages/TriggerListPage.tsx b/frontend/src/pages/TriggerListPage.tsx index bfaf8d091..fd8cd6bd7 100644 --- a/frontend/src/pages/TriggerListPage.tsx +++ b/frontend/src/pages/TriggerListPage.tsx @@ -221,8 +221,8 @@ export const TriggerListPage: FC = () => { enqueueSnackbar(`トリガーの削除が完了しました`, { variant: "success", }); - navigate(topPath()); - navigate(triggersPath()); + navigate(topPath(), { replace: true }); + navigate(triggersPath(), { replace: true }); setToggle(!toggle); } catch (e) { enqueueSnackbar("トリガーの削除が失敗しました", { From c54ad507d9f0501d124abdcad48ed9748f10fe89 Mon Sep 17 00:00:00 2001 From: Ryo Okubo Date: Sat, 26 Oct 2024 20:02:41 +0900 Subject: [PATCH 3/4] Simplify custom routes --- frontend/src/AppRouter.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/frontend/src/AppRouter.tsx b/frontend/src/AppRouter.tsx index 7d1ebdb0c..aa88ea0d6 100644 --- a/frontend/src/AppRouter.tsx +++ b/frontend/src/AppRouter.tsx @@ -71,7 +71,7 @@ interface Props { customRoutes?: { path: string; routePath: string; - component?: React.ReactNode; + element: React.ReactNode; }[]; } @@ -92,9 +92,7 @@ export const AppRouter: FC = ({ customRoutes }) => { {customRoutes && customRoutes.map((r) => ( - {r.component && ( - - )} + ))} From 8a4c443bd5272f3a75e9563e94a16a6cece6bd7a Mon Sep 17 00:00:00 2001 From: Ryo Okubo Date: Sat, 26 Oct 2024 20:10:12 +0900 Subject: [PATCH 4/4] Update snapshot --- frontend/src/pages/__snapshots__/EntryEditPage.test.tsx.snap | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frontend/src/pages/__snapshots__/EntryEditPage.test.tsx.snap b/frontend/src/pages/__snapshots__/EntryEditPage.test.tsx.snap index 0a63184d4..4bc96176d 100644 --- a/frontend/src/pages/__snapshots__/EntryEditPage.test.tsx.snap +++ b/frontend/src/pages/__snapshots__/EntryEditPage.test.tsx.snap @@ -279,6 +279,7 @@ exports[`EntryEditPage should match snapshot 1`] = `