From 49c31a26293e5f1c38634c99fdc373b85e371c30 Mon Sep 17 00:00:00 2001 From: japleenkaur Date: Fri, 14 Nov 2025 14:18:27 -0500 Subject: [PATCH 1/8] Convert FocusBar from class component to function component --- js/components/graph/FocusBar.js | 50 +++++++++++++-------------------- 1 file changed, 20 insertions(+), 30 deletions(-) diff --git a/js/components/graph/FocusBar.js b/js/components/graph/FocusBar.js index 0ab8b2dd6..422cd5fbf 100644 --- a/js/components/graph/FocusBar.js +++ b/js/components/graph/FocusBar.js @@ -15,31 +15,23 @@ const computerScienceFocusLabels = [ ["ASFOC1689B", "Artificial Intelligence"], ] -/** - * React component representing the focus menu bar - */ -export default class FocusBar extends React.Component { - constructor(props) { - super(props) - this.state = { - open: false, - } - } +export default function FocusBar({focusBarEnabled, highlightFocus, currFocus}) { + const [open, setOpen] = React.useState(false) /** * Changes whether the focus bar is open or not */ - toggleFocusBar = () => { - this.setState({ open: !this.state.open }) + const toggleFocusBar = () => { + setOpen(!open) } /** * Creates the menu items of the focus bar using the FocusTab component * @returns an array of FocusTab components */ - generateFocusTabs = () => { + const generateFocusTabs = () => { return computerScienceFocusLabels.map(([focusId, focusTitle]) => { - const selected = this.props.currFocus === focusId + const selected = currFocus === focusId return ( ) }) } - render() { - if (!this.props.focusBarEnabled) { - return null - } else { - return ( -
- -
- {this.state.open && this.generateFocusTabs()} -
-
- ) - } + if (!focusBarEnabled) { + return null } + + return ( +
+ +
+ {open && generateFocusTabs()} +
+
+ ) } FocusBar.propTypes = { From c8b4bfb030a5e91d820b3fbe2e12d32bf8c5c3e1 Mon Sep 17 00:00:00 2001 From: japleenkaur Date: Sat, 15 Nov 2025 22:43:02 -0500 Subject: [PATCH 2/8] complete class-to-function migration for graph components --- js/components/graph/FocusBar.js | 3 ++ js/components/graph/FocusTab.js | 67 ++++++++++------------- js/components/graph/GraphDropdown.js | 35 ++++++------ js/components/graph/GraphFallback.js | 33 ++++++------ js/components/graph/InfoBox.js | 77 +++++++++++++------------- js/components/graph/Sidebar.js | 81 +++++++++++++--------------- 6 files changed, 137 insertions(+), 159 deletions(-) diff --git a/js/components/graph/FocusBar.js b/js/components/graph/FocusBar.js index 422cd5fbf..78e06ac7c 100644 --- a/js/components/graph/FocusBar.js +++ b/js/components/graph/FocusBar.js @@ -15,6 +15,9 @@ const computerScienceFocusLabels = [ ["ASFOC1689B", "Artificial Intelligence"], ] +/** +* React component representing the focus menu bar +*/ export default function FocusBar({focusBarEnabled, highlightFocus, currFocus}) { const [open, setOpen] = React.useState(false) diff --git a/js/components/graph/FocusTab.js b/js/components/graph/FocusTab.js index e10cd24e2..9836ffe68 100644 --- a/js/components/graph/FocusTab.js +++ b/js/components/graph/FocusTab.js @@ -5,51 +5,42 @@ import { FocusModal } from "../common/react_modal.js.jsx" /** * React component representing an item on the focus menu bar */ -export default class FocusTab extends React.Component { - constructor(props) { - super(props) - this.state = { - showFocusModal: false, - } - } +export default function FocusTab({focusName, highlightFocus, selected, pId}) { + const [showFocusModal, setShowFocusModal] = React.useState(false) /** * Change whether the modal popup describing this focus is shown * @param {bool} value */ - toggleFocusModal = value => { - this.setState({ - showFocusModal: value, - }) - } + const toggleFocusModal = (value => { + setShowFocusModal(value) + }) - render() { - return ( -
- -
- this.toggleFocusModal(false)} - /> - {this.props.selected && ( - - )} -
+ return ( +
+ +
+ toggleFocusModal(false)} + /> + {selected && ( + + )}
- ) - } +
+ ) } FocusTab.propTypes = { diff --git a/js/components/graph/GraphDropdown.js b/js/components/graph/GraphDropdown.js index 4eb5fc541..241c642d2 100644 --- a/js/components/graph/GraphDropdown.js +++ b/js/components/graph/GraphDropdown.js @@ -1,39 +1,37 @@ import React from "react" import PropTypes from "prop-types" -export default class GraphDropdown extends React.Component { - render() { +export default function GraphDropdown({showGraphDropdown, onMouseMove, onMouseLeave, graphs = [], updateGraph}) { let className = "hidden" let graphTabLeft = 0 - if (this.props.graphs.length !== 0 && document.querySelector("#nav-graph")) { + if (graphs.length !== 0 && document.querySelector("#nav-graph")) { const navGraph = document.querySelector("#nav-graph") - if (this.props.graphs.length === 0) { - navGraph.classList.remove("show-dropdown-arrow") - } else { - if (!navGraph.classList.contains("show-dropdown-arrow")) { - navGraph.classList.add("show-dropdown-arrow") - } - if (this.props.showGraphDropdown) { - graphTabLeft = navGraph.getBoundingClientRect().left - className = "graph-dropdown-display" - } + if (graphs.length === 0) { + navGraph.classList.remove("show-dropdown-arrow") + } else { + if (!navGraph.classList.contains("show-dropdown-arrow")) { + navGraph.classList.add("show-dropdown-arrow") + } + if (showGraphDropdown) { + graphTabLeft = navGraph.getBoundingClientRect().left + className = "graph-dropdown-display" + } } } - return (
    - {this.props.graphs.map((graph, i) => { + {graphs.map((graph, i) => { return (
  • this.props.updateGraph(graph.title)} + onClick={() => updateGraph(graph.title)} data-testid={"test-graph-" + i} > {graph.title} @@ -43,7 +41,6 @@ export default class GraphDropdown extends React.Component {
) } -} GraphDropdown.defaultProps = { graphs: [], diff --git a/js/components/graph/GraphFallback.js b/js/components/graph/GraphFallback.js index bafd10d65..023ca1782 100644 --- a/js/components/graph/GraphFallback.js +++ b/js/components/graph/GraphFallback.js @@ -1,28 +1,25 @@ -import React from "react" import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" import { faTriangleExclamation } from "@fortawesome/free-solid-svg-icons" import PropTypes from "prop-types" -export default class GraphFallback extends React.Component { - render() { - const { error } = this.props +export default function GraphFallback(props) { + const { error } = props - return ( -
-
- -
- Your graph has failed to render. Please reload this page or report this - issue to David Liu at {""} - - david@cs.toronto.edu - -

Details: {error.message}

-
+ return ( +
+
+ +
+ Your graph has failed to render. Please reload this page or report this + issue to David Liu at {""} + + david@cs.toronto.edu + +

Details: {error.message}

- ) - } +
+ ) } GraphFallback.propTypes = { diff --git a/js/components/graph/InfoBox.js b/js/components/graph/InfoBox.js index 206d17ff5..7b7134d95 100644 --- a/js/components/graph/InfoBox.js +++ b/js/components/graph/InfoBox.js @@ -1,49 +1,46 @@ -import React from "react" import PropTypes from "prop-types" -export default class InfoBox extends React.Component { - render() { - // guard against rendering with no course - if (!this.props.nodeId) { - return null - } - - const className = this.props.showInfoBox - ? "tooltip-group-display" - : "tooltip-group-hidden" +export default function InfoBox({showInfoBox, nodeId, xPos, yPos, onClick, onMouseEnter, onMouseLeave}) { + // guard against rendering with no course + if (!nodeId) { + return null + } - const rectAttrs = { - id: this.props.nodeId + "-tooltip" + "-rect", - x: this.props.xPos, - y: this.props.yPos, - rx: "4", - ry: "4", - fill: "white", - stroke: "black", - strokeWidth: "2", - width: "60", - height: "30", - } + const className = showInfoBox + ? "tooltip-group-display" + : "tooltip-group-hidden" - const textAttrs = { - id: this.props.nodeId + "-tooltip" + "-text", - x: this.props.xPos + 60 / 2 - 18, - y: this.props.yPos + 30 / 2 + 6, - } + const rectAttrs = { + id: nodeId + "-tooltip" + "-rect", + x: xPos, + y: yPos, + rx: "4", + ry: "4", + fill: "white", + stroke: "black", + strokeWidth: "2", + width: "60", + height: "30", + } - return ( - - - Info - - ) + const textAttrs = { + id: nodeId + "-tooltip" + "-text", + x: xPos + 60 / 2 - 18, + y: yPos + 30 / 2 + 6, } + + return ( + + + Info + + ) } InfoBox.propTypes = { diff --git a/js/components/graph/Sidebar.js b/js/components/graph/Sidebar.js index 69712bffe..f4d2a07b1 100644 --- a/js/components/graph/Sidebar.js +++ b/js/components/graph/Sidebar.js @@ -2,25 +2,20 @@ import React from "react" import PropTypes from "prop-types" import Button from "./Button" -export default class Sidebar extends React.Component { - constructor(props) { - super(props) - this.state = { - collapsed: true, - results: [], - } - } +export default function Sidebar({fceCount, reset, activeCourses, courses, courseClick, xClick, sidebarItemClick, onHover, onMouseLeave}) { + const [collapsed, setCollapsed] = React.useState(true) + const [results, setResults] = React.useState([]) - toggleSidebar = () => { - this.setState({ collapsed: !this.state.collapsed }) + const toggleSidebar = () => { + setCollapsed(!collapsed) } - filteredSearch = query => { - if (!query || !this.props.courses) { + const filteredSearch = (query) => { + if (!query || !courses) { return } - return this.props.courses + return courses .filter(([courseId, courseLabel]) => { return ( courseId.includes(query) || @@ -38,10 +33,10 @@ export default class Sidebar extends React.Component { * "CSC999" will resolve to `null` * @return {string} course node id */ - courseIdFromLabel(courseLabel) { - for (let i = 0; i < this.props.courses.length; i++) { - if (this.props.courses[i][1] === courseLabel) { - return this.props.courses[i][0] + const courseIdFromLabel = (courseLabel) => { + for (let i = 0; i < courses.length; i++) { + if (courses[i][1] === courseLabel) { + return courses[i][0] } } return null @@ -52,10 +47,10 @@ export default class Sidebar extends React.Component { * Render the FCE counter above the sidebar on the left side. * @return {HTMLDivElement} FCE to the DOM */ - renderFCE = () => { - const fceString = Number.isInteger(this.props.fceCount) - ? this.props.fceCount + ".0" - : this.props.fceCount + const renderFCE = () => { + const fceString = Number.isInteger(fceCount) + ? fceCount + ".0" + : fceCount return (
@@ -68,21 +63,21 @@ export default class Sidebar extends React.Component { * Render the dropdown results within the sidebar dropdown. * @return {HTMLDivElement} Searchbar to the DOM */ - renderDropdown = () => { - if (this.props.courses) { - const showDropdown = this.state.results ? "" : "hidden" + const renderDropdown = () => { + if (courses) { + const showDropdown = results ? "" : "hidden" const masterDropdown = `${showDropdown} search-dropdown` return (
    - {this.state.results?.map(([resultId, resultLabel]) => ( + {results?.map(([resultId, resultLabel]) => (
  • {resultLabel}
  • @@ -96,8 +91,8 @@ export default class Sidebar extends React.Component { * Render courses that are in the sidebar. * @return {HTMLBodyElement} list of div's for each course that is active */ - renderActiveCourses = () => { - const temp = this.props.activeCourses ? [...this.props.activeCourses] : [] + const renderActiveCourses = () => { + const temp = activeCourses ? [...activeCourses] : [] // sort the list of rendered courses, alphabetically temp.sort((a, b) => a.localeCompare(b)) return ( @@ -108,16 +103,16 @@ export default class Sidebar extends React.Component { key={`active ${course}`} data-testid={`test ${course}`} onClick={() => { - this.props.courseClick(this.courseIdFromLabel(course)) + courseClick(courseIdFromLabel(course)) }} className="course-selection" - onMouseEnter={this.props.onHover} - onMouseLeave={this.props.onMouseLeave} + onMouseEnter={onHover} + onMouseLeave={onMouseLeave} > {course}
) @@ -126,8 +121,7 @@ export default class Sidebar extends React.Component { ) } - render() { - const collapsedClass = this.state.collapsed ? "collapsed" : "expanded" + const collapsedClass = collapsed ? "collapsed" : "expanded" const masterSidebarClass = `${collapsedClass} sidebar` return ( @@ -136,7 +130,7 @@ export default class Sidebar extends React.Component { data-testid="test-toggle" onWheel={e => e.stopPropagation()} > - {this.renderFCE()} + {renderFCE()}
- {this.renderDropdown()} + {renderDropdown()}

Selected courses

- {this.renderActiveCourses()} + {renderActiveCourses()}
this.toggleSidebar()} + onClick={() => toggleSidebar()} data-testid="test-sidebar-button" > @@ -174,7 +168,6 @@ export default class Sidebar extends React.Component {
) } -} Sidebar.propTypes = { fceCount: PropTypes.number, From a3876fce71c90cf21cab2f6b4f098471f5860521 Mon Sep 17 00:00:00 2001 From: japleenkaur Date: Sat, 15 Nov 2025 22:53:25 -0500 Subject: [PATCH 3/8] Updated CHANGELOG.md file. --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 918b8d3ee..6352f075d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,7 @@ - Added test cases for the `ExportModal` component in `js/components/common` - Updated backend tests to use `tasty-discover` - Added documentation for running a subset of the backend tests +- Migrate graph-related components (FocusBar, FocusTab, GraphDropdown, GraphFallback, Infobox, and Sidebar) from classes to functions ## [0.7.1] - 2025-06-16 From 50c911d59ab3e90ee944e4dd343d6771139d424d Mon Sep 17 00:00:00 2001 From: japleenkaur Date: Sun, 14 Dec 2025 20:32:05 -0500 Subject: [PATCH 4/8] refactor graphdown --- CHANGELOG.md | 1 + js/components/common/NavBar.js.jsx | 38 +++++++++- js/components/graph/Container.js | 2 +- js/components/graph/Graph.js | 62 ++--------------- js/components/graph/GraphDropdown.js | 69 ++++++++----------- .../__snapshots__/GraphDropdown.test.js.snap | 1 - style/app.scss | 8 +++ 7 files changed, 79 insertions(+), 102 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2cb9309a4..cd3045519 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,6 +48,7 @@ - Switched CircleCI ImageMagick download to use http - Modified CI config to take advantage of partial dependency caching and exploit parallelism when resolving/updating dependencies - Migrate graph-related components (FocusBar, FocusTab, GraphDropdown, GraphFallback, Infobox, and Sidebar) from classes to functions +- Refactor GraphDropdown component from being a child of Graph to being a child of NavBar ## [0.7.1] - 2025-06-16 diff --git a/js/components/common/NavBar.js.jsx b/js/components/common/NavBar.js.jsx index e046c7976..039060c5c 100644 --- a/js/components/common/NavBar.js.jsx +++ b/js/components/common/NavBar.js.jsx @@ -2,12 +2,32 @@ import React from "react" import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" import { faDownload } from "@fortawesome/free-solid-svg-icons" import { Tooltip } from "react-tooltip" +import GraphDropdown from "../graph/GraphDropdown" /** * NavBar component. */ -export function NavBar({ selected_page, open_modal }) { +export function NavBar({ selected_page, open_modal, graphs = [], updateGraph}) { const isActive = page => (page === selected_page ? "selected-page" : undefined) + const [showGraphDropdown, setShowGraphDropdown] = React.useState(false) + const [dropdownTimeouts, setDropdownTimeouts] = React.useState([]) + + const clearDropdownTimeouts = () => { + dropdownTimeouts.forEach(timeout => clearTimeout(timeout)) + setDropdownTimeouts([]) + } + + const handleShowGraphDropdown = () => { + clearDropdownTimeouts() + setShowGraphDropdown(true) + } + + const handleHideGraphDropdown = () => { + const timeout = setTimeout(() => { + setShowGraphDropdown(false) + }, 500) + setDropdownTimeouts(dropdownTimeouts.concat(timeout)) + } return (