diff --git a/package.json b/package.json index cc5b116a5..2237dcd3c 100644 --- a/package.json +++ b/package.json @@ -79,8 +79,8 @@ "react-intl": "6.4.4", "react-motion": "^0.5.2", "react-redux": "^8.1.2", - "react-router": "^5.3.4", - "react-router-dom": "^5.3.4", + "react-router": "^6.14.2", + "react-router-dom": "^6.14.2", "react-syntax-highlighter": "^15.5.0", "react-toggle": "4.1.3", "redux": "^4.2.1", diff --git a/src/components/Assignments/Assignment/AssignmentsTable/AssignmentsTable.js b/src/components/Assignments/Assignment/AssignmentsTable/AssignmentsTable.js index f87a9cd92..90bdd5553 100644 --- a/src/components/Assignments/Assignment/AssignmentsTable/AssignmentsTable.js +++ b/src/components/Assignments/Assignment/AssignmentsTable/AssignmentsTable.js @@ -3,7 +3,6 @@ import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import { Table, Modal } from 'react-bootstrap'; import { FormattedMessage, injectIntl } from 'react-intl'; -import { withRouter } from 'react-router'; import { defaultMemoize } from 'reselect'; import moment from 'moment'; @@ -18,6 +17,8 @@ import { UserUIDataContext } from '../../../../helpers/contexts'; import { EMPTY_LIST, EMPTY_OBJ, EMPTY_ARRAY } from '../../../../helpers/common'; import { prepareInitialValues, transformSubmittedData } from '../../../forms/EditAssignmentForm'; +import withRouter, { withRouterProps } from '../../../../helpers/withRouter'; + const fetchAssignmentStatus = (statuses, assignmentId) => { const assignStatus = statuses && Array.isArray(statuses) ? statuses.find(assignStatus => assignStatus.id === assignmentId) : null; @@ -182,7 +183,7 @@ class AssignmentsTable extends Component { editAssignment = null, deleteAssignment = null, intl: { locale }, - history: { push }, + navigate, } = this.props; const someAssignmentsAreLoading = assignments.some(isLoading); const assignmentsPreprocessedAll = assignments @@ -297,7 +298,7 @@ class AssignmentsTable extends Component { discussionOpen={() => this.openDialog(assignment)} setSelected={multiActions ? this.selectAssignmentClickHandler(assignmentsPreprocessedAll) : null} selected={Boolean(this.state.selectedAssignments[assignment.id])} - doubleClickPush={openOnDoubleclick ? push : null} + doubleClickPush={openOnDoubleclick ? navigate : null} /> ))} @@ -479,9 +480,7 @@ AssignmentsTable.propTypes = { editAssignment: PropTypes.func, deleteAssignment: PropTypes.func, intl: PropTypes.object.isRequired, - history: PropTypes.shape({ - push: PropTypes.func.isRequired, - }), + navigate: withRouterProps.navigate, }; export default withRouter(injectIntl(AssignmentsTable)); diff --git a/src/components/Assignments/ShadowAssignment/ShadowAssignmentsTable/ShadowAssignmentsTable.js b/src/components/Assignments/ShadowAssignment/ShadowAssignmentsTable/ShadowAssignmentsTable.js index f6e974091..0c7f62310 100644 --- a/src/components/Assignments/ShadowAssignment/ShadowAssignmentsTable/ShadowAssignmentsTable.js +++ b/src/components/Assignments/ShadowAssignment/ShadowAssignmentsTable/ShadowAssignmentsTable.js @@ -2,7 +2,6 @@ import React from 'react'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import { Table } from 'react-bootstrap'; -import { withRouter } from 'react-router'; import { FormattedMessage, injectIntl } from 'react-intl'; import { isReady, isLoading, getJsData } from '../../../../redux/helpers/resourceManager'; @@ -11,6 +10,7 @@ import { compareShadowAssignments } from '../../../helpers/assignments'; import { LoadingIcon } from '../../../icons'; import { UserUIDataContext } from '../../../../helpers/contexts'; import { EMPTY_LIST, EMPTY_OBJ } from '../../../../helpers/common'; +import withRouter, { withRouterProps } from '../../../../helpers/withRouter'; const ShadowAssignmentsTable = ({ shadowAssignments = EMPTY_LIST, @@ -18,7 +18,7 @@ const ShadowAssignmentsTable = ({ stats = EMPTY_OBJ, isAdmin = false, intl: { locale }, - history: { push }, + navigate, }) => ( {({ openOnDoubleclick = false }) => ( @@ -93,7 +93,7 @@ const ShadowAssignmentsTable = ({ Object.keys(stats).length !== 0 ? stats.assignments.find(item => item.id === assignment.id) : null } isAdmin={isAdmin} - doubleClickPush={openOnDoubleclick ? push : null} + doubleClickPush={openOnDoubleclick ? navigate : null} /> ))} @@ -108,9 +108,7 @@ ShadowAssignmentsTable.propTypes = { stats: PropTypes.object, isAdmin: PropTypes.bool, intl: PropTypes.shape({ locale: PropTypes.string.isRequired }).isRequired, - history: PropTypes.shape({ - push: PropTypes.func.isRequired, - }), + navigate: withRouterProps.navigate, }; export default withRouter(injectIntl(ShadowAssignmentsTable)); diff --git a/src/components/Assignments/SolutionsTable/SolutionsTable.js b/src/components/Assignments/SolutionsTable/SolutionsTable.js index e5c308582..cd78890e6 100644 --- a/src/components/Assignments/SolutionsTable/SolutionsTable.js +++ b/src/components/Assignments/SolutionsTable/SolutionsTable.js @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import { FormattedMessage } from 'react-intl'; import { Table } from 'react-bootstrap'; -import { withRouter } from 'react-router'; +import { useNavigate } from 'react-router-dom'; import { defaultMemoize } from 'reselect'; import NoSolutionYetTableRow from './NoSolutionYetTableRow'; @@ -29,8 +29,8 @@ const SolutionsTable = ({ assignmentSolversLoading = false, showActionButtons = true, onSelect = null, - history: { push }, }) => { + const navigate = useNavigate(); const highlightsIndex = createHighlightsIndex(highlights); return ( @@ -141,7 +141,7 @@ const SolutionsTable = ({ highlighted={highlightsIndex.has(id)} showActionButtons={showActionButtons} onSelect={onSelect} - doubleclickAction={openOnDoubleclick ? push : null} + doubleclickAction={openOnDoubleclick ? navigate : null} {...data} /> ); @@ -166,9 +166,6 @@ SolutionsTable.propTypes = { assignmentSolversLoading: PropTypes.bool, showActionButtons: PropTypes.bool, onSelect: PropTypes.func, - history: PropTypes.shape({ - push: PropTypes.func.isRequired, - }), }; -export default withRouter(SolutionsTable); +export default SolutionsTable; diff --git a/src/components/Exercises/ExercisesList/ExercisesList.js b/src/components/Exercises/ExercisesList/ExercisesList.js index 742d36507..e60a41240 100644 --- a/src/components/Exercises/ExercisesList/ExercisesList.js +++ b/src/components/Exercises/ExercisesList/ExercisesList.js @@ -2,7 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Table } from 'react-bootstrap'; import { FormattedMessage } from 'react-intl'; -import { withRouter } from 'react-router'; +import { useNavigate } from 'react-router-dom'; import ExercisesListItem from '../ExercisesListItem'; import { LoadingIcon } from '../../icons'; @@ -15,46 +15,51 @@ const ExercisesList = ({ showAssignButton = false, assignExercise = null, reload, - history: { push }, -}) => ( - - {({ openOnDoubleclick = false }) => ( - - {Boolean(heading) && {heading}} - - {exercises.map((exercise, idx) => - exercise ? ( - - ) : ( - - +
- - +}) => { + const navigate = useNavigate(); + return ( + + {({ openOnDoubleclick = false }) => ( + + {Boolean(heading) && {heading}} + + {exercises.map((exercise, idx) => + exercise ? ( + + ) : ( + + + + ) + )} + + {exercises.length === 0 && ( + + - ) - )} - - {exercises.length === 0 && ( - - - - )} - -
+ + +
+
- -
- )} -
-); + )} +
+ )} +
+ ); +}; ExercisesList.propTypes = { heading: PropTypes.any, @@ -63,9 +68,6 @@ ExercisesList.propTypes = { showAssignButton: PropTypes.bool, assignExercise: PropTypes.func, reload: PropTypes.func, - history: PropTypes.shape({ - push: PropTypes.func.isRequired, - }), }; -export default withRouter(ExercisesList); +export default ExercisesList; diff --git a/src/components/Groups/ResultsTable/ResultsTable.js b/src/components/Groups/ResultsTable/ResultsTable.js index 2cb113db1..8ba76141f 100644 --- a/src/components/Groups/ResultsTable/ResultsTable.js +++ b/src/components/Groups/ResultsTable/ResultsTable.js @@ -143,9 +143,9 @@ class ResultsTable extends Component { showOnlyMe: false, }; - componentDidMount = () => { + componentDidMount() { this.setState({ showOnlyMe: storageGetItem(localStorageShowOnlyMeKey, false) }); - }; + } toggleShowOnlyMe = () => { const showOnlyMe = !this.state.showOnlyMe; diff --git a/src/components/Solutions/SolutionActions/SolutionActions.js b/src/components/Solutions/SolutionActions/SolutionActions.js index be13813b3..c059b5776 100644 --- a/src/components/Solutions/SolutionActions/SolutionActions.js +++ b/src/components/Solutions/SolutionActions/SolutionActions.js @@ -1,7 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { FormattedMessage } from 'react-intl'; -import { withRouter } from 'react-router'; +import { useLocation, useNavigate } from 'react-router-dom'; import withLinks from '../../../helpers/withLinks'; import { safeGet } from '../../../helpers/common'; @@ -112,10 +112,11 @@ const SolutionActions = ({ setReviewState = null, deleteReview = null, setPoints = null, - history: { push }, - location: { pathname }, links: { SOLUTION_SOURCE_CODES_URI_FACTORY }, }) => { + const { pathname } = useLocation(); + const navigate = useNavigate(); + const review = solution && solution.review; const assignmentId = solution && solution.assignmentId; const accepted = solution && solution.accepted; @@ -154,12 +155,12 @@ const SolutionActions = ({ open: openReview && (!review || !review.startedAt) && - (isOnReviewPage ? openReview : () => openReview().then(() => push(reviewPageUri))), + (isOnReviewPage ? openReview : () => openReview().then(() => navigate(reviewPageUri))), reopen: openReview && review && review.closedAt && - (isOnReviewPage ? openReview : () => openReview().then(() => push(reviewPageUri))), + (isOnReviewPage ? openReview : () => openReview().then(() => navigate(reviewPageUri))), openClose: setReviewState && (!review || !review.startedAt) && showAllButtons && (() => setReviewState(true)), close: setReviewState && review && review.startedAt && !review.closedAt && (() => setReviewState(true)), delete: showAllButtons && review && review.startedAt && deleteReview, @@ -224,13 +225,7 @@ SolutionActions.propTypes = { setReviewState: PropTypes.func, deleteReview: PropTypes.func, setPoints: PropTypes.func, - history: PropTypes.shape({ - push: PropTypes.func.isRequired, - }), - location: PropTypes.shape({ - pathname: PropTypes.string.isRequired, - }).isRequired, links: PropTypes.object.isRequired, }; -export default withLinks(withRouter(SolutionActions)); +export default withLinks(SolutionActions); diff --git a/src/components/forms/ForkExerciseForm/ForkExerciseForm.js b/src/components/forms/ForkExerciseForm/ForkExerciseForm.js index fe915fbdb..dffd75167 100644 --- a/src/components/forms/ForkExerciseForm/ForkExerciseForm.js +++ b/src/components/forms/ForkExerciseForm/ForkExerciseForm.js @@ -1,11 +1,11 @@ -import React, { Component } from 'react'; +import React from 'react'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import { injectIntl, FormattedMessage, defineMessages } from 'react-intl'; import { Form } from 'react-bootstrap'; import { connect } from 'react-redux'; import { reduxForm, Field } from 'redux-form'; -import { withRouter } from 'react-router'; +import { useNavigate } from 'react-router-dom'; import { SelectField } from '../Fields'; import SubmitButton from '../SubmitButton'; @@ -29,135 +29,121 @@ const messages = defineMessages({ }, }); -class ForkExerciseForm extends Component { - viewForkedExercise = () => { - const { - forkedExerciseId: id, - history: { push }, - links: { EXERCISE_URI_FACTORY }, - } = this.props; - push(EXERCISE_URI_FACTORY(id)); +const ForkExerciseForm = ({ + forkStatus, + resetId, + submitting, + handleSubmit, + submitFailed, + submitSucceeded, + invalid, + groups, + groupsAccessor, + forkedExerciseId, + links: { EXERCISE_URI_FACTORY }, + intl: { locale, formatMessage }, +}) => { + const navigate = useNavigate(); + const viewForkedExercise = () => { + navigate(EXERCISE_URI_FACTORY(forkedExerciseId)); }; - render() { - const { - forkStatus, - resetId, - submitting, - handleSubmit, - submitFailed, - submitSucceeded, - invalid, - groups, - groupsAccessor, - intl: { locale, formatMessage }, - } = this.props; - - if (forkStatus === forkStatuses.FULFILLED) { - return ( - - - - - + + +
-

+ if (forkStatus === forkStatuses.FULFILLED) { + return ( + + + + + + - - - -
+

+ +

+
+ + - - + {resetId && ( + - {resetId && ( - - )} - -
-
- ); - } - - return ( -

- {submitFailed && ( - - - - - )} -
- - {groups => ( - <> - hasPermissions(group, 'createExercise')) - .map(group => ({ - key: group.id, - name: getGroupCanonicalLocalizedName(group, groupsAccessor, locale), - })) - .sort((a, b) => a.name.localeCompare(b.name, locale))} - append={ - } - noShadow - confirmQuestion={ - - } - messages={{ - submit: , - submitting: ( - - ), - success: ( - - ), - }} - /> - } - /> - - )} - -
-
+ )} + +
+
); } -} + + return ( +
+ {submitFailed && ( + + + + + )} +
+ + {groups => ( + <> + hasPermissions(group, 'createExercise')) + .map(group => ({ + key: group.id, + name: getGroupCanonicalLocalizedName(group, groupsAccessor, locale), + })) + .sort((a, b) => a.name.localeCompare(b.name, locale))} + append={ + } + noShadow + confirmQuestion={ + + } + messages={{ + submit: , + submitting: , + success: , + }} + /> + } + /> + + )} + +
+
+ ); +}; ForkExerciseForm.propTypes = { - history: PropTypes.shape({ - push: PropTypes.func.isRequired, - replace: PropTypes.func.isRequired, - }), exerciseId: PropTypes.string.isRequired, forkId: PropTypes.string.isRequired, forkStatus: PropTypes.string, @@ -199,6 +185,6 @@ export default withLinks( reduxForm({ form: 'forkExercise', validate, - })(injectIntl(withRouter(ForkExerciseForm))) + })(injectIntl(ForkExerciseForm)) ) ); diff --git a/src/components/forms/RelocateGroupForm/RelocateGroupForm.js b/src/components/forms/RelocateGroupForm/RelocateGroupForm.js index e4cd581d2..10f104822 100644 --- a/src/components/forms/RelocateGroupForm/RelocateGroupForm.js +++ b/src/components/forms/RelocateGroupForm/RelocateGroupForm.js @@ -78,10 +78,6 @@ const RelocateGroupForm = ({ ); RelocateGroupForm.propTypes = { - history: PropTypes.shape({ - push: PropTypes.func.isRequired, - replace: PropTypes.func.isRequired, - }), submitting: PropTypes.bool, submitFailed: PropTypes.bool, submitSucceeded: PropTypes.bool, diff --git a/src/components/layout/Navigation/Navigation.js b/src/components/layout/Navigation/Navigation.js index a24e7af85..65e87f59d 100644 --- a/src/components/layout/Navigation/Navigation.js +++ b/src/components/layout/Navigation/Navigation.js @@ -2,8 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Card, OverlayTrigger, Tooltip } from 'react-bootstrap'; import { FormattedMessage } from 'react-intl'; -import { withRouter } from 'react-router'; -import { Link } from 'react-router-dom'; +import { Link, useLocation } from 'react-router-dom'; import AssignmentNameContainer from '../../../containers/AssignmentNameContainer'; import ExercisesNameContainer from '../../../containers/ExercisesNameContainer'; @@ -55,10 +54,10 @@ const Navigation = ({ emphasizeUser = false, links, secondaryLinks, - location, titlePrefix = null, titleSuffix = null, }) => { + const location = useLocation(); links = Array.isArray(links) ? links.filter(link => link) : []; secondaryLinks = Array.isArray(secondaryLinks) ? secondaryLinks.filter(link => link) : []; const onlyUser = Boolean(userId && !groupId && !exerciseId && !assignmentId && !shadowId); @@ -195,12 +194,8 @@ Navigation.propTypes = { emphasizeUser: PropTypes.bool, links: PropTypes.array, secondaryLinks: PropTypes.array, - location: PropTypes.shape({ - pathname: PropTypes.string.isRequired, - search: PropTypes.string.isRequired, - }).isRequired, titlePrefix: PropTypes.oneOfType([PropTypes.element, PropTypes.string]), titleSuffix: PropTypes.oneOfType([PropTypes.element, PropTypes.string]), }; -export default withRouter(Navigation); +export default Navigation; diff --git a/src/components/layout/Sidebar/Sidebar.js b/src/components/layout/Sidebar/Sidebar.js index d62fdcef7..1fa34db88 100644 --- a/src/components/layout/Sidebar/Sidebar.js +++ b/src/components/layout/Sidebar/Sidebar.js @@ -2,9 +2,8 @@ import React from 'react'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import classnames from 'classnames'; -import { FormattedMessage, injectIntl } from 'react-intl'; +import { FormattedMessage } from 'react-intl'; import { defaultMemoize } from 'reselect'; -import { withRouter } from 'react-router'; import { Link } from 'react-router-dom'; import Admin from './Admin'; @@ -27,11 +26,8 @@ const getUserData = defaultMemoize(user => getJsData(user)); const Sidebar = ({ pendingFetchOperations, - isCollapsed, loggedInUser, effectiveRole = null, - studentOf, - supervisorOf, currentUrl, instances, small = false, @@ -47,12 +43,8 @@ const Sidebar = ({ ARCHIVE_URI, SIS_INTEGRATION_URI, }, - location: { pathname, search }, - intl: { locale }, }) => { const user = getUserData(loggedInUser); - // The following might get handy yet - // const currentLink = pathname + search; return (