From 2e8fc0c73f784620554a36728989882bd298f5cb Mon Sep 17 00:00:00 2001 From: Alan Vezina Date: Tue, 14 Nov 2023 14:43:04 -0600 Subject: [PATCH 001/116] ProductMenu: Don't use routes --- .../components/navigation/ProductMenu.tsx | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/packages/components/src/internal/components/navigation/ProductMenu.tsx b/packages/components/src/internal/components/navigation/ProductMenu.tsx index d8dcd1ad68..675257119e 100644 --- a/packages/components/src/internal/components/navigation/ProductMenu.tsx +++ b/packages/components/src/internal/components/navigation/ProductMenu.tsx @@ -19,14 +19,13 @@ import { List, Map } from 'immutable'; import { DropdownButton } from 'react-bootstrap'; import { withRouter, WithRouterProps } from 'react-router'; import { ActionURL } from '@labkey/api'; +import { Location } from '../../util/URL'; import { blurActiveElement } from '../../util/utils'; import { LoadingSpinner } from '../base/LoadingSpinner'; import { useServerContext } from '../base/ServerContext'; import { AppProperties } from '../../app/models'; -import { - getCurrentAppProperties, -} from '../../app/utils'; +import { getCurrentAppProperties } from '../../app/utils'; import { Alert } from '../base/Alert'; @@ -67,7 +66,7 @@ export interface ProductMenuButtonProps { } const ProductMenuButtonImpl: FC = memo(props => { - const { appProperties = getCurrentAppProperties(), routes } = props; + const { appProperties = getCurrentAppProperties(), location } = props; const [menuOpen, setMenuOpen] = useState(false); const [error, setError] = useState(); const [loading, setLoading] = useState(LoadingState.INITIALIZED); @@ -135,7 +134,7 @@ const ProductMenuButtonImpl: FC = memo id="product-menu" onToggle={toggleMenu} open={menuOpen} - title={} + title={} > {menuOpen && ( = memo(props => { interface ProductMenuButtonTitle { container: Container; folderItems: FolderMenuItem[]; - routes: any[]; + location: Location; } export const ProductMenuButtonTitle: FC = memo(props => { - const { container, folderItems, routes } = props; + const { container, folderItems, location } = props; const title = useMemo(() => { return folderItems?.length > 1 ? (container.path === HOME_PATH ? HOME_TITLE : container.title) : 'Menu'; }, [container.path, container.title, folderItems?.length]); const subtitle = useMemo(() => { - return getHeaderMenuSubtitle(routes?.[1]?.path); - }, [routes]); + return getHeaderMenuSubtitle(location?.pathname?.split('/')[1]); + }, [location]); return ( <> From 560ea511c856e8c79a8121feca28ba3818aec4e5 Mon Sep 17 00:00:00 2001 From: Alan Vezina Date: Wed, 15 Nov 2023 10:54:11 -0600 Subject: [PATCH 002/116] NavigationBar: Don't use routes from React Router --- .../src/internal/components/navigation/NavigationBar.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/components/src/internal/components/navigation/NavigationBar.tsx b/packages/components/src/internal/components/navigation/NavigationBar.tsx index dae82650b7..73fe65a8c3 100644 --- a/packages/components/src/internal/components/navigation/NavigationBar.tsx +++ b/packages/components/src/internal/components/navigation/NavigationBar.tsx @@ -67,6 +67,7 @@ export const NavigationBarImpl: FC = memo(props => { children, extraDevItems, extraUserItems, + location, menuSectionConfigs, notificationsConfig, onSearch, @@ -81,7 +82,6 @@ export const NavigationBarImpl: FC = memo(props => { showSearchBox, signOutUrl, user, - routes, } = props; const { moduleContext } = useServerContext(); @@ -91,8 +91,8 @@ export const NavigationBarImpl: FC = memo(props => { }, [onSearch]); const isAdminPage = useMemo(() => { - return isAdminRoute(routes?.[1]?.path); - }, [routes]); + return isAdminRoute(location?.pathname); + }, [location]); const _searchPlaceholder = searchPlaceholder ?? getPrimaryAppProperties(moduleContext)?.searchPlaceholder ?? SEARCH_PLACEHOLDER; From f760b01652fa408d8ac4d6d74f80dde0d5d19246 Mon Sep 17 00:00:00 2001 From: Alan Vezina Date: Wed, 15 Nov 2023 10:54:42 -0600 Subject: [PATCH 003/116] Remove react-router, add react-router-dom, remove history --- packages/components/package-lock.json | 225 ++++++-------------------- packages/components/package.json | 4 +- 2 files changed, 49 insertions(+), 180 deletions(-) diff --git a/packages/components/package-lock.json b/packages/components/package-lock.json index 4693aa17b2..a86360119f 100644 --- a/packages/components/package-lock.json +++ b/packages/components/package-lock.json @@ -18,7 +18,6 @@ "enzyme": "~3.11.0", "font-awesome": "~4.7.0", "formsy-react": "~1.1.5", - "history": "~4.7.2", "immer": "~10.0.2", "immutable": "~3.8.2", "moment": "~2.29.3", @@ -31,7 +30,7 @@ "react-color": "~2.19.3", "react-datepicker": "~4.17.0", "react-dom": "~16.14.0", - "react-router": "~3.2.6", + "react-router-dom": "~6.18.0", "react-select": "~5.7.0", "react-treebeard": "~3.2.4", "vis-network": "~6.5.2" @@ -49,7 +48,6 @@ "@types/react-bootstrap": "~0.32.32", "@types/react-datepicker": "~4.15.0", "@types/react-dom": "~16.9.19", - "@types/react-router": "~3.0.28", "@types/react-test-renderer": "~16.9.6", "blob-polyfill": "7.0.20220408", "bootstrap-sass": "~3.4.3", @@ -3490,6 +3488,14 @@ "url": "https://opencollective.com/popperjs" } }, + "node_modules/@remix-run/router": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.11.0.tgz", + "integrity": "sha512-BHdhcWgeiudl91HvVa2wxqZjSHbheSgIiDvxrF1VjFzBzpTtuDPkOdOi3Iqvc08kXtFkLjhbS+ML9aM8mJS+wQ==", + "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", @@ -4132,22 +4138,6 @@ "redux": "^4.0.0" } }, - "node_modules/@types/react-router": { - "version": "3.0.28", - "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-3.0.28.tgz", - "integrity": "sha512-cf3V8fSBLAzmICC8l5qbnrJhdjF0ia8K0EZwL8IpJDBQN6ieHDdw8t1+9regufQxEVip1T4tCQRwhAmy26Zn2A==", - "dev": true, - "dependencies": { - "@types/history": "^3.2.5", - "@types/react": "*" - } - }, - "node_modules/@types/react-router/node_modules/@types/history": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/@types/history/-/history-3.2.5.tgz", - "integrity": "sha512-TqWYI0mlqS5qhH8MHgJJs0RcgvvZLxkn0bi2qK07liZqP7M9rl1kpJ6TCAUo5Cp4vP5DObIqHcky0MWyyQojVQ==", - "dev": true - }, "node_modules/@types/react-test-renderer": { "version": "16.9.7", "resolved": "https://registry.npmjs.org/@types/react-test-renderer/-/react-test-renderer-16.9.7.tgz", @@ -6547,15 +6537,6 @@ "node": ">=8" } }, - "node_modules/create-react-class": { - "version": "15.7.0", - "resolved": "https://registry.npmjs.org/create-react-class/-/create-react-class-15.7.0.tgz", - "integrity": "sha512-QZv4sFWG9S5RUvkTYWbflxeZX+JG7Cz0Tn33rQBJ+WFQTqTfUTjMjiv9tnfXazjsO5r0KhPs+AqCjyrQX6h2ng==", - "dependencies": { - "loose-envify": "^1.3.1", - "object-assign": "^4.1.1" - } - }, "node_modules/cross-env": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", @@ -9400,18 +9381,6 @@ "he": "bin/he" } }, - "node_modules/history": { - "version": "4.7.2", - "resolved": "https://registry.npmjs.org/history/-/history-4.7.2.tgz", - "integrity": "sha512-1zkBRWW6XweO0NBcjiphtVJVsIQ+SXF29z9DVkceeaSLVMFXHool+fdCZD4spDCfZJCILPILc3bm7Bc+HRi0nA==", - "dependencies": { - "invariant": "^2.2.1", - "loose-envify": "^1.2.0", - "resolve-pathname": "^2.2.0", - "value-equal": "^0.4.0", - "warning": "^3.0.0" - } - }, "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", @@ -14205,18 +14174,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/query-string": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/query-string/-/query-string-4.3.4.tgz", - "integrity": "sha512-O2XLNDBIg1DnTOa+2XrIwSiXEV8h2KImXUnjhhn2+UsvZ+Es2uyd5CCRTNQlDGbzUQOW3aYCBx9rVA6dzsiY7Q==", - "dependencies": { - "object-assign": "^4.1.0", - "strict-uri-encode": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/querystringify": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", @@ -14566,32 +14523,33 @@ } }, "node_modules/react-router": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-3.2.6.tgz", - "integrity": "sha512-nlxtQE8B22hb/JxdaslI1tfZacxFU8x8BJryXOnR2RxB4vc01zuHYAHAIgmBkdk1kzXaA25hZxK6KAH/+CXArw==", + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.18.0.tgz", + "integrity": "sha512-vk2y7Dsy8wI02eRRaRmOs9g2o+aE72YCx5q9VasT1N9v+lrdB79tIqrjMfByHiY5+6aYkH2rUa5X839nwWGPDg==", "dependencies": { - "create-react-class": "^15.5.1", - "history": "^3.0.0", - "hoist-non-react-statics": "^3.3.2", - "invariant": "^2.2.1", - "loose-envify": "^1.2.0", - "prop-types": "^15.7.2", - "react-is": "^16.13.0", - "warning": "^3.0.0" + "@remix-run/router": "1.11.0" + }, + "engines": { + "node": ">=14.0.0" }, "peerDependencies": { - "react": "^0.14.0 || ^15.0.0 || ^16.0.0" + "react": ">=16.8" } }, - "node_modules/react-router/node_modules/history": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/history/-/history-3.3.0.tgz", - "integrity": "sha512-ABLnJwKEZGXGqWsXaKYD8NNle49ZbKs1WEBlxrFsQ8dIudZpO5NJaH8WJOqh5lXVhAq7bHksfirrobBmrT7qBw==", + "node_modules/react-router-dom": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.18.0.tgz", + "integrity": "sha512-Ubrue4+Ercc/BoDkFQfc6og5zRQ4A8YxSO3Knsne+eRbZ+IepAsK249XBH/XaFuOYOYr3L3r13CXTLvYt5JDjw==", "dependencies": { - "invariant": "^2.2.1", - "loose-envify": "^1.2.0", - "query-string": "^4.2.2", - "warning": "^3.0.0" + "@remix-run/router": "1.11.0", + "react-router": "6.18.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" } }, "node_modules/react-select": { @@ -15054,11 +15012,6 @@ "node": ">=4" } }, - "node_modules/resolve-pathname": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-2.2.0.tgz", - "integrity": "sha512-bAFz9ld18RzJfddgrO2e/0S2O81710++chRMUxHjXOYKF6jTAMrUNZrEZ1PvV0zlhfjidm08iRPdTLPno1FuRg==" - }, "node_modules/resolve-url-loader": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-5.0.0.tgz", @@ -15896,14 +15849,6 @@ "node": ">= 0.4" } }, - "node_modules/strict-uri-encode": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", - "integrity": "sha512-R3f198pcvnB+5IpnBlRkphuE9n46WyVl8I39W/ZUTZLz4nqSP/oLYUrcnJrw462Ds8he4YKMov2efsTIw1BDGQ==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -16927,11 +16872,6 @@ "node": ">=10.12.0" } }, - "node_modules/value-equal": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-0.4.0.tgz", - "integrity": "sha512-x+cYdNnaA3CxvMaTX0INdTCN8m8aF2uY9BvEqmxuYp8bL09cs/kWVQPVGcA35fMktdOsP69IgU7wFj/61dJHEw==" - }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -20355,6 +20295,11 @@ "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==" }, + "@remix-run/router": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.11.0.tgz", + "integrity": "sha512-BHdhcWgeiudl91HvVa2wxqZjSHbheSgIiDvxrF1VjFzBzpTtuDPkOdOi3Iqvc08kXtFkLjhbS+ML9aM8mJS+wQ==" + }, "@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -20927,24 +20872,6 @@ "redux": "^4.0.0" } }, - "@types/react-router": { - "version": "3.0.28", - "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-3.0.28.tgz", - "integrity": "sha512-cf3V8fSBLAzmICC8l5qbnrJhdjF0ia8K0EZwL8IpJDBQN6ieHDdw8t1+9regufQxEVip1T4tCQRwhAmy26Zn2A==", - "dev": true, - "requires": { - "@types/history": "^3.2.5", - "@types/react": "*" - }, - "dependencies": { - "@types/history": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/@types/history/-/history-3.2.5.tgz", - "integrity": "sha512-TqWYI0mlqS5qhH8MHgJJs0RcgvvZLxkn0bi2qK07liZqP7M9rl1kpJ6TCAUo5Cp4vP5DObIqHcky0MWyyQojVQ==", - "dev": true - } - } - }, "@types/react-test-renderer": { "version": "16.9.7", "resolved": "https://registry.npmjs.org/@types/react-test-renderer/-/react-test-renderer-16.9.7.tgz", @@ -22747,15 +22674,6 @@ } } }, - "create-react-class": { - "version": "15.7.0", - "resolved": "https://registry.npmjs.org/create-react-class/-/create-react-class-15.7.0.tgz", - "integrity": "sha512-QZv4sFWG9S5RUvkTYWbflxeZX+JG7Cz0Tn33rQBJ+WFQTqTfUTjMjiv9tnfXazjsO5r0KhPs+AqCjyrQX6h2ng==", - "requires": { - "loose-envify": "^1.3.1", - "object-assign": "^4.1.1" - } - }, "cross-env": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", @@ -24834,18 +24752,6 @@ "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", "dev": true }, - "history": { - "version": "4.7.2", - "resolved": "https://registry.npmjs.org/history/-/history-4.7.2.tgz", - "integrity": "sha512-1zkBRWW6XweO0NBcjiphtVJVsIQ+SXF29z9DVkceeaSLVMFXHool+fdCZD4spDCfZJCILPILc3bm7Bc+HRi0nA==", - "requires": { - "invariant": "^2.2.1", - "loose-envify": "^1.2.0", - "resolve-pathname": "^2.2.0", - "value-equal": "^0.4.0", - "warning": "^3.0.0" - } - }, "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", @@ -28344,15 +28250,6 @@ "side-channel": "^1.0.4" } }, - "query-string": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/query-string/-/query-string-4.3.4.tgz", - "integrity": "sha512-O2XLNDBIg1DnTOa+2XrIwSiXEV8h2KImXUnjhhn2+UsvZ+Es2uyd5CCRTNQlDGbzUQOW3aYCBx9rVA6dzsiY7Q==", - "requires": { - "object-assign": "^4.1.0", - "strict-uri-encode": "^1.0.0" - } - }, "querystringify": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", @@ -28616,31 +28513,20 @@ } }, "react-router": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-3.2.6.tgz", - "integrity": "sha512-nlxtQE8B22hb/JxdaslI1tfZacxFU8x8BJryXOnR2RxB4vc01zuHYAHAIgmBkdk1kzXaA25hZxK6KAH/+CXArw==", + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.18.0.tgz", + "integrity": "sha512-vk2y7Dsy8wI02eRRaRmOs9g2o+aE72YCx5q9VasT1N9v+lrdB79tIqrjMfByHiY5+6aYkH2rUa5X839nwWGPDg==", "requires": { - "create-react-class": "^15.5.1", - "history": "^3.0.0", - "hoist-non-react-statics": "^3.3.2", - "invariant": "^2.2.1", - "loose-envify": "^1.2.0", - "prop-types": "^15.7.2", - "react-is": "^16.13.0", - "warning": "^3.0.0" - }, - "dependencies": { - "history": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/history/-/history-3.3.0.tgz", - "integrity": "sha512-ABLnJwKEZGXGqWsXaKYD8NNle49ZbKs1WEBlxrFsQ8dIudZpO5NJaH8WJOqh5lXVhAq7bHksfirrobBmrT7qBw==", - "requires": { - "invariant": "^2.2.1", - "loose-envify": "^1.2.0", - "query-string": "^4.2.2", - "warning": "^3.0.0" - } - } + "@remix-run/router": "1.11.0" + } + }, + "react-router-dom": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.18.0.tgz", + "integrity": "sha512-Ubrue4+Ercc/BoDkFQfc6og5zRQ4A8YxSO3Knsne+eRbZ+IepAsK249XBH/XaFuOYOYr3L3r13CXTLvYt5JDjw==", + "requires": { + "@remix-run/router": "1.11.0", + "react-router": "6.18.0" } }, "react-select": { @@ -29004,11 +28890,6 @@ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==" }, - "resolve-pathname": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-2.2.0.tgz", - "integrity": "sha512-bAFz9ld18RzJfddgrO2e/0S2O81710++chRMUxHjXOYKF6jTAMrUNZrEZ1PvV0zlhfjidm08iRPdTLPno1FuRg==" - }, "resolve-url-loader": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-5.0.0.tgz", @@ -29622,11 +29503,6 @@ "internal-slot": "^1.0.4" } }, - "strict-uri-encode": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", - "integrity": "sha512-R3f198pcvnB+5IpnBlRkphuE9n46WyVl8I39W/ZUTZLz4nqSP/oLYUrcnJrw462Ds8he4YKMov2efsTIw1BDGQ==" - }, "string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -30340,11 +30216,6 @@ "convert-source-map": "^1.6.0" } }, - "value-equal": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-0.4.0.tgz", - "integrity": "sha512-x+cYdNnaA3CxvMaTX0INdTCN8m8aF2uY9BvEqmxuYp8bL09cs/kWVQPVGcA35fMktdOsP69IgU7wFj/61dJHEw==" - }, "vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", diff --git a/packages/components/package.json b/packages/components/package.json index f78de0e2aa..050abc5f15 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -59,7 +59,6 @@ "enzyme": "~3.11.0", "font-awesome": "~4.7.0", "formsy-react": "~1.1.5", - "history": "~4.7.2", "immer": "~10.0.2", "immutable": "~3.8.2", "moment": "~2.29.3", @@ -72,7 +71,7 @@ "react-color": "~2.19.3", "react-datepicker": "~4.17.0", "react-dom": "~16.14.0", - "react-router": "~3.2.6", + "react-router-dom": "~6.18.0", "react-select": "~5.7.0", "react-treebeard": "~3.2.4", "vis-network": "~6.5.2" @@ -90,7 +89,6 @@ "@types/react-bootstrap": "~0.32.32", "@types/react-datepicker": "~4.15.0", "@types/react-dom": "~16.9.19", - "@types/react-router": "~3.0.28", "@types/react-test-renderer": "~16.9.6", "blob-polyfill": "7.0.20220408", "bootstrap-sass": "~3.4.3", From 46321d80887451987a11aa1e97e96e8ef0ab0c73 Mon Sep 17 00:00:00 2001 From: Alan Vezina Date: Wed, 15 Nov 2023 10:55:23 -0600 Subject: [PATCH 004/116] ProductMenu: improve helpers --- .../components/navigation/ProductMenu.tsx | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/packages/components/src/internal/components/navigation/ProductMenu.tsx b/packages/components/src/internal/components/navigation/ProductMenu.tsx index 675257119e..d5df55343c 100644 --- a/packages/components/src/internal/components/navigation/ProductMenu.tsx +++ b/packages/components/src/internal/components/navigation/ProductMenu.tsx @@ -305,7 +305,7 @@ export const ProductMenuButtonTitle: FC = memo(props => }, [container.path, container.title, folderItems?.length]); const subtitle = useMemo(() => { - return getHeaderMenuSubtitle(location?.pathname?.split('/')[1]); + return getHeaderMenuSubtitle(location?.pathname); }, [location]); return ( @@ -358,11 +358,17 @@ const HEADER_MENU_SUBTITLE_MAP = { [WORKFLOW_KEY]: 'Workflow', }; +function getBaseRoute(pathname: string) { + // pathname is prefixed with, and split up, by /, so the first segment is always an empty string when splitting, the + // second is the base route. + return pathname?.toLowerCase().split('/')[1]; +} + // export for jest testing -export function getHeaderMenuSubtitle(baseRoute: string) { - return HEADER_MENU_SUBTITLE_MAP[baseRoute?.toLowerCase()] ?? 'Dashboard'; +export function getHeaderMenuSubtitle(pathname: string) { + return HEADER_MENU_SUBTITLE_MAP[getBaseRoute(pathname)] ?? 'Dashboard'; } -export function isAdminRoute(baseRoute: string) { - return HEADER_MENU_SUBTITLE_MAP[baseRoute?.toLowerCase()] === 'Administration'; +export function isAdminRoute(pathname: string) { + return HEADER_MENU_SUBTITLE_MAP[getBaseRoute(pathname)] === 'Administration'; } From 09a725bc40b5ffcf404cb9f6e2cf59f0e8eb4317 Mon Sep 17 00:00:00 2001 From: Alan Vezina Date: Wed, 15 Nov 2023 10:56:37 -0600 Subject: [PATCH 005/116] Don't export Location --- packages/components/src/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts index 9c04349465..4fa2bd7c8d 100644 --- a/packages/components/src/index.ts +++ b/packages/components/src/index.ts @@ -1792,7 +1792,6 @@ export type { WithFormStepsProps } from './internal/components/forms/FormStep'; export type { BulkAddData, SharedEditableGridPanelProps } from './internal/components/editable/EditableGrid'; export type { IImportData, ISelectRowsResult } from './internal/query/api'; export type { Row, RowValue, SelectRowsOptions, SelectRowsResponse } from './internal/query/selectRows'; -export type { Location } from './internal/util/URL'; export type { RoutingTableState, ServerNotificationState, From 783108a8fd34a0ecc7d66540b7ba883372d2e8db Mon Sep 17 00:00:00 2001 From: Alan Vezina Date: Wed, 15 Nov 2023 11:19:54 -0600 Subject: [PATCH 006/116] Add withRouterDeprecated A convenience wrapper to help us transition from react-router 3 to 6. It is not a fully backwards compatible change, but it is mostly backwards compatible. --- .../components/src/internal/routerTypes.ts | 19 ++++++ packages/components/src/internal/util/URL.ts | 63 ++++++++++++------- .../src/internal/withRouterDeprecated.tsx | 50 +++++++++++++++ 3 files changed, 108 insertions(+), 24 deletions(-) create mode 100644 packages/components/src/internal/routerTypes.ts create mode 100644 packages/components/src/internal/withRouterDeprecated.tsx diff --git a/packages/components/src/internal/routerTypes.ts b/packages/components/src/internal/routerTypes.ts new file mode 100644 index 0000000000..b645043cf3 --- /dev/null +++ b/packages/components/src/internal/routerTypes.ts @@ -0,0 +1,19 @@ +import { Location } from 'history'; + +// Note: this file can probably be removed once we stop using the deprecated types below, right now we need the types +// in utils/URL.ts and withRouterDeprecated which would create a circular dependency. + +export type QueryParams = Record; + +export interface DeprecatedRouter { + goBack: () => void; + goForward: () => void; + push: (string) => void; + replace: (string) => void; +} + +export interface DeprecatedLocation extends Location { + // You should not be relying on this Location type, instead you should be using the Location type from the History + // library, and you should be using something like: const queryParams = new URLSearchParams(location.search); + query: QueryParams; +} diff --git a/packages/components/src/internal/util/URL.ts b/packages/components/src/internal/util/URL.ts index fcc1a3e4e3..ef63628a3b 100644 --- a/packages/components/src/internal/util/URL.ts +++ b/packages/components/src/internal/util/URL.ts @@ -13,18 +13,40 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { InjectedRouter, WithRouterProps } from 'react-router'; +import { Location } from 'history'; +import { SetURLSearchParams } from 'react-router-dom'; -// We export Location like this in order to avoid having a direct dependency on the History library -export type Location = WithRouterProps['location']; +import { DeprecatedRouter, QueryParams } from '../routerTypes'; -function setParameters( - router: InjectedRouter, - location: Location, - params: Record, - asReplace = false -): void { - const query = { ...location.query }; +/** + * Takes a search string (typically from Location) and converts it to a QueryParams object. If the same key is used + * multiple times in a search string (e.g. ?foo=bar&foo=baz) it will return an array of values for that key, if the key + * only appears once it will return a string for that key. If you know the key you are looking for will not be an array + * use: const myValue = getQueryParams(search).myValue as string; + * @param search + */ +export function getQueryParams(search: string): QueryParams { + if (!search) return {}; + const paramsArray = new URLSearchParams(search).entries(); + return [...paramsArray].reduce((result, tuple) => { + const [key, value] = tuple; + if (result.hasOwnProperty(key)) { + if (Array.isArray(result[key])) { + result[key].push(value); + } else { + result[key] = [result[key], value]; + } + } else { + result[key] = value; + } + return result; + }, {}); +} + +// TODO: convert this to use SetURLSearchParams, which negates the need for router and location as it is a React style +// useState setter that lets us pass exactly what we want, or a function that accesses the current params while setting +function setParameters(router: DeprecatedRouter, location: Location, params: QueryParams, asReplace = false): void { + const query = getQueryParams(location.search); Object.keys(params).forEach(key => { const value = params[key]; @@ -43,7 +65,7 @@ function setParameters( } } -export function removeParameters(router: InjectedRouter, location: Location, ...params: string[]): void { +export function removeParameters(router: DeprecatedRouter, location: Location, ...params: string[]): void { if (!params) return; const paramsObj = params.reduce((result, param) => { result[param] = undefined; @@ -52,26 +74,19 @@ export function removeParameters(router: InjectedRouter, location: Location, ... setParameters(router, location, paramsObj, true); } -export function replaceParameters( - router: InjectedRouter, - location: Location, - params: Record -): void { +export function replaceParameters(router: DeprecatedRouter, location: Location, params: QueryParams): void { setParameters(router, location, params, true); } -export function pushParameters( - router: InjectedRouter, - location: Location, - params: Record -): void { +export function pushParameters(router: DeprecatedRouter, location: Location, params: QueryParams): void { setParameters(router, location, params); } -export function resetParameters(router: InjectedRouter, location: Location, except: string[] = []): void { - const updatedParams = Object.keys(location.query).reduce((result, key: string) => { +export function resetParameters(router: DeprecatedRouter, location: Location, except: string[] = []): void { + const currentParams = getQueryParams(location.search); + const updatedParams = Object.keys(currentParams).reduce((result, key: string) => { if (except.indexOf(key) > -1) { - result[key] = location.query[key]; + result[key] = currentParams[key]; } else { result[key] = undefined; } diff --git a/packages/components/src/internal/withRouterDeprecated.tsx b/packages/components/src/internal/withRouterDeprecated.tsx new file mode 100644 index 0000000000..8a7a3a9ec2 --- /dev/null +++ b/packages/components/src/internal/withRouterDeprecated.tsx @@ -0,0 +1,50 @@ +import React, { Component, ComponentType, FC, useMemo } from 'react'; +import { Params, useLocation, useNavigate, useParams } from 'react-router-dom'; +import { DeprecatedLocation, DeprecatedRouter } from './routerTypes'; +import { getQueryParams } from './util/URL'; + +export interface DeprecatedWithRouterProps { + router: DeprecatedRouter; + location: DeprecatedLocation; + params: Params; +} + +type WithRouterComponent = ComponentType; + +/** + * @deprecated This wrapper is so we can transition to React Router 6 over time, without having to refactor nearly all + * of our page components, and many other components that used withRouter from React Router 3. If you are writing a new + * component you should not use this, instead use the official React Router hooks. + * @param Component + */ +export function withRouterDeprecated(Component: WithRouterComponent): ComponentType { + const Wrapped: FC = (props: T) => { + const navigate = useNavigate(); + const params = useParams(); + const location = useLocation(); + const router = useMemo( + () => ({ + goBack: () => { + navigate(-1); + }, + goForward: () => { + navigate(1); + }, + push: path => { + navigate(path); + }, + replace: path => { + navigate(path, { replace: true }); + }, + }), + [navigate] + ); + const deprecatedLocation = { + ...location, + query: getQueryParams(location.search), + }; + return ; + }; + + return Wrapped; +} From 7c3a24b10eaba7d116ec80acc152a521249a3094 Mon Sep 17 00:00:00 2001 From: Alan Vezina Date: Wed, 15 Nov 2023 15:21:16 -0600 Subject: [PATCH 007/116] Temporarily disable useRouteLeave --- .../components/assay/AssayImportPanels.tsx | 10 +++++----- .../components/lineage/grid/LineageGrid.tsx | 16 +++++++-------- .../src/internal/util/RouteLeave.tsx | 20 ++++++++++--------- .../src/internal/withRouterDeprecated.tsx | 3 +++ 4 files changed, 26 insertions(+), 23 deletions(-) diff --git a/packages/components/src/internal/components/assay/AssayImportPanels.tsx b/packages/components/src/internal/components/assay/AssayImportPanels.tsx index 5bd15aef07..7f7879418c 100644 --- a/packages/components/src/internal/components/assay/AssayImportPanels.tsx +++ b/packages/components/src/internal/components/assay/AssayImportPanels.tsx @@ -33,10 +33,10 @@ import { AssayDefinitionModel, AssayDomainTypes } from '../../AssayDefinitionMod import { AssayUploadTabs } from '../../constants'; import { FormButtons } from '../../FormButtons'; import { getQueryDetails } from '../../query/api'; +import { DeprecatedLocation } from '../../routerTypes'; import { SCHEMAS } from '../../schemas'; import { getActionErrorMessage, resolveErrorMessage } from '../../util/messaging'; -import { Location } from '../../util/URL'; import { Alert } from '../base/Alert'; import { LoadingSpinner } from '../base/LoadingSpinner'; import { Container } from '../base/models/Container'; @@ -102,8 +102,8 @@ interface OwnProps { fileSizeLimits?: Map; getIsDirty?: () => boolean; jobNotificationProvider?: string; - loadSelectedSamples?: (location: Location, sampleColumn: QueryColumn) => Promise>; - location?: Location; + loadSelectedSamples?: (location: DeprecatedLocation, sampleColumn: QueryColumn) => Promise>; + location?: DeprecatedLocation; // Not currently used, but related logic retained in component maxRows?: number; onCancel: () => void; @@ -165,7 +165,7 @@ class AssayImportPanelsBody extends Component { const { location, selectStep } = this.props; if (location?.query?.dataTab) { - selectStep(parseInt(location.query.dataTab, 10)); + selectStep(parseInt(location.query.dataTab as string, 10)); } this.initModel(); @@ -244,7 +244,7 @@ class AssayImportPanelsBody extends Component { let workflowTask: number; if (location?.query?.workflowTaskId) { - const _workflowTask = parseInt(location.query.workflowTaskId, 10); + const _workflowTask = parseInt(location.query.workflowTaskId as string, 10); workflowTask = isNaN(_workflowTask) ? undefined : _workflowTask; } diff --git a/packages/components/src/internal/components/lineage/grid/LineageGrid.tsx b/packages/components/src/internal/components/lineage/grid/LineageGrid.tsx index 1317293e3f..502d50d29e 100644 --- a/packages/components/src/internal/components/lineage/grid/LineageGrid.tsx +++ b/packages/components/src/internal/components/lineage/grid/LineageGrid.tsx @@ -2,14 +2,15 @@ * Copyright (c) 2018-2019 LabKey Corporation. All rights reserved. No portion of this work may be reproduced in * any form or by any electronic or mechanical means without written permission from LabKey Corporation. */ -import React, { FC, memo, PureComponent, ReactNode } from 'react'; +import React, { FC, memo, PureComponent, ReactNode, useMemo } from 'react'; import { Draft, produce } from 'immer'; +import { Location } from 'history'; +import { useSearchParams } from 'react-router-dom'; import { createGridModel } from '../actions'; import { LineageGridModel } from '../models'; import { InjectedLineage, withLineage, WithLineageOptions } from '../withLineage'; import { LINEAGE_DIRECTIONS } from '../types'; -import { Location } from '../../../util/URL'; import { LineageGridDisplay } from './LineageGridDisplay'; @@ -59,18 +60,15 @@ function ensureNumber(value: string): number { return isNaN(numValue) ? undefined : numValue; } -export interface LineageGridFromLocationProps { - location: Location; -} - -export const LineageGridFromLocation: FC = memo(({ location }) => { - const { distance, members, p, seeds } = location.query; +export const LineageGridFromLocation: FC = memo(() => { + const [searchParams, _] = useSearchParams(); + const { distance, members, p, seeds } = useMemo(() => Object.fromEntries(searchParams.entries()), [searchParams]); return ( ); diff --git a/packages/components/src/internal/util/RouteLeave.tsx b/packages/components/src/internal/util/RouteLeave.tsx index 3945f91030..9042539eba 100644 --- a/packages/components/src/internal/util/RouteLeave.tsx +++ b/packages/components/src/internal/util/RouteLeave.tsx @@ -1,5 +1,6 @@ import React, { ComponentType, FC, useCallback, useEffect, useMemo, useRef } from 'react'; -import { InjectedRouter, withRouter, WithRouterProps, PlainRoute } from 'react-router'; +import { DeprecatedRouter } from '../routerTypes'; +import { DeprecatedWithRouterProps, withRouterDeprecated } from '../withRouterDeprecated'; export const CONFIRM_MESSAGE = 'You have unsaved changes that will be lost. Are you sure you want to continue?'; @@ -26,9 +27,10 @@ type GetSetIsDirty = [() => boolean, (dirty: boolean) => void]; * navigating away from the page, not when closing the tab or browser window. Browsers do not let you customize the * message displayed when the browser/tab is closed. */ +// FIXME: use the appropriate RR6 APIs, do not merge until that is completed export const useRouteLeave = ( - router: InjectedRouter, - routes: PlainRoute[], + router?: DeprecatedRouter, + routes?: any[], confirmMessage = CONFIRM_MESSAGE ): GetSetIsDirty => { const initialHistoryLength = useMemo(() => history.length, []); @@ -93,7 +95,7 @@ export const useRouteLeave = ( const currentRoute = routes?.[routes.length - 1]; // setRouteLeaveHook returns a cleanup function. return router?.setRouteLeaveHook(currentRoute, onRouteLeave); - }, [onRouteLeave]); + }, [onRouteLeave, router, routes]); useEffect(() => { window.addEventListener('beforeunload', beforeUnload); @@ -107,16 +109,16 @@ export const useRouteLeave = ( }; export function withRouteLeave( - Component: ComponentType + Component: ComponentType ): ComponentType { - const wrapped: FC = props => { - const { router, routes, confirmMessage } = props; - const [getIsDirty, setIsDirty] = useRouteLeave(router, routes, confirmMessage); + const wrapped: FC = props => { + const { router, confirmMessage } = props; + const [getIsDirty, setIsDirty] = useRouteLeave(router, undefined, confirmMessage); return ; }; - return withRouter(wrapped) as FC; + return withRouterDeprecated(wrapped) as FC; } export interface WithDirtyCheckLinkProps { diff --git a/packages/components/src/internal/withRouterDeprecated.tsx b/packages/components/src/internal/withRouterDeprecated.tsx index 8a7a3a9ec2..e5ebcffd3d 100644 --- a/packages/components/src/internal/withRouterDeprecated.tsx +++ b/packages/components/src/internal/withRouterDeprecated.tsx @@ -36,6 +36,9 @@ export function withRouterDeprecated(Component: WithRouterComponent): Comp replace: path => { navigate(path, { replace: true }); }, + setRouteLeaveHook: () => { + // FIXME: temporary no-op while I get stuff to compile, this will be removed before merging + }, }), [navigate] ); From 61e07152aae0577cb863bb0217af02c2741ffbda Mon Sep 17 00:00:00 2001 From: Alan Vezina Date: Wed, 15 Nov 2023 15:23:37 -0600 Subject: [PATCH 008/116] Query pages: use RR hooks --- .../components/listing/QueriesListing.tsx | 15 ++++------ .../components/listing/SchemaListing.tsx | 5 ++-- .../listing/pages/QueriesListingPage.tsx | 30 +++++++++---------- .../listing/pages/QueryDetailPage.tsx | 12 ++++---- .../listing/pages/QueryListingPage.tsx | 12 ++++---- 5 files changed, 33 insertions(+), 41 deletions(-) diff --git a/packages/components/src/internal/components/listing/QueriesListing.tsx b/packages/components/src/internal/components/listing/QueriesListing.tsx index aa8e9802a4..20a45f3614 100644 --- a/packages/components/src/internal/components/listing/QueriesListing.tsx +++ b/packages/components/src/internal/components/listing/QueriesListing.tsx @@ -14,11 +14,11 @@ * limitations under the License. */ import React, { Component, ReactNode } from 'react'; -import { Link } from 'react-router'; -import { fromJS, List, Map } from 'immutable'; +import { fromJS, List } from 'immutable'; import { Query } from '@labkey/api'; import { GridColumn } from '../base/models/GridColumn'; +import { QueryInfo } from '../../../public/QueryInfo'; import { AppURL } from '../../url/AppURL'; import { naturalSortByProperty } from '../../../public/sort'; import { Grid } from '../base/Grid'; @@ -35,14 +35,9 @@ const columns = List([ new GridColumn({ index: 'name', title: 'Name', - cell: (name: string, map: Map) => { - if (name && map) { - const queryData: QueryData = map.toJS(); - return ( - - {queryData.title} - - ); + cell: (name: string, info: QueryInfo) => { + if (name && info) { + return {info.title}; } return name; }, diff --git a/packages/components/src/internal/components/listing/SchemaListing.tsx b/packages/components/src/internal/components/listing/SchemaListing.tsx index 46f9b2f5e0..8f5a9c5412 100644 --- a/packages/components/src/internal/components/listing/SchemaListing.tsx +++ b/packages/components/src/internal/components/listing/SchemaListing.tsx @@ -14,7 +14,6 @@ * limitations under the License. */ import React, { FC, memo, useEffect, useState } from 'react'; -import { Link } from 'react-router'; import { List } from 'immutable'; import { Query } from '@labkey/api'; @@ -36,9 +35,9 @@ const columns = List([ cell: (schemaName: string, details: SchemaDetails) => { if (details) { return ( - + {schemaName} - + ); } diff --git a/packages/components/src/internal/components/listing/pages/QueriesListingPage.tsx b/packages/components/src/internal/components/listing/pages/QueriesListingPage.tsx index c806cb28c5..7e06266a73 100644 --- a/packages/components/src/internal/components/listing/pages/QueriesListingPage.tsx +++ b/packages/components/src/internal/components/listing/pages/QueriesListingPage.tsx @@ -2,8 +2,8 @@ * Copyright (c) 2016-2018 LabKey Corporation. All rights reserved. No portion of this work may be reproduced in * any form or by any electronic or mechanical means without written permission from LabKey Corporation. */ -import React, { Component, ReactNode } from 'react'; -import { Link, WithRouterProps } from 'react-router'; +import React, { FC, memo } from 'react'; +import { useParams } from 'react-router-dom'; import { QueriesListing } from '../QueriesListing'; import { Page } from '../../base/Page'; @@ -11,18 +11,16 @@ import { Breadcrumb } from '../../navigation/Breadcrumb'; import { AppURL } from '../../../url/AppURL'; import { PageHeader } from '../../base/PageHeader'; -export class QueriesListingPage extends Component { - render = (): ReactNode => { - const { schema } = this.props.params; +export const QueriesListingPage: FC = memo(() => { + const { schema } = useParams(); - return ( - - - Schemas - - - - - ); - }; -} + return ( + + + Schemas + + + + + ); +}); diff --git a/packages/components/src/internal/components/listing/pages/QueryDetailPage.tsx b/packages/components/src/internal/components/listing/pages/QueryDetailPage.tsx index 7992992f3c..15ad047b99 100644 --- a/packages/components/src/internal/components/listing/pages/QueryDetailPage.tsx +++ b/packages/components/src/internal/components/listing/pages/QueryDetailPage.tsx @@ -3,7 +3,7 @@ * any form or by any electronic or mechanical means without written permission from LabKey Corporation. */ import React, { FC, PureComponent, memo, ReactNode, useMemo } from 'react'; -import { Link, WithRouterProps } from 'react-router'; +import { useParams } from 'react-router-dom'; import { InjectedQueryModels, withQueryModels } from '../../../../public/QueryModel/withQueryModels'; import { AppURL } from '../../../url/AppURL'; @@ -64,9 +64,9 @@ class DetailBodyImpl extends PureComponent { return ( - Schemas - {schemaLabel} - {plural} + Schemas + {schemaLabel} + {plural} {title && } @@ -77,8 +77,8 @@ class DetailBodyImpl extends PureComponent { const DetailBody = withQueryModels(DetailBodyImpl); -export const QueryDetailPage: FC = memo(({ params }) => { - const { schema, query, id } = params; +export const QueryDetailPage: FC = memo(() => { + const { schema, query, id } = useParams(); const modelId = `q.details.${schema}.${query}.${id}`; const queryConfigs = useMemo( diff --git a/packages/components/src/internal/components/listing/pages/QueryListingPage.tsx b/packages/components/src/internal/components/listing/pages/QueryListingPage.tsx index b3c4a56dbf..20ffaa27d1 100644 --- a/packages/components/src/internal/components/listing/pages/QueryListingPage.tsx +++ b/packages/components/src/internal/components/listing/pages/QueryListingPage.tsx @@ -3,7 +3,7 @@ * any form or by any electronic or mechanical means without written permission from LabKey Corporation. */ import React, { FC, memo, useMemo } from 'react'; -import { Link, WithRouterProps } from 'react-router'; +import { useParams } from 'react-router-dom'; import { InjectedQueryModels, QueryConfigMap, withQueryModels } from '../../../../public/QueryModel/withQueryModels'; import { Page } from '../../base/Page'; @@ -28,8 +28,8 @@ const QueryListingBodyImpl: FC = memo(({ action {queryInfo !== undefined && ( - Schemas - {schemaTitle} + Schemas + {schemaTitle} )} @@ -42,8 +42,8 @@ const QueryListingBodyImpl: FC = memo(({ action const QueryListingBody = withQueryModels(QueryListingBodyImpl); -export const QueryListingPage: FC = ({ params }) => { - const { schema, query } = params; +export const QueryListingPage: FC = () => { + const { schema, query } = useParams(); const modelId = `q.${schema}.${query}`; const queryConfigs: QueryConfigMap = useMemo( () => ({ @@ -54,7 +54,7 @@ export const QueryListingPage: FC = ({ params }) => { schemaQuery: new SchemaQuery(schema, query), }, }), - [modelId] + [modelId, query, schema] ); // Key is used here so that if the schema or query change via the URL we remount the component which will From 1666c3fa855aba0966689581cab12b799d43dc41 Mon Sep 17 00:00:00 2001 From: Alan Vezina Date: Wed, 15 Nov 2023 15:24:39 -0600 Subject: [PATCH 009/116] SubNav/NavItem: use RR hooks --- .../src/internal/components/navigation/NavItem.tsx | 8 +++----- .../src/internal/components/navigation/SubNav.tsx | 2 +- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/components/src/internal/components/navigation/NavItem.tsx b/packages/components/src/internal/components/navigation/NavItem.tsx index 93bdb1dd2a..c9686eae3d 100644 --- a/packages/components/src/internal/components/navigation/NavItem.tsx +++ b/packages/components/src/internal/components/navigation/NavItem.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ import React, { FC, memo, useEffect, useRef, useState } from 'react'; -import { withRouter, WithRouterProps } from 'react-router'; +import { useLocation } from 'react-router-dom'; import { AppURL } from '../../url/AppURL'; @@ -24,7 +24,8 @@ interface NavItemProps { to?: string | AppURL; } -const NavItemImpl: FC = memo(({ children, location, onActive, to, onClick }) => { +export const NavItem: FC = memo(({ children, onActive, to, onClick }) => { + const location = useLocation(); const href = to instanceof AppURL ? to.toHref() : to; const itemRef = useRef(); const [active, setActive] = useState(false); @@ -55,9 +56,6 @@ const NavItemImpl: FC = memo(({ children, locati ); }); -// Export as "default" to avoid erroneous type warning use of withRouter() -export default withRouter(NavItemImpl); - export const ParentNavItem: FC = memo(({ children, to, onClick }) => { const href = to instanceof AppURL ? to.toHref() : to; diff --git a/packages/components/src/internal/components/navigation/SubNav.tsx b/packages/components/src/internal/components/navigation/SubNav.tsx index bf52c47c7a..d56433f987 100644 --- a/packages/components/src/internal/components/navigation/SubNav.tsx +++ b/packages/components/src/internal/components/navigation/SubNav.tsx @@ -23,7 +23,7 @@ import { useServerContext } from '../base/ServerContext'; import { hasPremiumModule, hasProductProjects } from '../../app/utils'; -import NavItem, { ParentNavItem } from './NavItem'; +import { NavItem, ParentNavItem } from './NavItem'; import { ITab, SubNavGlobalContext } from './types'; import { useSubNavContext } from './hooks'; From 9733f722687696e5a3ef1cdb1a33bc8c5b8bbb6f Mon Sep 17 00:00:00 2001 From: Alan Vezina Date: Wed, 15 Nov 2023 15:25:32 -0600 Subject: [PATCH 010/116] ProductMenu: use RR hooks --- .../internal/components/navigation/ProductMenu.tsx | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/components/src/internal/components/navigation/ProductMenu.tsx b/packages/components/src/internal/components/navigation/ProductMenu.tsx index d5df55343c..31138952a3 100644 --- a/packages/components/src/internal/components/navigation/ProductMenu.tsx +++ b/packages/components/src/internal/components/navigation/ProductMenu.tsx @@ -17,9 +17,9 @@ import React, { MouseEvent, FC, memo, useCallback, useState, useEffect, useRef, import classNames from 'classnames'; import { List, Map } from 'immutable'; import { DropdownButton } from 'react-bootstrap'; -import { withRouter, WithRouterProps } from 'react-router'; +import { Location } from 'history'; import { ActionURL } from '@labkey/api'; -import { Location } from '../../util/URL'; +import { useLocation } from 'react-router-dom'; import { blurActiveElement } from '../../util/utils'; import { LoadingSpinner } from '../base/LoadingSpinner'; @@ -65,8 +65,9 @@ export interface ProductMenuButtonProps { showFolderMenu: boolean; } -const ProductMenuButtonImpl: FC = memo(props => { - const { appProperties = getCurrentAppProperties(), location } = props; +export const ProductMenuButton: FC = memo(props => { + const { appProperties = getCurrentAppProperties() } = props; + const location = useLocation(); const [menuOpen, setMenuOpen] = useState(false); const [error, setError] = useState(); const [loading, setLoading] = useState(LoadingState.INITIALIZED); @@ -149,9 +150,6 @@ const ProductMenuButtonImpl: FC = memo ); }); - -export const ProductMenuButton = withRouter(ProductMenuButtonImpl); - export interface ProductMenuProps extends ProductMenuButtonProps { className: string; error: string; From 2a2c9cd8bef9f56433d59859d2399bd22160e84e Mon Sep 17 00:00:00 2001 From: Alan Vezina Date: Wed, 15 Nov 2023 15:27:02 -0600 Subject: [PATCH 011/116] ProfilePage: don't use withRouter --- .../src/internal/components/user/ProfilePage.tsx | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/components/src/internal/components/user/ProfilePage.tsx b/packages/components/src/internal/components/user/ProfilePage.tsx index dd98fbb230..c6717bd8ad 100644 --- a/packages/components/src/internal/components/user/ProfilePage.tsx +++ b/packages/components/src/internal/components/user/ProfilePage.tsx @@ -4,7 +4,6 @@ */ import React, { FC, useCallback, useState } from 'react'; import { Button } from 'react-bootstrap'; -import { WithRouterProps } from 'react-router'; import { isLoginAutoRedirectEnabled } from '../administration/utils'; import { InsufficientPermissionsPage } from '../permissions/InsufficientPermissionsPage'; @@ -31,15 +30,15 @@ import { ChangePasswordModal } from './ChangePasswordModal'; import { useUserProperties } from './hooks'; -interface Props extends WithRouterProps { +interface Props { updateUserDisplayName: (displayName: string) => void; } const TITLE = 'User Profile'; -const ProfilePageImpl: FC = props => { - const { router, routes, updateUserDisplayName } = props; - const [_, setIsDirty] = useRouteLeave(router, routes); +export const ProfilePage: FC = props => { + const { updateUserDisplayName } = props; + const [_, setIsDirty] = useRouteLeave(); const [showChangePassword, setShowChangePassword] = useState(false); const { moduleContext, user } = useServerContext(); const userProperties = useUserProperties(user); @@ -108,5 +107,3 @@ const ProfilePageImpl: FC = props => { ); }; - -export const ProfilePage = ProfilePageImpl; From 0d1e4cf8206ac70ccde4b7e7eb49dbb0f06a4555 Mon Sep 17 00:00:00 2001 From: Alan Vezina Date: Wed, 15 Nov 2023 15:27:45 -0600 Subject: [PATCH 012/116] AuditQueriesListingPage: Use RR hooks --- .../auditlog/AuditQueriesListingPage.tsx | 43 ++++++++++--------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/packages/components/src/internal/components/auditlog/AuditQueriesListingPage.tsx b/packages/components/src/internal/components/auditlog/AuditQueriesListingPage.tsx index e024eec98d..ac497683bb 100644 --- a/packages/components/src/internal/components/auditlog/AuditQueriesListingPage.tsx +++ b/packages/components/src/internal/components/auditlog/AuditQueriesListingPage.tsx @@ -6,8 +6,7 @@ import React, { FC, memo, useCallback, useEffect, useMemo, useState } from 'reac import { fromJS } from 'immutable'; import { Col, Row } from 'react-bootstrap'; import { Filter } from '@labkey/api'; - -import { WithRouterProps } from 'react-router'; +import { useSearchParams } from 'react-router-dom'; import { GridPanel } from '../../../public/QueryModel/GridPanel'; @@ -74,8 +73,7 @@ const AuditQueriesListingPageBody: FC = memo(pro value === SOURCE_AUDIT_QUERY.value || value === DATACLASS_DATA_UPDATE_AUDIT_QUERY.value; let auditEventType = isQueryDataUpdate ? QUERY_UPDATE_AUDIT_QUERY.value : value; const isAssayEvent = value === ASSAY_AUDIT_QUERY.value; - if (isAssayEvent) - auditEventType = EXPERIMENT_AUDIT_EVENT; + if (isAssayEvent) auditEventType = EXPERIMENT_AUDIT_EVENT; const detail_ = await getAuditDetail(selectedRowId, auditEventType); setDetail(detail_.merge({ rowId: selectedRowId }) as AuditDetailsModel); setError(undefined); @@ -130,8 +128,9 @@ const AuditQueriesListingPageBody: FC = memo(pro const AuditQueriesListingBodyWithModels = withQueryModels(AuditQueriesListingPageBody); -export const AuditQueriesListingPage: FC = memo(({ location, router }) => { - const locationEventType = location.query?.eventType; +export const AuditQueriesListingPage: FC = memo(() => { + const [searchParams, setSearchParams] = useSearchParams(); + const locationEventType = searchParams.get('eventType'); const [eventType, setEventType] = useState(() => locationEventType ?? SAMPLE_TIMELINE_AUDIT_QUERY.value); const { moduleContext, project, user } = useServerContext(); const auditQueries = useMemo(() => getAuditQueries(moduleContext), [moduleContext]); @@ -168,22 +167,26 @@ export const AuditQueriesListingPage: FC = memo(({ location, ro const onChange = useCallback( (_, eventType_) => { if (eventType_ === eventType) return; - const query = Object.keys(location.query).reduce((query_, key) => { - // remove query parameters from next model event type - if (!key.startsWith('query.')) { - if (key === AUDIT_EVENT_TYPE_PARAM) { - query_[key] = eventType_; - } else { - query_[key] = location.query[key]; - } - } - - return query_; - }, {}); - router.replace({ ...location, query }); + setSearchParams( + params => { + const paramsObj = Object.fromEntries(params.entries()); + return Object.keys(paramsObj).reduce((result, key) => { + if (!key.startsWith('query.')) { + if (key === AUDIT_EVENT_TYPE_PARAM) { + result[key] = eventType_; + } else { + result[key] = paramsObj[key]; + } + } + + return result; + }, {}); + }, + { replace: true } + ); setEventType(eventType_); }, - [eventType, location, router] + [eventType, setSearchParams] ); const title = 'Audit Logs'; From cc2394ca98d5210c5c3bda493ed8d0fff3e3a2de Mon Sep 17 00:00:00 2001 From: Alan Vezina Date: Wed, 15 Nov 2023 16:04:43 -0600 Subject: [PATCH 013/116] UsersGridPanel: Use RR hooks --- .../administration/UserManagement.tsx | 10 +++---- .../components/user/UsersGridPanel.tsx | 26 ++++++++++++------- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/packages/components/src/internal/components/administration/UserManagement.tsx b/packages/components/src/internal/components/administration/UserManagement.tsx index bf0b9152fd..da33a8865b 100644 --- a/packages/components/src/internal/components/administration/UserManagement.tsx +++ b/packages/components/src/internal/components/administration/UserManagement.tsx @@ -6,7 +6,6 @@ import React, { FC, PureComponent, ReactNode } from 'react'; import { List } from 'immutable'; import { MenuItem } from 'react-bootstrap'; import { PermissionRoles, Project, Utils } from '@labkey/api'; -import { WithRouterProps } from 'react-router'; import { User } from '../base/models/User'; import { Container } from '../base/models/Container'; @@ -95,7 +94,7 @@ interface OwnProps { } // exported for jest testing -export type UserManagementProps = OwnProps & InjectedPermissionsPage & NotificationsContextProps & WithRouterProps; +export type UserManagementProps = OwnProps & InjectedPermissionsPage & NotificationsContextProps; interface State { policy: SecurityPolicy; @@ -279,8 +278,7 @@ export class UserManagement extends PureComponent { }; render(): ReactNode { - const { allowResetPassword, container, extraRoles, location, project, user, rolesByUniqueName, router } = - this.props; + const { allowResetPassword, container, extraRoles, project, user, rolesByUniqueName } = this.props; const { policy, userLimitSettings } = this.state; // issue 39501: only allow permissions changes to be made if policy is stored in this container (i.e. not inherited) @@ -305,15 +303,13 @@ export class UserManagement extends PureComponent { allowResetPassword={allowResetPassword} showDetailsPanel={user.hasManageUsersPermission()} userLimitSettings={userLimitSettings} - router={router} - location={location} /> ); } } -type ImplProps = InjectedPermissionsPage & NotificationsContextProps & WithRouterProps; +type ImplProps = InjectedPermissionsPage & NotificationsContextProps; export const UserManagementPageImpl: FC = props => { const { api } = useAppContext(); diff --git a/packages/components/src/internal/components/user/UsersGridPanel.tsx b/packages/components/src/internal/components/user/UsersGridPanel.tsx index 2f65320c35..664308403c 100644 --- a/packages/components/src/internal/components/user/UsersGridPanel.tsx +++ b/packages/components/src/internal/components/user/UsersGridPanel.tsx @@ -2,16 +2,16 @@ * Copyright (c) 2019 LabKey Corporation. All rights reserved. No portion of this work may be reproduced in * any form or by any electronic or mechanical means without written permission from LabKey Corporation. */ -import React, { PureComponent, ReactNode } from 'react'; +import React, { FC, memo, PureComponent, ReactNode } from 'react'; import { List, Map } from 'immutable'; import { Col, MenuItem, Row } from 'react-bootstrap'; import { Filter } from '@labkey/api'; -import { InjectedRouter } from 'react-router'; +import { SetURLSearchParams, useSearchParams } from 'react-router-dom'; import { getSelected } from '../../actions'; import { QueryModel } from '../../../public/QueryModel/QueryModel'; -import { removeParameters, Location } from '../../util/URL'; +import { removeParameters } from '../../util/URL'; import { UserLimitSettings } from '../permissions/actions'; @@ -48,7 +48,6 @@ const OMITTED_COLUMNS = [ interface OwnProps { // option to disable the reset password UI pieces for this component allowResetPassword?: boolean; - location: Location; // optional array of role options, objects with id and label values (i.e. [{id: "org.labkey.api.security.roles.ReaderRole", label: "Reader (default)"}]) // note that the createNewUser action will not use this value but it will be passed back to the onCreateComplete newUserRoleOptions?: any[]; @@ -56,7 +55,9 @@ interface OwnProps { onUsersStateChangeComplete: (response: any) => any; policy: SecurityPolicy; rolesByUniqueName?: Map; - router: InjectedRouter; + // searchParams/setSearchParams can be removed as props if we convert to an FC and use the useSearchParams hook + searchParams: URLSearchParams; + setSearchParams: SetURLSearchParams; showDetailsPanel?: boolean; user: User; userLimitSettings?: UserLimitSettings; @@ -85,7 +86,7 @@ export class UsersGridPanelImpl extends PureComponent { this.state = { // location is really only undefined when running in jest tests because the react-router context isn't // properly setup. - usersView: this.getUsersView(props.location?.query.usersView), + usersView: this.getUsersView(this.props.searchParams.get('usersView')), showDialog: undefined, selectedUserId: undefined, }; @@ -105,11 +106,11 @@ export class UsersGridPanelImpl extends PureComponent { this.reloadUsersModel(); } - const curUsersView = this.props.location?.query.usersView; + const curUsersView = this.props.searchParams.get('usersView'); if (curUsersView !== undefined) { this.setState({ usersView: this.getUsersView(curUsersView) }); - removeParameters(this.props.router, this.props.location, 'usersView'); + removeParameters(this.props.setSearchParams, 'usersView'); } } @@ -370,4 +371,11 @@ export class UsersGridPanelImpl extends PureComponent { } } -export const UsersGridPanel = withQueryModels(UsersGridPanelImpl); +const UsersGridPanelWithModels = withQueryModels(UsersGridPanelImpl); + +type PanelProps = Omit; + +export const UsersGridPanel: FC = memo(props => { + const [searchParams, setSearchParams] = useSearchParams(); + return ; +}); From d4f08ed227bec986161610c74755ed79d560328c Mon Sep 17 00:00:00 2001 From: Alan Vezina Date: Wed, 15 Nov 2023 16:09:38 -0600 Subject: [PATCH 014/116] URL.ts: Update util methods to use setURLSearchParams instead of router and location --- packages/components/src/internal/util/URL.ts | 100 ++++++++++--------- 1 file changed, 52 insertions(+), 48 deletions(-) diff --git a/packages/components/src/internal/util/URL.ts b/packages/components/src/internal/util/URL.ts index ef63628a3b..7be3dc264c 100644 --- a/packages/components/src/internal/util/URL.ts +++ b/packages/components/src/internal/util/URL.ts @@ -13,22 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Location } from 'history'; import { SetURLSearchParams } from 'react-router-dom'; -import { DeprecatedRouter, QueryParams } from '../routerTypes'; +import { QueryParams } from '../routerTypes'; -/** - * Takes a search string (typically from Location) and converts it to a QueryParams object. If the same key is used - * multiple times in a search string (e.g. ?foo=bar&foo=baz) it will return an array of values for that key, if the key - * only appears once it will return a string for that key. If you know the key you are looking for will not be an array - * use: const myValue = getQueryParams(search).myValue as string; - * @param search - */ -export function getQueryParams(search: string): QueryParams { - if (!search) return {}; - const paramsArray = new URLSearchParams(search).entries(); - return [...paramsArray].reduce((result, tuple) => { +function getQueryParamsFromSearchParams(searchParams: URLSearchParams): QueryParams { + return [...searchParams.entries()].reduce((result, tuple) => { const [key, value] = tuple; if (result.hasOwnProperty(key)) { if (Array.isArray(result[key])) { @@ -43,56 +33,70 @@ export function getQueryParams(search: string): QueryParams { }, {}); } -// TODO: convert this to use SetURLSearchParams, which negates the need for router and location as it is a React style -// useState setter that lets us pass exactly what we want, or a function that accesses the current params while setting -function setParameters(router: DeprecatedRouter, location: Location, params: QueryParams, asReplace = false): void { - const query = getQueryParams(location.search); +/** + * Takes a search string (typically from Location) or URLSearchParams (typically from useSearchParams hook) and converts + * it to a QueryParams object. If the same key is used multiple times in a search string (e.g. ?foo=bar&foo=baz) it will + * return an array of values for that key, if the key only appears once it will return a string for that kΩey. If you + * know the key you are looking for will not be an array use: const myValue = getQueryParams(search).myValue as string; + * @param search + */ +export function getQueryParams(search: string | URLSearchParams): QueryParams { + if (!search) return {}; - Object.keys(params).forEach(key => { - const value = params[key]; + if (search instanceof URLSearchParams) { + return getQueryParamsFromSearchParams(search); + } - if (value === undefined) { - delete query[key]; - } else { - query[key] = value; - } - }); + return getQueryParamsFromSearchParams(new URLSearchParams(search)); +} - if (asReplace) { - router.replace({ ...location, query }); - } else { - router.push({ ...location, query }); - } +function setParameters(setParams: SetURLSearchParams, params: QueryParams, asReplace = false): void { + const options = asReplace ? { replace: true } : undefined; + + setParams(current => { + const query = getQueryParams(current); + Object.keys(params).forEach(key => { + const value = params[key]; + + if (value === undefined) { + delete query[key]; + } else { + query[key] = value; + } + }); + + return query; + }, options); } -export function removeParameters(router: DeprecatedRouter, location: Location, ...params: string[]): void { +export function removeParameters(setParams: SetURLSearchParams, ...params: string[]): void { if (!params) return; const paramsObj = params.reduce((result, param) => { result[param] = undefined; return result; }, {}); - setParameters(router, location, paramsObj, true); + setParameters(setParams, paramsObj, true); } -export function replaceParameters(router: DeprecatedRouter, location: Location, params: QueryParams): void { - setParameters(router, location, params, true); +export function replaceParameters(setParams: SetURLSearchParams, params: QueryParams): void { + setParameters(setParams, params, true); } -export function pushParameters(router: DeprecatedRouter, location: Location, params: QueryParams): void { - setParameters(router, location, params); +export function pushParameters(setParams: SetURLSearchParams, params: QueryParams): void { + setParameters(setParams, params); } -export function resetParameters(router: DeprecatedRouter, location: Location, except: string[] = []): void { - const currentParams = getQueryParams(location.search); - const updatedParams = Object.keys(currentParams).reduce((result, key: string) => { - if (except.indexOf(key) > -1) { - result[key] = currentParams[key]; - } else { - result[key] = undefined; - } - - return result; - }, {}); +export function resetParameters(setParams: SetURLSearchParams, except: string[] = []): void { + setParams(current => { + const currentParams = getQueryParams(current); + return Object.keys(currentParams).reduce((result, key: string) => { + if (except.indexOf(key) > -1) { + result[key] = currentParams[key]; + } else { + result[key] = undefined; + } - setParameters(router, location, updatedParams); + return result; + }, {}); + }); } From a595b21823586640f74a5a9cc9dd721bb0a05bd5 Mon Sep 17 00:00:00 2001 From: Alan Vezina Date: Wed, 15 Nov 2023 16:10:11 -0600 Subject: [PATCH 015/116] mockUtils: Update createMockRouterProps to make a DeprecatedRouter --- packages/components/src/internal/mockUtils.ts | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/packages/components/src/internal/mockUtils.ts b/packages/components/src/internal/mockUtils.ts index 6d67b1dab2..881a5eb284 100644 --- a/packages/components/src/internal/mockUtils.ts +++ b/packages/components/src/internal/mockUtils.ts @@ -1,5 +1,5 @@ -import { InjectedRouter, WithRouterProps } from 'react-router'; - +import { DeprecatedRouter } from './routerTypes'; +import { DeprecatedWithRouterProps } from './withRouterDeprecated'; import { InjectedRouteLeaveProps } from './util/RouteLeave'; /** @@ -27,15 +27,11 @@ export const createMockWithRouteLeave = ( */ export const createMockWithRouterProps = ( mockFn = (): any => () => {}, - routerOverrides: Partial = {} -): WithRouterProps => { - const defaultRouter: InjectedRouter = { - createHref: mockFn(), - createPath: mockFn(), - go: mockFn(), + routerOverrides: Partial = {} +): DeprecatedWithRouterProps => { + const defaultRouter: DeprecatedRouter = { goBack: mockFn(), goForward: mockFn(), - isActive: mockFn(), push: mockFn(), replace: mockFn(), setRouteLeaveHook: mockFn(), @@ -48,11 +44,9 @@ export const createMockWithRouterProps = ( query: {}, hash: '', state: undefined, - action: 'PUSH', key: '', }, params: {}, router: Object.assign(defaultRouter, routerOverrides), - routes: [], }; }; From d7300b2652680b69a7ff1b71f1f19509ee8077d9 Mon Sep 17 00:00:00 2001 From: Alan Vezina Date: Wed, 15 Nov 2023 16:12:00 -0600 Subject: [PATCH 016/116] CreateProjectPage: Use RR hooks --- .../components/project/CreateProjectPage.tsx | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/components/src/internal/components/project/CreateProjectPage.tsx b/packages/components/src/internal/components/project/CreateProjectPage.tsx index 5dd89a7113..1197ab0749 100644 --- a/packages/components/src/internal/components/project/CreateProjectPage.tsx +++ b/packages/components/src/internal/components/project/CreateProjectPage.tsx @@ -1,6 +1,6 @@ import React, { FC, memo, useCallback, useState } from 'react'; -import { WithRouterProps } from 'react-router'; import { ActionURL } from '@labkey/api'; +import { useNavigate, useSearchParams } from 'react-router-dom'; import { FormButtons } from '../../FormButtons'; @@ -129,18 +129,21 @@ export const CreateProjectContainer: FC = memo(prop ); }); -export const CreateProjectPage: FC = memo(({ router }) => { +export const CreateProjectPage = memo(() => { + const [_, setParams] = useSearchParams(); + const navigate = useNavigate(); const { api } = useAppContext(); const { createNotification } = useNotificationsContext(); const { moduleContext, user } = useServerContext(); const { reload } = useFolderMenuContext(); const dispatch = useServerContextDispatch(); const hasProjects = hasProductProjects(moduleContext); + const onCancel = useCallback(() => navigate(-1), [navigate]); const onCreated = useCallback( (project: Container) => { // Reroute user back to projects listing page - router.replace(AppURL.create('admin', 'projects').addParam('created', project.name).toString()); + setParams({ created: project.name }, { replace: true }); const appProps = getCurrentAppProperties(); if (!appProps?.controllerName) return; @@ -168,13 +171,13 @@ export const CreateProjectPage: FC = memo(({ router }) => { // Reload the folder menu to ensure the new project appears in the navigation for this session reload(); }, - [createNotification, dispatch, hasProjects, moduleContext, reload, router] + [createNotification, dispatch, hasProjects, moduleContext, reload, setParams] ); return ( - + ); }); From 307c74457c9b20e91e3702698045fd2884c1d628 Mon Sep 17 00:00:00 2001 From: Alan Vezina Date: Wed, 15 Nov 2023 16:12:22 -0600 Subject: [PATCH 017/116] ProjectManagementPage: use RR hooks --- .../project/ProjectManagementPage.tsx | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/packages/components/src/internal/components/project/ProjectManagementPage.tsx b/packages/components/src/internal/components/project/ProjectManagementPage.tsx index bf181b7a3a..71167d8ee9 100644 --- a/packages/components/src/internal/components/project/ProjectManagementPage.tsx +++ b/packages/components/src/internal/components/project/ProjectManagementPage.tsx @@ -1,9 +1,7 @@ import React, { FC, memo, useCallback, useEffect, useMemo, useState } from 'react'; import { Button } from 'react-bootstrap'; - -import { Link, WithRouterProps } from 'react-router'; - import { Security } from '@labkey/api'; +import { useSearchParams } from 'react-router-dom'; import { useServerContext } from '../base/ServerContext'; import { AppURL, createProductUrl } from '../../url/AppURL'; @@ -25,8 +23,9 @@ import { ProjectSettings } from './ProjectSettings'; import { ProjectListing } from './ProjectListing'; -export const ProjectManagementPage: FC = memo(({ location, router, routes }) => { - const [getIsDirty, setIsDirty] = useRouteLeave(router, routes); +export const ProjectManagementPage: FC = memo(() => { + const [searchParams, setSearchParams] = useSearchParams(); + const [getIsDirty, setIsDirty] = useRouteLeave(); const [successMsg, setSuccessMsg] = useState(); const { user, moduleContext, container } = useServerContext(); const { api } = useAppContext(); @@ -55,9 +54,9 @@ export const ProjectManagementPage: FC = memo(({ location, rout } let defaultContainer = container?.isFolder ? container : projects_?.[0]; - const createdProjectName = location.query.created; + const createdProjectName = searchParams.get('created'); if (createdProjectName) { - removeParameters(router, location, 'created'); + removeParameters(setSearchParams, 'created'); const createdProject = projects_.find(proj => proj.name === createdProjectName); if (createdProject) defaultContainer = createdProject; } @@ -72,10 +71,10 @@ export const ProjectManagementPage: FC = memo(({ location, rout }, [reloadCounter]); useEffect(() => { - const successMessage = location.query.successMsg; + const successMessage = searchParams.get('successMsg'); if (successMessage) { setSuccessMsg(`${successMessage} successfully deleted.`); - removeParameters(router, location, 'successMsg'); + removeParameters(setSearchParams, 'successMsg'); } }, []); @@ -147,7 +146,7 @@ export const ProjectManagementPage: FC = memo(({ location, rout {loaded && !error && projects?.length === 0 && ( No projects have been created. Click{' '} - here to get started. + here to get started. )} {projects?.length > 0 && ( From ce933b18a0fc059975c37e7722e8f44d06aca12c Mon Sep 17 00:00:00 2001 From: Alan Vezina Date: Wed, 15 Nov 2023 16:13:01 -0600 Subject: [PATCH 018/116] PermissionAssignments: Use RR hooks --- .../PermissionManagementPage.tsx | 10 +++---- .../permissions/PermissionAssignments.tsx | 26 +++++++++---------- 2 files changed, 17 insertions(+), 19 deletions(-) diff --git a/packages/components/src/internal/components/administration/PermissionManagementPage.tsx b/packages/components/src/internal/components/administration/PermissionManagementPage.tsx index 6c245aef08..977463edf7 100644 --- a/packages/components/src/internal/components/administration/PermissionManagementPage.tsx +++ b/packages/components/src/internal/components/administration/PermissionManagementPage.tsx @@ -5,8 +5,6 @@ import React, { FC, memo, useCallback, useMemo, useState } from 'react'; import { Map } from 'immutable'; -import { withRouter, WithRouterProps } from 'react-router'; - import { useServerContext } from '../base/ServerContext'; import { useNotificationsContext } from '../notifications/NotificationsContext'; @@ -26,12 +24,12 @@ import { showPremiumFeatures } from './utils'; import { getUpdatedPolicyRoles } from './actions'; // exported for testing -export type Props = InjectedPermissionsPage & WithRouterProps; +export type Props = InjectedPermissionsPage; // exported for testing export const PermissionManagementPageImpl: FC = memo(props => { - const { roles, router, routes } = props; - const [getIsDirty, setIsDirty] = useRouteLeave(router, routes); + const { roles } = props; + const [getIsDirty, setIsDirty] = useRouteLeave(); const [policyLastModified, setPolicyLastModified] = useState(undefined); const [hidePageDescription, setHidePageDescription] = useState(false); const { dismissNotifications, createNotification } = useNotificationsContext(); @@ -97,4 +95,4 @@ export const PermissionManagementPageImpl: FC = memo(props => { ); }); -export const PermissionManagementPage = withRouter(withPermissionsPage(PermissionManagementPageImpl)); +export const PermissionManagementPage = withPermissionsPage(PermissionManagementPageImpl); diff --git a/packages/components/src/internal/components/permissions/PermissionAssignments.tsx b/packages/components/src/internal/components/permissions/PermissionAssignments.tsx index de33234459..fd23028599 100644 --- a/packages/components/src/internal/components/permissions/PermissionAssignments.tsx +++ b/packages/components/src/internal/components/permissions/PermissionAssignments.tsx @@ -4,10 +4,12 @@ */ import React, { FC, memo, useCallback, useEffect, useMemo, useState } from 'react'; import { List } from 'immutable'; -import { WithRouterProps } from 'react-router'; import { Security } from '@labkey/api'; +import { useLocation, useNavigate } from 'react-router-dom'; import { FormButtons } from '../../FormButtons'; +import { InjectedRouteLeaveProps } from '../../util/RouteLeave'; +import { getQueryParams } from '../../util/URL'; import { UserDetailsPanel } from '../user/UserDetailsPanel'; import { @@ -23,7 +25,6 @@ import { useServerContext } from '../base/ServerContext'; import { AppContext, useAppContext } from '../../AppContext'; import { resolveErrorMessage } from '../../util/messaging'; -import { InjectedRouteLeaveProps } from '../../util/RouteLeave'; import { Alert } from '../base/Alert'; @@ -49,7 +50,7 @@ import { GroupDetailsPanel } from './GroupDetailsPanel'; import { InjectedPermissionsPage } from './withPermissionsPage'; // exported for testing -export interface PermissionAssignmentsProps extends InjectedPermissionsPage, InjectedRouteLeaveProps, WithRouterProps { +export interface PermissionAssignmentsProps extends InjectedPermissionsPage, InjectedRouteLeaveProps { onSuccess: () => void; /** Subset list of role uniqueNames to show in this component usage */ rolesToShow?: List; @@ -66,7 +67,6 @@ export const PermissionAssignments: FC = memo(props const { getIsDirty, inactiveUsersById, - location, onSuccess, principals, principalsById, @@ -74,7 +74,6 @@ export const PermissionAssignments: FC = memo(props rolesByUniqueName, rolesToShow, setIsDirty, - router, rootRolesToShow, setLastModified, setProjectCount, @@ -103,7 +102,8 @@ export const PermissionAssignments: FC = memo(props const homeFolderPath = isAppHome ? container.path : container.parentPath; const selectedPrincipal = principalsById?.get(selectedUserId); - const initExpandedRole = location.query.expand; + const location = useLocation(); + const initExpandedRole = getQueryParams(location.search).expand as string; const projectUser = useContainerUser(getProjectPath(container?.path)); const loadGroupMembership = useCallback(async () => { @@ -257,10 +257,12 @@ export const PermissionAssignments: FC = memo(props loadGroupMembership(); }, [onSuccess, loadGroupMembership]); + const navigate = useNavigate(); + const onCancel = useCallback(() => { setIsDirty(false); - router.goBack(); - }, [router, setIsDirty]); + navigate(-1); + }, [navigate, setIsDirty]); const onSaveSuccess = useCallback(async () => { await loadPolicy(); @@ -540,11 +542,9 @@ export const PermissionAssignments: FC = memo(props - {router && ( - - )} + - )} + + + + View Audit History + + +); + export const ProjectManagementPage: FC = memo(() => { useAdministrationSubNav(); const [searchParams, setSearchParams] = useSearchParams(); @@ -80,24 +95,6 @@ export const ProjectManagementPage: FC = memo(() => { } }, []); - const renderButtons = useMemo( - () => () => ( - <> - - - View Audit History - - - ), - [] - ); - const onError = useCallback((e: string) => { setError(e); }, []); From 5bb3517042dc60efc8013ed6f811255a6e36d3fd Mon Sep 17 00:00:00 2001 From: Alan Vezina Date: Wed, 6 Dec 2023 11:19:12 -0600 Subject: [PATCH 080/116] Bump react-router-dom, use useRef in useRouteLeave again (yay) --- packages/components/package-lock.json | 50 +++++++++---------- packages/components/package.json | 2 +- .../src/internal/util/RouteLeave.tsx | 42 ++++++---------- 3 files changed, 40 insertions(+), 54 deletions(-) diff --git a/packages/components/package-lock.json b/packages/components/package-lock.json index a86360119f..cb3b2398a0 100644 --- a/packages/components/package-lock.json +++ b/packages/components/package-lock.json @@ -30,7 +30,7 @@ "react-color": "~2.19.3", "react-datepicker": "~4.17.0", "react-dom": "~16.14.0", - "react-router-dom": "~6.18.0", + "react-router-dom": "~6.20.1", "react-select": "~5.7.0", "react-treebeard": "~3.2.4", "vis-network": "~6.5.2" @@ -3489,9 +3489,9 @@ } }, "node_modules/@remix-run/router": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.11.0.tgz", - "integrity": "sha512-BHdhcWgeiudl91HvVa2wxqZjSHbheSgIiDvxrF1VjFzBzpTtuDPkOdOi3Iqvc08kXtFkLjhbS+ML9aM8mJS+wQ==", + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.13.1.tgz", + "integrity": "sha512-so+DHzZKsoOcoXrILB4rqDkMDy7NLMErRdOxvzvOKb507YINKUP4Di+shbTZDhSE/pBZ+vr7XGIpcOO0VLSA+Q==", "engines": { "node": ">=14.0.0" } @@ -14523,11 +14523,11 @@ } }, "node_modules/react-router": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.18.0.tgz", - "integrity": "sha512-vk2y7Dsy8wI02eRRaRmOs9g2o+aE72YCx5q9VasT1N9v+lrdB79tIqrjMfByHiY5+6aYkH2rUa5X839nwWGPDg==", + "version": "6.20.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.20.1.tgz", + "integrity": "sha512-ccvLrB4QeT5DlaxSFFYi/KR8UMQ4fcD8zBcR71Zp1kaYTC5oJKYAp1cbavzGrogwxca+ubjkd7XjFZKBW8CxPA==", "dependencies": { - "@remix-run/router": "1.11.0" + "@remix-run/router": "1.13.1" }, "engines": { "node": ">=14.0.0" @@ -14537,12 +14537,12 @@ } }, "node_modules/react-router-dom": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.18.0.tgz", - "integrity": "sha512-Ubrue4+Ercc/BoDkFQfc6og5zRQ4A8YxSO3Knsne+eRbZ+IepAsK249XBH/XaFuOYOYr3L3r13CXTLvYt5JDjw==", + "version": "6.20.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.20.1.tgz", + "integrity": "sha512-npzfPWcxfQN35psS7rJgi/EW0Gx6EsNjfdJSAk73U/HqMEJZ2k/8puxfwHFgDQhBGmS3+sjnGbMdMSV45axPQw==", "dependencies": { - "@remix-run/router": "1.11.0", - "react-router": "6.18.0" + "@remix-run/router": "1.13.1", + "react-router": "6.20.1" }, "engines": { "node": ">=14.0.0" @@ -20296,9 +20296,9 @@ "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==" }, "@remix-run/router": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.11.0.tgz", - "integrity": "sha512-BHdhcWgeiudl91HvVa2wxqZjSHbheSgIiDvxrF1VjFzBzpTtuDPkOdOi3Iqvc08kXtFkLjhbS+ML9aM8mJS+wQ==" + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.13.1.tgz", + "integrity": "sha512-so+DHzZKsoOcoXrILB4rqDkMDy7NLMErRdOxvzvOKb507YINKUP4Di+shbTZDhSE/pBZ+vr7XGIpcOO0VLSA+Q==" }, "@sinclair/typebox": { "version": "0.27.8", @@ -28513,20 +28513,20 @@ } }, "react-router": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.18.0.tgz", - "integrity": "sha512-vk2y7Dsy8wI02eRRaRmOs9g2o+aE72YCx5q9VasT1N9v+lrdB79tIqrjMfByHiY5+6aYkH2rUa5X839nwWGPDg==", + "version": "6.20.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.20.1.tgz", + "integrity": "sha512-ccvLrB4QeT5DlaxSFFYi/KR8UMQ4fcD8zBcR71Zp1kaYTC5oJKYAp1cbavzGrogwxca+ubjkd7XjFZKBW8CxPA==", "requires": { - "@remix-run/router": "1.11.0" + "@remix-run/router": "1.13.1" } }, "react-router-dom": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.18.0.tgz", - "integrity": "sha512-Ubrue4+Ercc/BoDkFQfc6og5zRQ4A8YxSO3Knsne+eRbZ+IepAsK249XBH/XaFuOYOYr3L3r13CXTLvYt5JDjw==", + "version": "6.20.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.20.1.tgz", + "integrity": "sha512-npzfPWcxfQN35psS7rJgi/EW0Gx6EsNjfdJSAk73U/HqMEJZ2k/8puxfwHFgDQhBGmS3+sjnGbMdMSV45axPQw==", "requires": { - "@remix-run/router": "1.11.0", - "react-router": "6.18.0" + "@remix-run/router": "1.13.1", + "react-router": "6.20.1" } }, "react-select": { diff --git a/packages/components/package.json b/packages/components/package.json index 050abc5f15..ad8891ae02 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -71,7 +71,7 @@ "react-color": "~2.19.3", "react-datepicker": "~4.17.0", "react-dom": "~16.14.0", - "react-router-dom": "~6.18.0", + "react-router-dom": "~6.20.1", "react-select": "~5.7.0", "react-treebeard": "~3.2.4", "vis-network": "~6.5.2" diff --git a/packages/components/src/internal/util/RouteLeave.tsx b/packages/components/src/internal/util/RouteLeave.tsx index 94415007bd..6519e17d94 100644 --- a/packages/components/src/internal/util/RouteLeave.tsx +++ b/packages/components/src/internal/util/RouteLeave.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; import { unstable_usePrompt as usePrompt } from 'react-router-dom'; export const CONFIRM_MESSAGE = 'You have unsaved changes that will be lost. Are you sure you want to continue?'; @@ -20,47 +20,33 @@ type GetSetIsDirty = [() => boolean, (dirty: boolean) => void]; * The useRouteLeave hook is useful if you want to display a confirmation dialog when the user tries to navigate away * from a "dirty" form or page. This hook ties into both the React Router RouteLeave event, and the browser beforeunload * event. This allows us to prevent navigation via back button, link clicking, or browser window/tab closing. - * - * NOTE: due to how this implemented you cannot call setIsDirty(true) and then immediately invoke navigate. Instead, you - * need to wait for the new dirty state to get persisted. The easiest way to do this is to have a local variable called - * successURL (or similar) that you set when you want to navigate, call setIsDirty and setSuccessURL and then navigate - * within a useEffect call that checks if successURL is undefined. It will look something like this: - * - * const navigate = useNavigate(); - * const [getIsDirty, setisDirty] = useRouteLeave; - * const [successURL, setSuccessURL] = useState(undefined); - * - * const mySuccessHandler = useCallback(() => { - * setIsDirty(false); - * setSuccessURL(AppURL.create('some', 'route', 'parts)); - * }); - * - * useEffect(() => { - * if (successURL !== undefined) navigate(successURL); - * }, [navigate, successURL]); - * * @param confirmMessage: The confirm message you want to display to the user, this message is only displayed when * navigating away from the page, not when closing the tab or browser window. Browsers do not let you customize the * message displayed when the browser/tab is closed. */ export const useRouteLeave = (confirmMessage = CONFIRM_MESSAGE): GetSetIsDirty => { - const [isDirty, setIsDirty] = useState(false); - - // TODO: getIsDirty is an artifact of the react-router 3 version of this component. We should update this hook to - // return isDirty directly. Putting this off for now to limit the scope of the changes during the RR upgrade. - const getIsDirty = useCallback((): boolean => isDirty, [isDirty]); + const isDirty = useRef(false); + const setIsDirty = useCallback( + (dirty: boolean) => { + isDirty.current = dirty; + }, + [isDirty] + ); + const getIsDirty = useCallback((): boolean => { + return isDirty.current; + }, []); // usePrompt prevents users from going to URLs within our App - usePrompt({ when: isDirty, message: confirmMessage }); + usePrompt({ when: getIsDirty, message: confirmMessage }); // BeforeUnload is needed so we can prevent the user from going to URLs outside our App (e.g. to FM or LKS) const beforeUnload = useCallback( event => { - if (isDirty === true) { + if (getIsDirty() === true) { event.returnValue = true; } }, - [isDirty] + [getIsDirty] ); useEffect(() => { From 0ed398154f546d8ae7a2a557400586a45ca6196a Mon Sep 17 00:00:00 2001 From: Alan Vezina Date: Wed, 6 Dec 2023 14:12:33 -0600 Subject: [PATCH 081/116] SubNav: Fix issues with noun, remove unused attributes from ITab --- .../src/internal/components/navigation/NavItem.tsx | 13 ++++++------- .../src/internal/components/navigation/SubNav.tsx | 6 +++--- .../src/internal/components/navigation/types.ts | 4 ---- 3 files changed, 9 insertions(+), 14 deletions(-) diff --git a/packages/components/src/internal/components/navigation/NavItem.tsx b/packages/components/src/internal/components/navigation/NavItem.tsx index acc5552a14..232c26142b 100644 --- a/packages/components/src/internal/components/navigation/NavItem.tsx +++ b/packages/components/src/internal/components/navigation/NavItem.tsx @@ -20,11 +20,10 @@ import { AppURL } from '../../url/AppURL'; interface NavItemProps { onActive?: (activeEl: HTMLElement) => void; - onClick?: () => void; to?: string | AppURL; } -export const NavItem: FC = memo(({ children, onActive, to, onClick }) => { +export const NavItem: FC = memo(({ children, onActive, to }) => { const location = useLocation(); const href = to instanceof AppURL ? to.toString() : to; const itemRef = useRef(); @@ -49,25 +48,25 @@ export const NavItem: FC = memo(({ children, onActive, to, onClick return (
  • - + {children}
  • ); }); -export const ParentNavItem: FC = memo(({ children, to, onClick }) => { - const href = to instanceof AppURL ? to.toHref() : to; +export const ParentNavItem: FC = memo(({ children, to }) => { + const href = to instanceof AppURL ? to.toString() : to; return ( diff --git a/packages/components/src/internal/components/navigation/SubNav.tsx b/packages/components/src/internal/components/navigation/SubNav.tsx index 5962fb556b..bd067be1e6 100644 --- a/packages/components/src/internal/components/navigation/SubNav.tsx +++ b/packages/components/src/internal/components/navigation/SubNav.tsx @@ -91,7 +91,7 @@ const SubNavImpl: FC = ({ noun, tabs }) => {