Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

### 🔧 Internal changes

- Extracted read-only selection, prerequisite, and status computations (`isSelected`, `arePrereqsSatisfied*`, `compute*Status`, `totalSelectedCredits`) from `Graph.js` into a typed `graphState.ts` module
- Fixed lint-staged pre-commit hook running Prettier and ESLint over the whole repository instead of only staged files, and added `pnpm-lock.yaml` to `.prettierignore`
- Extracted the root `<svg>` setup (viewBox calculation, shared filters/markers) of `Graph.js` into a typed `GraphCanvas.tsx` component
- Refactored `react_modal.js.jsx` into modular functional TypeScript components (`CampusMap.tsx`, `CourseModal.tsx`, `DayBox.tsx`, `Description.tsx`, `FocusModal.tsx`, `MapModal.tsx`, `MapSidebar.tsx`), added shared `types.ts`, and removed unused dead code
Expand Down
189 changes: 14 additions & 175 deletions js/components/graph/Graph.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,15 @@ import Button from "./Button"
import InfoBox from "./InfoBox"
import GraphCanvas, { NODE_DROPSHADOW_FILTER_ID } from "./GraphCanvas"
import Sidebar from "./Sidebar"
import { parseAnd } from "./requirementParser"
import { findRelationship, numCredits, transformPoint, formatTransform } from "./util"
import { numCredits, transformPoint, formatTransform } from "./util"
import { loadGraphData } from "./loadGraphData"
import {
isSelected,
computeNodeStatus,
computeBoolStatus,
computeEdgeStatus,
totalSelectedCredits,
} from "./graphState"
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
import {
faMagnifyingGlassMinus,
Expand Down Expand Up @@ -136,17 +142,8 @@ export class Graph extends React.Component {

componentDidUpdate(prevProps, prevState) {
if (prevState.nodesJSON !== this.state.nodesJSON) {
let totalFCEs = 0
Object.values(this.state.nodesJSON).forEach(nodeJSON => {
if (
!this.state.hybridsJSON[nodeJSON.id_] &&
this.state.nodesStatus[nodeJSON.id_].selected
) {
totalFCEs += numCredits(nodeJSON.id_)
}
})
if (this.props.setFCECount) {
this.props.setFCECount(totalFCEs)
this.props.setFCECount(totalSelectedCredits(this.state))
}
}

Expand Down Expand Up @@ -178,22 +175,8 @@ export class Graph extends React.Component {
* Update the status of the Edge, based on the status of the Node/Bool it points from/to.
*/
updateEdgeStatus = (status, edgeID, source, target) => {
const isSourceSelected = this.isSelected(source)
const isTargetSelected = this.isSelected(target)
const targetStatus = this.state.nodesStatus[target]
? this.state.nodesStatus[target].status
: this.state.boolsStatus[target]

if (!status) {
if (!isSourceSelected && targetStatus === "missing") {
status = "missing"
} else if (!isSourceSelected) {
status = "inactive"
} else if (!isTargetSelected) {
status = "takeable"
} else {
status = "active"
}
status = computeEdgeStatus(this.state, source, target)
}
this.setState(state => {
const edgesStatus = { ...state.edgesStatus }
Expand Down Expand Up @@ -691,7 +674,7 @@ export class Graph extends React.Component {
* Update the Bool's state at any moment given the prereqs and current state.
*/
updateNodeBool = boolId => {
const newState = this.arePrereqsSatisfiedBool(boolId) ? "active" : "inactive"
const newState = computeBoolStatus(this.state, boolId)
const childs = this.state.connections.children[boolId]
const inEdges = this.state.connections.inEdges[boolId]
const outEdges = this.state.connections.outEdges[boolId]
Expand Down Expand Up @@ -728,32 +711,7 @@ export class Graph extends React.Component {
* @param {boolean} recursive whether we should recurse on its children
*/
updateNode = (nodeId, recursive) => {
let newState
if (this.state.hybridsJSON[nodeId]) {
// For a hybrid node, set the state to 'active' or 'inactive' depending on if its text is satisfied
// as a prerequisite string
if (this.arePrereqsSatisfiedHybrid(nodeId)) {
newState = "active"
} else {
newState = "inactive"
}
} else {
// For a regular course node, set the state to 'active', 'takeable', 'inactive', or 'overridden'
// depending on whether the user has selected the course and whether the course's prereqs are met
if (this.arePrereqsSatisfiedNode(nodeId)) {
if (this.isSelected(nodeId)) {
newState = "active"
} else {
newState = "takeable"
}
} else {
if (this.isSelected(nodeId)) {
newState = "overridden"
} else {
newState = "inactive"
}
}
}
const newState = computeNodeStatus(this.state, nodeId)

const childs = this.state.connections.children[nodeId]
const status =
Expand Down Expand Up @@ -890,7 +848,7 @@ export class Graph extends React.Component {
() => {
inEdges?.forEach(edge => {
const currentEdge = this.state.edgesJSON[edge]
if (!this.isSelected(currentEdge.source)) {
if (!isSelected(this.state, currentEdge.source)) {
this.updateEdgeStatus(
"missing",
currentEdge.id_,
Expand Down Expand Up @@ -940,7 +898,7 @@ export class Graph extends React.Component {
() => {
inEdges?.forEach(edge => {
const currentEdge = this.state.edgesJSON[edge]
if (!this.isSelected(currentEdge.source)) {
if (!isSelected(this.state, currentEdge.source)) {
this.updateEdgeStatus(
"missing",
currentEdge.id_,
Expand Down Expand Up @@ -1002,125 +960,6 @@ export class Graph extends React.Component {
})
}

/**
* Checks if a node or bool node is currently selected or active.
* @param {string} nodeId
* @returns {boolean}
*/
isSelected = nodeId => {
if (this.state.nodesStatus[nodeId]) {
return this.isSelectedNode(nodeId)
} else {
return this.isSelectedBool(nodeId)
}
}

/**
* Checks whether this Node is selected
* @return {boolean}
*/
isSelectedNode = nodeId => {
if (this.state.nodesJSON[nodeId]) {
return this.state.nodesStatus[nodeId].selected
} else {
return this.state.nodesStatus[nodeId].status === "active"
}
}

/**
* Check whether the Bool is selected.
* @returns {boolean} Whether status is active or not.
*/
isSelectedBool = boolId => {
return this.state.boolsStatus[boolId] === "active"
}

/**
* Check if the prerequisite courses have been satisfied based on bool type.
* @param {string} boolId the ID of the bool node.
* @returns {boolean} Whether the bool is satisfied based on its prerequisite nodes.
*/
arePrereqsSatisfiedBool = boolId => {
const isAllTrue = element => {
return this.state.nodesStatus[element]
? this.isSelected(element)
: this.isSelected(element)
}

if (this.state.boolsJSON[boolId].text[0].text === "and") {
return this.state.connections.parents[boolId].every(isAllTrue)
} else if (this.state.boolsJSON[boolId].text[0].text === "or") {
return this.state.connections.parents[boolId].some(isAllTrue)
}
}

/**
* Recursively checks whether all prerequisite/preceding nodes for a node are satisfied.
* @param {string} nodeId the ID of the node.
* @return {boolean} whether all of the node's prerequisite nodes are satisfied by the selected courses.
*/
arePrereqsSatisfiedNode = nodeId => {
const parents = this.state.connections.parents[nodeId]
/**
* Recursively checks that preceding nodes are selected
* @param {string|Array} element Node(s)/other on the graph
* @return {boolean}
*/
const isAllTrue = element => {
if (typeof element === "string") {
if (this.state.nodesStatus[element] || this.state.boolsJSON[element]) {
return this.isSelected(element)
} else {
return false
}
} else {
return element.some(isAllTrue)
}
}

return parents.every(isAllTrue)
}

/**
* Checks whether a hybrid node's prereq string is satisfied.
* @param {string} nodeId the ID of the hybrid node.
* @return {boolean} whether the node's text prerequisite string is satisfied by the selected courses.
*/
arePrereqsSatisfiedHybrid = nodeId => {
// Concatenate prereq string
let hybridNode = this.state.hybridsJSON[nodeId]
let hybridText = hybridNode.text.map(textTag => textTag.text).join("")

// Parse prereq string into a nested list alternating between AND and OR conditions
let prereqList = parseAnd(hybridText)

// Recursively check if each prerequisite condition is satisfied within the selected courses
let nodesList = Object.values(this.state.nodesJSON)
const isSelectedCourse = course => {
let prereqNode = findRelationship(course, nodesList)
if (prereqNode !== undefined) {
return this.isSelectedNode(prereqNode.id_)
} else {
return false
}
}
const andSatisfied = andList => {
if (typeof andList === "string") {
return isSelectedCourse(andList)
} else {
return andList.every(orSatisfied)
}
}
const orSatisfied = orList => {
if (typeof orList === "string") {
return isSelectedCourse(orList)
} else {
return orList.some(andSatisfied)
}
}
return andSatisfied(prereqList)
}

/**
* Renders a group of Bools
* @param {JSON} boolsJSON
Expand Down
Loading
Loading