Skip to content

Commit 9a11fef

Browse files
authored
(chore) optimize dashboard interactions (#531)
* (chore) optimize dashboard interactions * (fix) preserve duplicate course drag ids
1 parent e0a0795 commit 9a11fef

5 files changed

Lines changed: 53 additions & 9 deletions

File tree

frontend/src/Popover.jsx

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { bindManualHoverPopover } from 'utils/manualHoverPopover';
88

99
const BASE_COURSE_OFFERINGS_URL = 'https://www.princetoncourses.com/course/';
1010
const COURSE_POPOVER_CLEANUP_KEY = '__tigerpathCoursePopoverCleanup';
11+
const COURSE_POPOVER_CONTENT_KEY = '__tigerpathCoursePopoverContent';
1112

1213
function getRatingColor(rating) {
1314
if (rating == null) return '#e0e0e0';
@@ -40,11 +41,6 @@ export function addPopover(course, courseKey, semIndex, duplicateCourseCounts =
4041
const courseElement = document.getElementById(courseKey);
4142
if (!courseElement) return;
4243

43-
const existingCleanup = courseElement[COURSE_POPOVER_CLEANUP_KEY];
44-
if (typeof existingCleanup === 'function') {
45-
existingCleanup();
46-
}
47-
4844
let courseId = course['id'];
4945
let courseSemList = course['semester_list'];
5046
if (courseSemList && courseSemList.length > 0) {
@@ -97,6 +93,17 @@ export function addPopover(course, courseKey, semIndex, duplicateCourseCounts =
9793
}
9894

9995
courseElement.setAttribute('data-bs-content', content);
96+
const contentSignature = `${titleHtml}|${content}`;
97+
const existingCleanup = courseElement[COURSE_POPOVER_CLEANUP_KEY];
98+
if (
99+
typeof existingCleanup === 'function' &&
100+
courseElement[COURSE_POPOVER_CONTENT_KEY] === contentSignature
101+
) {
102+
return;
103+
}
104+
if (typeof existingCleanup === 'function') {
105+
existingCleanup();
106+
}
100107

101108
// Use Bootstrap 5 Popover API
102109
const Popover = window.bootstrap?.Popover;
@@ -123,5 +130,7 @@ export function addPopover(course, courseKey, semIndex, duplicateCourseCounts =
123130
cleanupHoverBehavior();
124131
popoverInstance.dispose();
125132
delete courseElement[COURSE_POPOVER_CLEANUP_KEY];
133+
delete courseElement[COURSE_POPOVER_CONTENT_KEY];
126134
};
135+
courseElement[COURSE_POPOVER_CONTENT_KEY] = contentSignature;
127136
}

frontend/src/components/Requirements.jsx

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ import TreeView from 'react-treeview/lib/react-treeview.js';
88
import { v1 as uuidv1 } from 'uuid';
99

1010
const REQUIREMENTS_POPOVER_CLEANUP_KEY = '__tigerpathReqPopoverCleanup';
11+
const REQUIREMENTS_POPOVER_CONTENT_KEY = '__tigerpathReqPopoverContent';
1112
const HEADER_POPOVER_CLEANUP_KEY = '__tigerpathHeaderPopoverCleanup';
13+
const HEADER_POPOVER_CONTENT_KEY = '__tigerpathHeaderPopoverContent';
1214
const TREE_ITEM_CLICK_HANDLER_KEY = '__tigerpathTreeItemClickHandler';
1315

1416
function escapeHref(url) {
@@ -502,7 +504,7 @@ export default function Requirements({ onChange, requirements, schedule, activeP
502504
items.forEach((item) => {
503505
const existingHandler = item[TREE_ITEM_CLICK_HANDLER_KEY];
504506
if (typeof existingHandler === 'function') {
505-
item.removeEventListener('click', existingHandler);
507+
return;
506508
}
507509

508510
const clickHandler = (event) => {
@@ -541,7 +543,18 @@ export default function Requirements({ onChange, requirements, schedule, activeP
541543
'.reqLabel:not(.reqLabel-main)'
542544
);
543545
reqLabels.forEach((reqLabel) => {
546+
const contentSignature = [
547+
reqLabel.getAttribute('reqpath') || '',
548+
reqLabel.getAttribute('title') || '',
549+
reqLabel.getAttribute('data-bs-content') || '',
550+
].join('|');
544551
const existingCleanup = reqLabel[REQUIREMENTS_POPOVER_CLEANUP_KEY];
552+
if (
553+
typeof existingCleanup === 'function' &&
554+
reqLabel[REQUIREMENTS_POPOVER_CONTENT_KEY] === contentSignature
555+
) {
556+
return;
557+
}
545558
if (typeof existingCleanup === 'function') existingCleanup();
546559

547560
const existing = Popover.getInstance(reqLabel);
@@ -583,7 +596,9 @@ export default function Requirements({ onChange, requirements, schedule, activeP
583596
cleanupHoverBehavior();
584597
popoverInstance.dispose();
585598
delete reqLabel[REQUIREMENTS_POPOVER_CLEANUP_KEY];
599+
delete reqLabel[REQUIREMENTS_POPOVER_CONTENT_KEY];
586600
};
601+
reqLabel[REQUIREMENTS_POPOVER_CONTENT_KEY] = contentSignature;
587602
});
588603
}, []); // eslint-disable-line react-hooks/exhaustive-deps
589604

@@ -594,7 +609,19 @@ export default function Requirements({ onChange, requirements, schedule, activeP
594609

595610
const icons = containerRef.current.querySelectorAll('.info-icon');
596611
icons.forEach((icon) => {
612+
const contentSignature = [
613+
icon.getAttribute('data-tp-info') || '',
614+
icon.getAttribute('data-tp-ref0') || '',
615+
icon.getAttribute('data-tp-ref1') || '',
616+
icon.getAttribute('data-bs-content') || '',
617+
].join('|');
597618
const existingCleanup = icon[HEADER_POPOVER_CLEANUP_KEY];
619+
if (
620+
typeof existingCleanup === 'function' &&
621+
icon[HEADER_POPOVER_CONTENT_KEY] === contentSignature
622+
) {
623+
return;
624+
}
598625
if (typeof existingCleanup === 'function') existingCleanup();
599626

600627
const existing = Popover.getInstance(icon);
@@ -640,7 +667,9 @@ export default function Requirements({ onChange, requirements, schedule, activeP
640667
cleanupHover();
641668
popoverInstance.dispose();
642669
delete icon[HEADER_POPOVER_CLEANUP_KEY];
670+
delete icon[HEADER_POPOVER_CONTENT_KEY];
643671
};
672+
icon[HEADER_POPOVER_CONTENT_KEY] = contentSignature;
644673
});
645674
}, []);
646675

frontend/src/components/Schedule.jsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import React, { useEffect } from 'react';
22
import { apiFetch } from 'utils/api';
33
import Semester from 'components/Semester';
44
import styled from 'styled-components';
5-
import { DEFAULT_SCHEDULE } from 'utils/SemesterUtils';
5+
import { DEFAULT_SCHEDULE, getScheduledCourseKey } from 'utils/SemesterUtils';
66
import { addPopover } from 'Popover';
77

88
const YEARS = [
@@ -112,7 +112,7 @@ export default function Schedule({ onChange, profile, schedule }) {
112112
for (let semIndex = 0; semIndex < sched.length; semIndex++) {
113113
for (let courseIndex = 0; courseIndex < sched[semIndex].length; courseIndex++) {
114114
let course = sched[semIndex][courseIndex];
115-
let courseKey = `course-card-${course['semester']}-${semIndex}-${courseIndex}`;
115+
let courseKey = getScheduledCourseKey(course, semIndex, courseIndex);
116116
addPopover(course, courseKey, semIndex, courseNameCounts);
117117
}
118118
}

frontend/src/components/Semester.jsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
SEMESTER_TYPE,
77
EXTERNAL_CREDITS_SEMESTER_INDEX,
88
getSemesterType,
9+
getScheduledCourseKey,
910
isFallSemester,
1011
isSpringSemester,
1112
} from 'utils/SemesterUtils';
@@ -111,7 +112,7 @@ export default function Semester({ onChange, schedule, semesterIndex, className,
111112
return (
112113
<>
113114
{semester[semIndex].map((course, courseIndex) => {
114-
let courseKey = `course-card-${course['semester']}-${semIndex}-${courseIndex}`;
115+
let courseKey = getScheduledCourseKey(course, semIndex, courseIndex);
115116
return (
116117
<CourseCard
117118
key={courseKey}

frontend/src/utils/SemesterUtils.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@ export function getSemesterNames(classYear) {
4444
return semesterNames;
4545
}
4646

47+
export function getScheduledCourseKey(course, semIndex, courseIndex) {
48+
const stableId = course?.id || `${semIndex}-${courseIndex}`;
49+
return `course-card-${course?.semester || 'both'}-${stableId}-${semIndex}-${courseIndex}`;
50+
}
51+
4752
/* Converts semester to term code */
4853
export function convertSemToTermCode(sem) {
4954
let code = '1';

0 commit comments

Comments
 (0)