Skip to content

Commit 86abb05

Browse files
authored
Additional UI browser back button fixes. (#1601)
Closes SoftUni-Internal/exam-systems-issues#1693 Closes SoftUni-Internal/exam-systems-issues#1684
1 parent b9d3d21 commit 86abb05

File tree

12 files changed

+151
-83
lines changed

12 files changed

+151
-83
lines changed

Servers/UI/OJS.Servers.Ui/ClientApp/src/components/contests/contest-problems/ContestProblems.tsx

Lines changed: 91 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import React, { useEffect, useState } from 'react';
2-
import { useLocation } from 'react-router-dom';
1+
import React, { useEffect, useRef, useState } from 'react';
2+
import { useLocation, useNavigationType } from 'react-router-dom';
33
import { Popover } from '@mui/material';
44

55
import { IProblemType } from '../../../common/types';
@@ -12,7 +12,7 @@ import styles from './ContestProblems.module.scss';
1212

1313
interface IContestProblemsProps {
1414
problems: Array<IProblemType>;
15-
onContestProblemChange: () => void;
15+
onContestProblemChange?: () => void;
1616
totalParticipantsCount?: number;
1717
sumMyPoints?: number;
1818
sumTotalPoints: number;
@@ -29,9 +29,11 @@ const ContestProblems = (props: IContestProblemsProps) => {
2929

3030
const { hash } = useLocation();
3131
const dispatch = useAppDispatch();
32+
const navigationType = useNavigationType();
3233
const { isDarkMode, themeColors, getColorClassName } = useTheme();
3334
const { selectedContestDetailsProblem } = useAppSelector((state) => state.contests);
34-
35+
const problemRefs = useRef<Map<number, HTMLDivElement>>(new Map());
36+
const wrapperRef = useRef<HTMLDivElement | null>(null);
3537
const [ excludedFromHomeworkAnchorElement, setExcludedFromHomeworkAnchorElement ] = useState<HTMLElement | null>(null);
3638

3739
const backgroundColorClassName = getColorClassName(isDarkMode
@@ -45,21 +47,40 @@ const ContestProblems = (props: IContestProblemsProps) => {
4547
const isExcludedFromHomeworkModalOpen = Boolean(excludedFromHomeworkAnchorElement);
4648

4749
useEffect(() => {
48-
if (!problems) {
49-
return;
50-
}
50+
if (!problems) { return; }
5151

5252
const selectedProblem = problems.find((prob) => prob.orderBy === Number(hash.substring(1)));
5353
if (selectedProblem) {
5454
dispatch(setSelectedContestDetailsProblem({ selectedProblem }));
55+
56+
/*
57+
Scroll to the currently selected problem only when
58+
the browser's back button is clicked.
59+
*/
60+
if (navigationType === 'POP') {
61+
const target = problemRefs.current.get(selectedProblem.id);
62+
const wrapper = wrapperRef.current;
63+
64+
if (target && wrapper) {
65+
const wrapperTop = wrapper.getBoundingClientRect().top;
66+
const targetTop = target.getBoundingClientRect().top;
67+
const offset = targetTop - wrapperTop + wrapper.scrollTop;
68+
69+
wrapper.scrollTo({
70+
top: offset - wrapper.clientHeight / 2,
71+
behavior: 'smooth',
72+
});
73+
}
74+
}
5575
} else {
5676
dispatch(setSelectedContestDetailsProblem({ selectedProblem: problems[0] }));
5777
}
58-
// eslint-disable-next-line react-hooks/exhaustive-deps
59-
}, []);
78+
}, [ dispatch, hash, problems, navigationType ]);
6079

6180
const onProblemClick = (problem: IProblemType) => {
62-
onContestProblemChange();
81+
if (onContestProblemChange) {
82+
onContestProblemChange();
83+
}
6384
dispatch(setSelectedContestDetailsProblem({ selectedProblem: problem }));
6485
};
6586

@@ -69,66 +90,74 @@ const ContestProblems = (props: IContestProblemsProps) => {
6990
<div>Tasks</div>
7091
<div>Points</div>
7192
</div>
72-
<div className={`${styles.problemsWrapper} ${backgroundColorClassName}`}>
73-
<div className={styles.problemsInnerWrapper}>
93+
<div className={`${styles.problemsWrapper} ${backgroundColorClassName}`} ref={wrapperRef}>
94+
<div
95+
className={styles.problemsInnerWrapper}
96+
>
7497
{problems.map((problem, idx) => {
7598
const isActive = selectedContestDetailsProblem?.id === problem.id;
7699
const isLast = idx === problems.length - 1;
77100
return (
78-
<LinkButton
79-
to={`#${problem.orderBy}`}
80-
type={LinkButtonType.plain}
81-
preventScrollReset
82-
key={`contest-problem-${problem.id}`}
83-
className={`${styles.problem} ${colorClassName} ${isActive
84-
? styles.activeProblem
85-
: ''}`}
86-
style={{
87-
borderBottom: `${isLast
88-
? 0
89-
: 1}px solid ${themeColors.textColor}`,
101+
<div
102+
ref={(el) => {
103+
if (el) { problemRefs.current.set(problem.id, el); }
90104
}}
91-
onClick={() => onProblemClick(problem)}
105+
key={`contest-problem-${problem.id}`}
92106
>
93-
<div className={styles.problemName}>
94-
{problem.name}
95-
{problem.isExcludedFromHomework && (
96-
<div
97-
style={{ display: 'inline' }}
98-
onMouseEnter={(e) => setExcludedFromHomeworkAnchorElement(e.currentTarget)}
99-
onMouseLeave={() => setExcludedFromHomeworkAnchorElement(null)}
100-
>
101-
<span className={styles.excludedMark}>*</span>
102-
{' '}
103-
<Popover
104-
open={isExcludedFromHomeworkModalOpen}
105-
anchorEl={excludedFromHomeworkAnchorElement}
106-
anchorOrigin={{
107-
vertical: 'bottom',
108-
horizontal: 'left',
109-
}}
110-
transformOrigin={{
111-
vertical: 'top',
112-
horizontal: 'left',
113-
}}
114-
sx={{ pointerEvents: 'none' }}
115-
onClose={() => setExcludedFromHomeworkAnchorElement(null)}
116-
disableRestoreFocus
117-
>
118-
<div className={`${styles.excludedFromHomeworkModal} ${modalBackgroundColorClassName}`}>
119-
The score received from this problem would not be included
120-
in the final results for this contest.
107+
<LinkButton
108+
to={`#${problem.orderBy}`}
109+
type={LinkButtonType.plain}
110+
preventScrollReset
111+
className={`${styles.problem} ${colorClassName} ${isActive
112+
? styles.activeProblem
113+
: ''}`}
114+
style={{
115+
borderBottom: `${isLast
116+
? 0
117+
: 1}px solid ${themeColors.textColor}`,
118+
}}
119+
onClick={() => onProblemClick(problem)}
120+
>
121+
<div className={styles.problemName}>
122+
{problem.name}
123+
{problem.isExcludedFromHomework && (
124+
<div
125+
style={{ display: 'inline' }}
126+
onMouseEnter={(e) => setExcludedFromHomeworkAnchorElement(e.currentTarget)}
127+
onMouseLeave={() => setExcludedFromHomeworkAnchorElement(null)}
128+
>
129+
<span className={styles.excludedMark}>*</span>
130+
{' '}
131+
<Popover
132+
open={isExcludedFromHomeworkModalOpen}
133+
anchorEl={excludedFromHomeworkAnchorElement}
134+
anchorOrigin={{
135+
vertical: 'bottom',
136+
horizontal: 'left',
137+
}}
138+
transformOrigin={{
139+
vertical: 'top',
140+
horizontal: 'left',
141+
}}
142+
sx={{ pointerEvents: 'none' }}
143+
onClose={() => setExcludedFromHomeworkAnchorElement(null)}
144+
disableRestoreFocus
145+
>
146+
<div className={`${styles.excludedFromHomeworkModal} ${modalBackgroundColorClassName}`}>
147+
The score received from this problem would not be included
148+
in the final results for this contest.
149+
</div>
150+
</Popover>
121151
</div>
122-
</Popover>
152+
)}
153+
</div>
154+
<div>
155+
{problem.points || 0}
156+
/
157+
{problem.maximumPoints}
123158
</div>
124-
)}
125-
</div>
126-
<div>
127-
{problem.points || 0}
128-
/
129-
{problem.maximumPoints}
130-
</div>
131-
</LinkButton>
159+
</LinkButton>
160+
</div>
132161
);
133162
})}
134163
</div>

Servers/UI/OJS.Servers.Ui/ClientApp/src/components/filters/Filter.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -840,17 +840,20 @@ const applyDefaultQueryValues = (
840840

841841
const handlePageChange = (
842842
setQueryParams: Dispatch<React.SetStateAction<IGetSubmissionsUrlParams>>,
843-
setSearchParams: (params: URLSearchParamsInit, navigateOpts?: NavigateOptions) => void,
844843
newPage: number,
844+
navigate: (to: string, options?: NavigateOptions) => void,
845845
) => {
846846
setQueryParams((prev) => {
847847
const updatedParams = { ...prev, page: newPage };
848848

849849
const newParams = new URLSearchParams(window.location.search);
850-
851850
newParams.set('page', newPage.toString());
852851

853-
setSearchParams(newParams);
852+
// Use navigate to preserve the hash
853+
const hash = window.location.hash;
854+
const newUrl = `${window.location.pathname}?${newParams.toString()}${hash}`;
855+
navigate(newUrl, { preventScrollReset: true });
856+
854857
return updatedParams;
855858
});
856859
};

Servers/UI/OJS.Servers.Ui/ClientApp/src/components/guidelines/pagination/PaginationControls.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,14 @@ interface IPaginationControlsProps extends IHaveOptionalClassName {
1313
count: number;
1414
page: number;
1515
onChange: (value: number) => void | undefined;
16+
isDataFetching: boolean;
1617
}
1718

1819
const PaginationControls = ({
1920
count,
2021
page,
2122
onChange,
23+
isDataFetching,
2224
className = '',
2325
} : IPaginationControlsProps) => {
2426
const { themeColors, getColorClassName } = useTheme();
@@ -35,6 +37,7 @@ const PaginationControls = ({
3537
'& .MuiPaginationItem-root.Mui-selected': { backgroundColor: '#44a9f8', color: '#ffffff' },
3638
'& .MuiPaginationItem-root': { color: themeColors.textColor },
3739
'& .MuiPaginationItem-ellipsis': { cursor: 'pointer' },
40+
'& .Mui-disabled': { pointerEvents: 'none' },
3841
},
3942
ellipsis: {
4043
pointerEvents: 'auto',
@@ -45,6 +48,10 @@ const PaginationControls = ({
4548
const classes = useStyles();
4649

4750
const handleEllipsisClick = (type: string) => {
51+
if (isDataFetching) {
52+
return;
53+
}
54+
4855
let newPage;
4956

5057
if (type === 'start-ellipsis') {
@@ -72,6 +79,7 @@ const PaginationControls = ({
7279
classes={{ ul: classes.ul }}
7380
showFirstButton
7481
showLastButton
82+
disabled={isDataFetching}
7583
renderItem={(item) => {
7684
if (item.type === 'start-ellipsis') {
7785
return (

Servers/UI/OJS.Servers.Ui/ClientApp/src/components/profile/profile-contest-participations/ProfileContestParticipations.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ const ProfileContestParticipations = ({
6565
const {
6666
data: userContestParticipations,
6767
isLoading: areContestParticipationsLoading,
68+
isFetching: areContestParticipationsFetching,
6869
error: contestParticipationsQueryError,
6970
} = useGetContestsParticipationsForUserQuery(
7071
{
@@ -90,12 +91,16 @@ const ProfileContestParticipations = ({
9091
const {
9192
data: allParticipatedContests,
9293
isLoading: areAllContestsLoading,
94+
isFetching: areAllContestsFetching,
9395
} = useGetAllParticipatedContestsQuery(
9496
// eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain
9597
{ username: profile?.userName! },
9698
{ skip: !canFetchParticipations },
9799
);
98100

101+
const isDataLoading = areContestParticipationsLoading || areAllContestsLoading;
102+
const isDataFetching = areAllContestsFetching || areContestParticipationsFetching;
103+
99104
useEffect(() => {
100105
const pageParam = parseInt(searchParams.get('page') || '1', 10);
101106
if (Number.isInteger(pageParam) && pageParam >= 1) {
@@ -238,7 +243,7 @@ const ProfileContestParticipations = ({
238243
/>
239244
), [ internalUser, userIsProfileOwner ]);
240245

241-
if (areContestParticipationsLoading || areAllContestsLoading) {
246+
if (isDataLoading) {
242247
return <div style={{ ...flexCenterObjectStyles, minHeight: '200px' }}><SpinningLoader /></div>;
243248
}
244249

@@ -288,6 +293,7 @@ const ProfileContestParticipations = ({
288293
{!isEmpty(userContestParticipations?.items) &&
289294
userContestParticipations && userContestParticipations.pagesCount > 1 && (
290295
<PaginationControls
296+
isDataFetching={isDataFetching}
291297
count={userContestParticipations.pagesCount}
292298
page={userContestParticipations.pageNumber}
293299
onChange={onPageChange}

Servers/UI/OJS.Servers.Ui/ClientApp/src/components/profile/profile-submissions/ProfileSubmisssions.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ const ProfileSubmissions = ({ userIsProfileOwner, isChosenInToggle }: IProfileSu
3737
const {
3838
data: userSubmissions,
3939
isLoading: areSubmissionsLoading,
40+
isFetching: areSubmissionsFetching,
4041
error: userSubmissionsQueryError,
4142
} = useGetUserSubmissionsQuery(
4243
// eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain
@@ -66,6 +67,7 @@ const ProfileSubmissions = ({ userIsProfileOwner, isChosenInToggle }: IProfileSu
6667

6768
return (
6869
<SubmissionsGrid
70+
isDataFetching={areSubmissionsFetching}
6971
isDataLoaded={!areSubmissionsLoading}
7072
submissions={userSubmissions!}
7173
className={styles.profileSubmissionsGrid}
@@ -89,7 +91,8 @@ const ProfileSubmissions = ({ userIsProfileOwner, isChosenInToggle }: IProfileSu
8991
shouldRender,
9092
userIsProfileOwner,
9193
userSubmissions,
92-
userSubmissionsQueryError ]);
94+
userSubmissionsQueryError,
95+
areSubmissionsFetching ]);
9396

9497
return render();
9598
};

Servers/UI/OJS.Servers.Ui/ClientApp/src/components/submissions/recent-submissions/RecentSubmissions.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,7 @@ const RecentSubmissions = () => {
254254
)
255255
: (
256256
<SubmissionsGrid
257+
isDataFetching={areSubmissionsFetching}
257258
className={styles.recentSubmissionsGrid}
258259
isDataLoaded={!areSubmissionsLoading}
259260
submissions={latestSubmissions}

Servers/UI/OJS.Servers.Ui/ClientApp/src/components/submissions/submissions-grid/SubmissionsGrid.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Dispatch, SetStateAction, useCallback, useMemo, useState } from 'react';
2-
import { NavigateOptions, URLSearchParamsInit } from 'react-router-dom';
2+
import { NavigateOptions, URLSearchParamsInit, useNavigate } from 'react-router-dom';
33
import isEmpty from 'lodash/isEmpty';
44
import { IDictionary } from 'src/common/common-types';
55
import { FilterColumnTypeEnum } from 'src/common/enums';
@@ -19,6 +19,7 @@ import styles from './SubmissionsGrid.module.scss';
1919

2020
interface ISubmissionsGridProps extends IHaveOptionalClassName {
2121
isDataLoaded: boolean;
22+
isDataFetching: boolean;
2223
submissions?: IPagedResultType<IPublicSubmission>;
2324
options: ISubmissionsGridOptions;
2425
searchParams: URLSearchParams;
@@ -37,6 +38,7 @@ interface ISubmissionsGridOptions {
3738
const SubmissionsGrid = ({
3839
className,
3940
isDataLoaded,
41+
isDataFetching,
4042
submissions,
4143
options,
4244
searchParams,
@@ -45,6 +47,7 @@ const SubmissionsGrid = ({
4547
}: ISubmissionsGridProps) => {
4648
const { isDarkMode, getColorClassName, themeColors } = useTheme();
4749
const { internalUser: user } = useAppSelector((state) => state.authorization);
50+
const navigate = useNavigate();
4851

4952
const [ selectedFilters, setSelectedFilters ] = useState<IDictionary<Array<IFilter>>>(mapUrlToFilters(searchParams, [
5053
{ name: 'Id', id: 'Id', columnType: FilterColumnTypeEnum.NUMBER },
@@ -59,7 +62,7 @@ const SubmissionsGrid = ({
5962
const isAdmin = user.isAdmin;
6063

6164
const onPageChange = (page: number) => {
62-
handlePageChange(setQueryParams, setSearchParams, page);
65+
handlePageChange(setQueryParams, page, navigate);
6366
};
6467

6568
const handleToggleFilter = (filterId: string | null) => {
@@ -269,6 +272,7 @@ const SubmissionsGrid = ({
269272
{renderSubmissionsGrid()}
270273
{submissions && areItemsAvailable && submissions?.pagesCount !== 0 && (
271274
<PaginationControls
275+
isDataFetching={isDataFetching}
272276
count={submissions.pagesCount}
273277
page={submissions.pageNumber}
274278
onChange={onPageChange}

0 commit comments

Comments
 (0)