Skip to content

Commit

Permalink
fix: CF-421: adding or removing courses now reflect in the graphical …
Browse files Browse the repository at this point in the history
…selector (#1037)

* feat: dark mode functionality added for editMarkModal's input and button elements

* feat: dark mode improvement for editMarkModal's cancel button

* feat: dark mode improvements for OptionHeader icons underneath the 'Term Planner' tab

* feat: dark mode added for the select menu in the settingsMenu tooltip under the TermPlanner tab

* feat: dark mode added to SettingMenu's DatePicker element

* feat: dark mode for export button done + editModalMark bug fixed

* fix: making sure the css for the select element in settingsMenu does not affect other select elements

* feat: dark mode improvement for popconfirm for unplan wanring, import tooltip, also moved the button for import and export tooltip to common styles

* feat: dark mode scrollbar added

* feat: dark mode scrollbar on courseSelector menu had ugly white padding, fixed it to be #333 color instead

* fix: href in courseSelector not very readable in dark mode, made the text a bit brighter and made a styled component for it

* feat: dark mode added for search bar

* feat: dark mode added for remove planner button

* fix: forgot to add the new styles.ts file

* feat: progressBar's text color and trailing color fixed

* feat: dividing line in courseDescription changed from white to a dark grey

* feat: bug icon turned into dark mode

* feat: dark mode added for quick add and remove buttons in course menu

* feat: courseProgression progress bar trailing color changed to dark grey

* feat: progress on dark mode for graph, need to save this commit before I merge in the latest changes since the graph was changed

* feat: dark mode for graph complete (nodes, arrows, hover states) + label now changes on hover (non-dark mode feature)

* feat: buttons on graphical selector are dark mode

* feat: saving progress on converting courseDescription panel to dark mode

* feat: dark mode added to the sidebar

* feat: sidebardrawer color changed, box shadow added to tabs so it looks more visible in dark mode

* feat: new images added in help menu in course selector, dark mode versions added too

* feat: TermPlanner's help menu tooltips now have dark mode pics and gifs

* feat: highlight adjacent nodes and edges on hover

* feat: highlight adjacent nodes opacity updated

* refactor: graph.ts, changing function names and object names to be more readable:

* feat: implemented a function that checks if a course is a prereq based on GraphData without calling the backend

* fix: two graphs get rendered if you switch tabs fast enough

* feat: created a function to store a hashmap of prereqs for later use for node styling

* fix: updated the function that checks for coursePrerequisite

Other options are:
1. Rely on GraphData, however it gets laggy when you hover over too many nodes
2. API call - would get slow with multiple requests
3. CourseEdge info gets stored into a hashmap at initialisation, so checking for prereqs is fast afterwards

* refactor: graph.ts function and object names made more readable

* refactor: rewriting the returns and using spread operator to reduce repetition in graph.ts

* feat: highlight prerequisite nodes on hover

* refactor: splitting functions up as they were getting too long

* fix: if the dark mode button is toggled on and off, it repaints the canvas more than one time

* feat: highlighted incoming edge if it's a prerequisite as well

* feat: forgot to add pics into the HelpMenu for the new graphical selector hover node feature

* feat: unlocked course nodes are now distinct from planned and locked courses

feat: HelpMenu pictures were also updated

* fix: adding and removing courses in graphical selector, updates the node in the graph without re-render

refactor: graph.ts duplicate function removed

* fix: endArrow opacity of graph was not changing on hover

* fix: instead of using courseSlicer, used axios request to update unlocked nodes after adding to planner

---------

Co-authored-by: Daysure <[email protected]>
  • Loading branch information
Dasyure and Dasyure authored Jul 21, 2023
1 parent 0c4fedd commit df01a76
Show file tree
Hide file tree
Showing 7 changed files with 152 additions and 90 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,15 @@ type CourseDescriptionPanelProps = {
courseCode: string;
onCourseClick?: (code: string) => void;
courseDescInfoCache: React.MutableRefObject<CourseDescInfoResCache>;
hasPlannerUpdated: React.MutableRefObject<boolean>;
};

const CourseDescriptionPanel = ({
className,
courseCode,
onCourseClick,
courseDescInfoCache
courseDescInfoCache,
hasPlannerUpdated
}: CourseDescriptionPanelProps) => {
const { degree, planner } = useSelector((state: RootState) => state);

Expand Down Expand Up @@ -127,7 +129,7 @@ const CourseDescriptionPanel = ({
{courseCode} - {course.title}
</Title>
</div>
<PlannerButton course={course} />
<PlannerButton course={course} hasPlannerUpdated={hasPlannerUpdated} />
</S.TitleWrapper>
{/* TODO: Style this better? */}
{course.is_legacy && (
Expand Down
5 changes: 4 additions & 1 deletion frontend/src/components/PlannerButton/PlannerButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@ import S from './styles';

interface PlannerButtonProps {
course: Course;
hasPlannerUpdated: React.MutableRefObject<boolean>;
}

const PlannerButton = ({ course }: PlannerButtonProps) => {
const PlannerButton = ({ course, hasPlannerUpdated }: PlannerButtonProps) => {
const coursesInPlanner = useSelector((state: RootState) => state.planner.courses);
const { degree, planner } = useSelector((state: RootState) => state);

Expand Down Expand Up @@ -56,6 +57,7 @@ const PlannerButton = ({ course }: PlannerButtonProps) => {
};
dispatch(addToUnplanned({ courseCode: course.code, courseData }));
addCourseToPlannerTimeout(true);
hasPlannerUpdated.current = true;
}
};

Expand All @@ -67,6 +69,7 @@ const PlannerButton = ({ course }: PlannerButtonProps) => {
);
addCourseToPlannerTimeout(false);
dispatch(removeCourses(res.data.courses));
hasPlannerUpdated.current = true;
} catch (e) {
// eslint-disable-next-line no-console
console.error('Error at removeFromPlanner', e);
Expand Down
1 change: 0 additions & 1 deletion frontend/src/pages/CourseSelector/CourseMenu/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ const SidebarWrapper = styled.div`
overflow: auto;
overflow-x: hidden;
height: 100%;
width: 100%;
border-right: 1px solid ${({ theme }) => theme.courseMenu?.borderColor};
`;

Expand Down
2 changes: 2 additions & 0 deletions frontend/src/pages/CourseSelector/CourseSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const CourseSelector = () => {
const { programCode, specs } = useSelector((state: RootState) => state.degree);
const { courses } = useSelector((state: RootState) => state.planner);
const { active, tabs } = useSelector((state: RootState) => state.courseTabs);
const hasPlannerUpdated = useRef<boolean>(false);

const dispatch = useDispatch();

Expand Down Expand Up @@ -100,6 +101,7 @@ const CourseSelector = () => {
courseCode={courseCode}
onCourseClick={onCourseClick}
courseDescInfoCache={courseDescInfoCache}
hasPlannerUpdated={hasPlannerUpdated}
/>
</div>
) : (
Expand Down
110 changes: 82 additions & 28 deletions frontend/src/pages/GraphicalSelector/CourseGraph/CourseGraph.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type { Graph, GraphOptions, IG6GraphEvent, INode, Item } from '@antv/g6';
import { Switch } from 'antd';
import axios from 'axios';
import { CourseEdge, CoursesAllUnlocked, GraphPayload } from 'types/api';
import { CourseValidation } from 'types/courses';
import { useDebouncedCallback } from 'use-debounce';
import prepareUserPayload from 'utils/prepareUserPayload';
import Spinner from 'components/Spinner';
Expand All @@ -19,12 +20,11 @@ import { ZOOM_IN_RATIO, ZOOM_OUT_RATIO } from '../constants';
import {
defaultEdge,
edgeInHoverStyle,
edgeOpacity,
edgeOutHoverStyle,
edgeUnhoverStyle,
mapEdgeOpacity,
mapNodeOpacity,
mapNodePrereq,
mapNodeRestore,
mapNodeStyle,
nodeLabelHoverStyle,
nodeLabelUnhoverStyle,
Expand All @@ -38,18 +38,26 @@ type Props = {
handleToggleFullscreen: () => void;
fullscreen: boolean;
focused?: string;
hasPlannerUpdated: React.MutableRefObject<boolean>;
};

interface CoursePrerequisite {
[key: string]: string[];
}

const CourseGraph = ({ onNodeClick, handleToggleFullscreen, fullscreen, focused }: Props) => {
const CourseGraph = ({
onNodeClick,
handleToggleFullscreen,
fullscreen,
focused,
hasPlannerUpdated
}: Props) => {
const { theme } = useSelector((state: RootState) => state.settings);
const previousTheme = useRef<typeof theme>(theme);
const { programCode, specs } = useSelector((state: RootState) => state.degree);
const { courses: plannedCourses } = useSelector((state: RootState) => state.planner);
const { degree, planner, courses } = useSelector((state: RootState) => state);
const { degree, planner } = useSelector((state: RootState) => state);
const allUnlocked = useRef<Record<string, CourseValidation> | undefined>({});
const windowSize = useAppWindowSize();

const graphRef = useRef<Graph | null>(null);
Expand All @@ -60,13 +68,22 @@ const CourseGraph = ({ onNodeClick, handleToggleFullscreen, fullscreen, focused

const containerRef = useRef<HTMLDivElement | null>(null);

function unwrap<T>(res: PromiseSettledResult<T>): T | undefined {
if (res.status === 'rejected') {
// eslint-disable-next-line no-console
console.error('Rejected request at unwrap', res.reason);
return undefined;
}
return res.value;
}

useEffect(() => {
const isCoursePrerequisite = (target: string, neighbour: string) => {
const prereqs = prerequisites[target] || [];
return prereqs.includes(neighbour);
};

const addAdjacentStyles = async (nodeItem: Item) => {
const addNeighbourStyles = async (nodeItem: Item) => {
const node = nodeItem as INode;
const neighbours = node.getNeighbors();
const opacity = theme === 'light' ? 0.3 : 0.4;
Expand All @@ -76,19 +93,17 @@ const CourseGraph = ({ onNodeClick, handleToggleFullscreen, fullscreen, focused
graphRef.current?.getNodes().forEach((n) => {
graphRef.current?.updateItem(n as Item, mapNodeOpacity(n.getID(), opacity));
n.getEdges().forEach((e) => {
graphRef.current?.updateItem(e, edgeOpacity(e.getID(), opacity));
graphRef.current?.updateItem(e, mapEdgeOpacity(Arrow, theme, e.getID(), opacity));
});
n.toBack();
});
// Highlight node's edges
node.getOutEdges().forEach((e) => {
graphRef.current?.updateItem(e, edgeOutHoverStyle(Arrow, theme, e.getID()));
graphRef.current?.updateItem(e, edgeOpacity(e.getID(), 1));
e.toFront();
});
node.getInEdges().forEach((e) => {
graphRef.current?.updateItem(e, edgeInHoverStyle(Arrow, theme, e.getID()));
graphRef.current?.updateItem(e, edgeOpacity(e.getID(), 1));
e.toFront();
});
// Target node and neighbouring nodes remain visible
Expand All @@ -104,7 +119,7 @@ const CourseGraph = ({ onNodeClick, handleToggleFullscreen, fullscreen, focused
});
};

const removeAdjacentStyles = async (nodeItem: Item) => {
const removeNeighbourStyles = async (nodeItem: Item) => {
const node = nodeItem as INode;
const edges = node.getEdges();
const { Arrow } = await import('@antv/g6');
Expand All @@ -116,13 +131,13 @@ const CourseGraph = ({ onNodeClick, handleToggleFullscreen, fullscreen, focused
const courseId = n.getID();
graphRef.current?.updateItem(
n as Item,
mapNodeRestore(courseId, plannedCourses, courses.courses, theme)
mapNodeStyle(courseId, plannedCourses, allUnlocked.current, theme)
);
graphRef.current?.updateItem(n as Item, mapNodeOpacity(courseId, 1));
n.toFront();
});
graphRef.current?.getEdges().forEach((e) => {
graphRef.current?.updateItem(e, edgeOpacity(e.getID(), 1));
graphRef.current?.updateItem(e, mapEdgeOpacity(Arrow, theme, e.getID(), 1));
});
};

Expand All @@ -131,7 +146,7 @@ const CourseGraph = ({ onNodeClick, handleToggleFullscreen, fullscreen, focused
const node = ev.item as Item;
graphRef.current?.setItemState(node, 'hover', true);
graphRef.current?.updateItem(node, nodeLabelHoverStyle(node.getID()));
addAdjacentStyles(node);
addNeighbourStyles(node);
graphRef.current?.paint();
};

Expand All @@ -143,11 +158,14 @@ const CourseGraph = ({ onNodeClick, handleToggleFullscreen, fullscreen, focused
node,
nodeLabelUnhoverStyle(node.getID(), plannedCourses, theme)
);
removeAdjacentStyles(node);
removeNeighbourStyles(node);
graphRef.current?.paint();
};

const initialiseGraph = async (courseCodes: string[], courseEdges: CourseEdge[]) => {
const initialiseGraph = async (
courseCodes: string[] | undefined,
courseEdges: CourseEdge[] | undefined
) => {
const container = containerRef.current;
if (!container) return;

Expand Down Expand Up @@ -183,7 +201,7 @@ const CourseGraph = ({ onNodeClick, handleToggleFullscreen, fullscreen, focused

graphRef.current = new Graph(graphArgs);
const data = {
nodes: courseCodes.map((c) => mapNodeStyle(c, plannedCourses, courses.courses, theme)),
nodes: courseCodes?.map((c) => mapNodeStyle(c, plannedCourses, allUnlocked.current, theme)),
edges: courseEdges
};

Expand All @@ -206,9 +224,9 @@ const CourseGraph = ({ onNodeClick, handleToggleFullscreen, fullscreen, focused
};

// Store a hashmap for performance reasons when highlighting nodes
const makePrerequisitesMap = (edges: CourseEdge[]) => {
const makePrerequisitesMap = (edges: CourseEdge[] | undefined) => {
const prereqs: CoursePrerequisite = prerequisites;
edges.forEach((e) => {
edges?.forEach((e) => {
if (!prereqs[e.target]) {
prereqs[e.target] = [e.source];
} else {
Expand All @@ -218,13 +236,13 @@ const CourseGraph = ({ onNodeClick, handleToggleFullscreen, fullscreen, focused
setPrerequisites(prereqs);
};

// Update styling for: each node, hovering state and edges
// Without re-render, update styling for: each node, hovering state and edges
const repaintCanvas = async () => {
const nodes = graphRef.current?.getNodes();
nodes?.map((n) =>
graphRef.current?.updateItem(
n,
mapNodeStyle(n.getID(), plannedCourses, courses.courses, theme)
mapNodeStyle(n.getID(), plannedCourses, allUnlocked.current, theme)
)
);

Expand All @@ -243,16 +261,38 @@ const CourseGraph = ({ onNodeClick, handleToggleFullscreen, fullscreen, focused
graphRef.current?.paint();
};

const getUnlocked = async () => {
try {
setLoading(true);
const res = await axios.post<CoursesAllUnlocked>(
'/courses/getAllUnlocked/',
JSON.stringify(prepareUserPayload(degree, planner))
);
allUnlocked.current = res.data.courses_state;
repaintCanvas();
setLoading(false);
} catch (err) {
// eslint-disable-next-line no-console
console.error('Error at updating allUnlocked', err);
}
};

const setupGraph = async () => {
try {
initialising.current = true;
const res = await axios.get<GraphPayload>(
`/programs/graph/${programCode}/${specs.join('+')}`
);
const { edges } = res.data;
makePrerequisitesMap(edges);
if (res.data.courses.length !== 0 && edges.length !== 0) {
initialiseGraph(res.data.courses, edges);
const res = await Promise.allSettled([
axios.get<GraphPayload>(`/programs/graph/${programCode}/${specs.join('+')}`),
axios.post<CoursesAllUnlocked>(
'/courses/getAllUnlocked/',
JSON.stringify(prepareUserPayload(degree, planner))
)
]);
const [programsRes, coursesRes] = res;
const programs = unwrap(programsRes)?.data;
allUnlocked.current = unwrap(coursesRes)?.data.courses_state;
makePrerequisitesMap(programs?.edges);
if (programs?.courses.length !== 0 && programs?.edges.length !== 0) {
initialiseGraph(programs?.courses, programs?.edges);
}
} catch (e) {
// eslint-disable-next-line no-console
Expand All @@ -261,12 +301,26 @@ const CourseGraph = ({ onNodeClick, handleToggleFullscreen, fullscreen, focused
};

if (!initialising.current) setupGraph();
// Repaint canvas when theme is changed without re-render
if (hasPlannerUpdated.current) {
hasPlannerUpdated.current = false;
getUnlocked();
}
// Change theme without re-render
if (previousTheme.current !== theme) {
previousTheme.current = theme;
repaintCanvas();
}
}, [onNodeClick, plannedCourses, programCode, specs, theme, prerequisites, courses]);
}, [
onNodeClick,
plannedCourses,
programCode,
specs,
theme,
prerequisites,
degree,
planner,
hasPlannerUpdated
]);

const showAllCourses = () => {
if (!graphRef.current) return;
Expand Down
Loading

0 comments on commit df01a76

Please sign in to comment.