diff --git a/backend/degree/serializers.py b/backend/degree/serializers.py
index 5ff2aa1c5..34139300b 100644
--- a/backend/degree/serializers.py
+++ b/backend/degree/serializers.py
@@ -116,9 +116,9 @@ class DegreePlanDetailSerializer(serializers.ModelSerializer):
)
degrees = DegreeDetailSerializer(read_only=True, many=True)
degree_ids = serializers.PrimaryKeyRelatedField(
+ many=True,
required=False,
- write_only=True,
- source="degree",
+ source="degrees",
queryset=Degree.objects.all(),
help_text="The degree_id this degree plan belongs to.",
)
diff --git a/frontend/degree-plan/components/FourYearPlan/CoursePlanned.tsx b/frontend/degree-plan/components/FourYearPlan/CoursePlanned.tsx
index e87220f40..4a13e42d9 100644
--- a/frontend/degree-plan/components/FourYearPlan/CoursePlanned.tsx
+++ b/frontend/degree-plan/components/FourYearPlan/CoursePlanned.tsx
@@ -55,7 +55,7 @@ const CoursePlanned = ({course, semesterIndex, removeCourse, highlightReqId, set
showCourseDetail(course)}>
{course.id}
- removeCourse(course)}>
+ removeCourse(course)}>
diff --git a/frontend/degree-plan/components/FourYearPlan/CoursesPlanned.tsx b/frontend/degree-plan/components/FourYearPlan/CoursesPlanned.tsx
index d8116a247..ea2e0e217 100644
--- a/frontend/degree-plan/components/FourYearPlan/CoursesPlanned.tsx
+++ b/frontend/degree-plan/components/FourYearPlan/CoursesPlanned.tsx
@@ -1,23 +1,35 @@
-import { useEffect, useState } from "react";
+import { Ref, useEffect, useState } from "react";
import CoursePlanned, { PlannedCourseContainer } from "./CoursePlanned";
import styled from "@emotion/styled";
const PlannedCoursesContainer = styled.div`
flex-grow: 1;
+ display: flex;
+ flex-direction: column;
+ gap: .5rem;
`;
-const CoursesPlanned = ({courses, semesterIndex, removeCourse, showCourseDetail, highlightReqId, className}: any) => {
+interface CoursesPlannedProps {
+ courses: any;
+ semesterIndex: number;
+ removeCourse: any;
+ showCourseDetail: any;
+ highlightReqId: any; // TODO: should not be anys
+ className: string;
+ dropRef: Ref;
+}
+
+const CoursesPlanned = ({courses, semesterIndex, removeCourse, showCourseDetail, highlightReqId, className, dropRef}: any) => {
const [courseOpen, setCourseOpen] = useState(false);
return (
- {courses.length === 0 ?
-
- : courses.map((course: any) =>
-
+ {courses.map((course: any) =>
+
)}
+
)
}
diff --git a/frontend/degree-plan/components/FourYearPlan/PlanPanel.tsx b/frontend/degree-plan/components/FourYearPlan/PlanPanel.tsx
index a117ca44f..06178bce3 100644
--- a/frontend/degree-plan/components/FourYearPlan/PlanPanel.tsx
+++ b/frontend/degree-plan/components/FourYearPlan/PlanPanel.tsx
@@ -29,7 +29,7 @@ const ShowStatsButton = ({ showStats, setShowStats }: { showStats: boolean, setS
)
}
-const PlanPanelHeader = styled.div`
+export const PanelHeader = styled.div`
display: flex;
justify-content: space-between;
background-color:'#DBE2F5';
@@ -38,13 +38,13 @@ const PlanPanelHeader = styled.div`
flex-grow: 0;
`;
-const OverflowSemesters = styled(Semesters)`
- overflow-y: scroll;
+export const PanelBody = styled.div`
+ overflow-y: auto;
flex-grow: 1;
padding: 1rem;
`;
-const PlanPanelContainer = styled.div`
+export const PanelContainer = styled.div`
display: flex;
flex-direction: column;
height: 100%;
@@ -71,6 +71,7 @@ const ModalInteriorWrapper = styled.div<{ $row?: boolean }>`
align-items: center;
padding: 1rem;
gap: .5rem;
+ text-align: center;
`;
const ModalInput = styled.input`
@@ -131,7 +132,10 @@ const ModalInterior = ({ modalObject, modalKey, close, create, rename, remove }:
return (
Are you sure you want to remove this degree plan?
- remove(modalObject.id)}>Remove
+ {
+ remove(modalObject.id)
+ close();
+ }}>Remove
);
}
@@ -199,7 +203,7 @@ const PlanPanel = ({ setActiveDegreeplanId, activeDegreeplan, degreeplans, isLoa
return (
<>
-
+
{modalKey && setModalKey(null)}
@@ -217,7 +221,7 @@ const PlanPanel = ({ setActiveDegreeplanId, activeDegreeplan, degreeplans, isLoa
/>
}
-
+
-
+
{/** map to semesters */}
-
-
+
+
+
+
>
);
}
diff --git a/frontend/degree-plan/components/FourYearPlan/SelectListDropdown.tsx b/frontend/degree-plan/components/FourYearPlan/SelectListDropdown.tsx
index b0595e805..d5f0c76f3 100644
--- a/frontend/degree-plan/components/FourYearPlan/SelectListDropdown.tsx
+++ b/frontend/degree-plan/components/FourYearPlan/SelectListDropdown.tsx
@@ -115,9 +115,9 @@ interface DropdownButton {
onClick: () => void;
makeActive: () => void;
mutators: {
- copy: () => void;
- remove: (() => void);
- rename: (() => void);
+ copy?: () => void;
+ remove?: (() => void);
+ rename?: (() => void);
};
}
@@ -247,9 +247,9 @@ interface SelectListDropdownProps {
selectItem: (id: T["id"]) => void;
getItemName: (item: T) => string;
mutators: {
- copy: (item: T) => void;
- remove: (item: T) => void;
- rename: (item: T) => void;
+ copy?: (item: T) => void;
+ remove?: (item: T) => void;
+ rename?: (item: T) => void;
create: () => void;
};
}
@@ -318,9 +318,9 @@ const SelectListDropdown = ({
onClick={() => selectItem(data.id)}
text={getItemName(data)}
mutators={{
- copy: () => copy(data),
- remove: () => remove(data),
- rename: () => rename(data)
+ copy: copy && (() => copy(data)),
+ remove: remove && (() => remove(data)),
+ rename: rename && (() => rename(data))
}}
/>
);
diff --git a/frontend/degree-plan/components/FourYearPlan/Semester.tsx b/frontend/degree-plan/components/FourYearPlan/Semester.tsx
index 94c1dd0d0..1ce0a3655 100644
--- a/frontend/degree-plan/components/FourYearPlan/Semester.tsx
+++ b/frontend/degree-plan/components/FourYearPlan/Semester.tsx
@@ -70,7 +70,7 @@ const Semester = ({semester, addCourse, index, highlightReqId, removeCourseFromS
{semester.name}
-
+
{showStats && }
diff --git a/frontend/degree-plan/components/Requirements/Major.tsx b/frontend/degree-plan/components/Requirements/Major.tsx
index 5149345c0..2eb831f8d 100644
--- a/frontend/degree-plan/components/Requirements/Major.tsx
+++ b/frontend/degree-plan/components/Requirements/Major.tsx
@@ -1,4 +1,4 @@
-import Requirement from "./Requirement"
+import Rule from "./Requirement"
import Icon from '@mdi/react';
import { mdiTrashCanOutline } from '@mdi/js';
import { mdiReorderHorizontal } from '@mdi/js';
@@ -34,7 +34,7 @@ const Major = ({major, setSearchClosed}: any) => {
{!collapsed &&
- {major.requirements.map((requirement: any) => ( ))}
+ {major.requirements.map((requirement: any) => ( ))}
}
)
diff --git a/frontend/degree-plan/components/Requirements/QObj.tsx b/frontend/degree-plan/components/Requirements/QObj.tsx
index 6b26dacbb..6b81bfd54 100644
--- a/frontend/degree-plan/components/Requirements/QObj.tsx
+++ b/frontend/degree-plan/components/Requirements/QObj.tsx
@@ -121,7 +121,7 @@ const RootQObj = ({query, reqId}) => {
break;
case 'attributes__code__in':
const attrs = stripChar(value, '[', ']');
- queryComponent = `attribute must be one of`;
+ queryComponent = `attribute must be one of `;
// for (const attr in attrs) {
queryComponent += stripChar(attrs, '\'');
// }
diff --git a/frontend/degree-plan/components/Requirements/ReqPanel.tsx b/frontend/degree-plan/components/Requirements/ReqPanel.tsx
index 15b07d597..699ee4f7d 100644
--- a/frontend/degree-plan/components/Requirements/ReqPanel.tsx
+++ b/frontend/degree-plan/components/Requirements/ReqPanel.tsx
@@ -1,57 +1,99 @@
-import Icon from '@mdi/react';
-import { mdiNoteEditOutline, mdiArrowLeft, mdiPlus } from '@mdi/js';
import { useEffect, useState } from 'react';
-import { PanelTopBar } from '@/pages/FourYearPlanPage';
import SelectListDropdown from '../FourYearPlan/SelectListDropdown';
-import SwitchFromList from '../FourYearPlan/SwitchFromList';
-import Requirement from './Requirement';
-import axios from 'axios';
+import Rule from './Requirement';
+import { Degree, DegreePlan } from '@/types';
+import styled from '@emotion/styled';
+import { PanelBody, PanelContainer, PanelHeader } from '@/components/FourYearPlan/PlanPanel'
+import { useSWRCrud } from '@/hooks/swrcrud';
+import { set, update } from 'lodash';
+import { useSWRConfig } from 'swr';
const requirementDropdownListStyle = {
maxHeight: '90%',
width: '100%',
- overflowY: 'scroll',
+ overflowY: 'auto',
paddingRight: '15px',
paddingLeft: '15px',
marginTop: '10px'
}
-
- const ReqPanel = ({majors, currentMajor, highlightReqId, setCurrentMajor, setMajors, setSearchClosed, setDegreeModalOpen, handleSearch, setHighlightReqId}:any) => {
- const [editMode, setEditMode] = useState(false);
- const [majorData, setMajorData] = useState({});
- useEffect(() => {
- const getMajor = async () => {
- // const res = await axios.get(`/degree/degrees/${currentMajor.id}`);
- // console.log(res.data);
- // setMajorData(res.data);
- // return;
- }
- if (currentMajor.id) getMajor();
- }, [currentMajor])
+const EmptyPanelContainer = styled.div`
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ height: 100%;
+ padding: 2rem;
+ text-align: center;
+`;
+
+const EmptyPanelImage = styled.img`
+ max-width: min(60%, 40vw);
+`;
+
+const EmptyPanel = () => {
+ return (
+
+
+ There's nothing here so far... add a degree to get started!
+
+ )
+}
+
+interface ReqPanelProps {
+ activeDegreePlan: DegreePlan | null;
+ isLoading: boolean;
+}
+const ReqPanel = ({activeDegreePlan, isLoading, highlightReqId, setSearchClosed, handleSearch, setHighlightReqId}: ReqPanelProps) => {
+ const degrees = activeDegreePlan?.degrees;
+ const [activeDegreeId, setActiveDegreeId] = useState(undefined);
+ const [activeDegree, setActiveDegree] = useState(undefined);
+ useEffect(() => {
+ if (!activeDegreeId && degrees?.length) {
+ setActiveDegreeId(degrees[0].id);
+ }
+ }, [activeDegreeId, activeDegreePlan])
+
+ useEffect(() => {
+ if (activeDegreeId && degrees) {
+ setActiveDegree(degrees.find(degree => degree.id === activeDegreeId));
+ }
+ })
- return(
- <>
-
- {},
- remove: () => {},
- rename: () => {},
- create: () => {}
- }}
- />
-
-
- {majorData && majorData.rules && majorData.rules.map((requirement: any) => (
-
- ))}
-
- >
- );
+ const { update: updateDegreeplan } = useSWRCrud('/api/degree/degreeplans/');
+
+ return(
+
+
+ setActiveDegreeId(id)}
+ getItemName={(degree: Degree) => `${degree.major}, ${degree.degree}`}
+ itemType={"major or degree"}
+ mutators={{
+ remove: (degree) => {
+ updateDegreeplan(
+ {degree_ids: activeDegreePlan?.degree_ids?.filter(id => id !== degree.id) || []},
+ activeDegreePlan?.id
+ )
+ if (degree.id === activeDegreeId) setActiveDegreeId(undefined);
+ },
+ create: () => updateDegreeplan(
+ {degree_ids: [...(activeDegreePlan?.degree_ids || []), 1867]}, // TODO: this is a placeholder, we need to add a new degree
+ activeDegreePlan?.id
+ )?.then(() => setActiveDegreeId(1867)),
+ }}
+ />
+
+
+ {activeDegree?.rules?.map((rule: any) => (
+
+ ))
+ ||
+ }
+
+
+ );
}
export default ReqPanel;
\ No newline at end of file
diff --git a/frontend/degree-plan/components/Requirements/Requirement.tsx b/frontend/degree-plan/components/Requirements/Requirement.tsx
index 373fbedd4..44572b994 100644
--- a/frontend/degree-plan/components/Requirements/Requirement.tsx
+++ b/frontend/degree-plan/components/Requirements/Requirement.tsx
@@ -1,79 +1,87 @@
-// interface IRequirement {
-// req: [ICourse]
-// }
-import Icon from '@mdi/react';
-import { mdiArrowDown, mdiArrowUp, mdiEye, mdiLightSwitch, mdiLightbulb, mdiLightbulbOutline, mdiMagnify, mdiMenuDown, mdiMenuUp } from '@mdi/js';
-import { titleStyle } from "@/pages/FourYearPlanPage";
-import Course from "./Course";
+import MDIcon from '@mdi/react';
+import { mdiEye, mdiMagnify, mdiMenuDown, mdiMenuUp } from '@mdi/js';
import { useState } from 'react';
import RootQObj , { trimQuery } from './QObj';
+import { Rule } from '@/types';
+import styled from '@emotion/styled';
+import { Icon } from '../bulma_derived_components';
-/* Recursive component */
-const Requirement = ({requirement, setSearchClosed, parent, handleSearch, setHighlightReqId, highlightReqId} : any) => {
- const [collapsed, setCollapsed] = useState(true);
+const RuleTitle = styled.div`
+ font-size: 1rem;
+ font-weight: 500;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ width: 100%;
+ background-color: #F5F5F5;
+ color: #575757;
+ padding: 0.25rem .5rem;
+ margin: 0.5rem 0;
+ border-radius: .5rem;
+`
+
+const CourseRequirementWrapper = styled.div`
+ margin: .5rem 0;
+ display: flex;
+ justify-content: space-between;
+`
+
+interface RuleProps {
+ rule: Rule;
+ setSearchClosed: any;
+ handleSearch: any;
+ setHighlightReqId: any;
+ highlightReqId: any;
+}
+
+/**
+ * Recursive component to represent a rule.
+ * @returns
+ */
+const Rule = ({ rule, setSearchClosed, handleSearch, setHighlightReqId, highlightReqId } : RuleProps) => {
+ const [collapsed, setCollapsed] = useState(false);
const handleShowSatisfyingCourses = () => {
console.log(highlightReqId);
- if (highlightReqId === requirement.id)
+ if (highlightReqId === rule.id)
setHighlightReqId(-1);
else
- setHighlightReqId(requirement.id);
+ setHighlightReqId(rule.id);
}
return (
- <>
- {parent === requirement.parent &&
- (requirement.q || requirement.title) &&
-
-
- {requirement.q ?
-
- :
-
- }
- {!collapsed &&
- {requirement.rules.map((rule: any, index: number) =>
-
- )}
-
}
-
}
- >
+ <>
+ {rule.q ?
+
+
+
+
+
+
+
{setSearchClosed(false); handleSearch(rule.id);}}>
+
+
+
+
+ :
+ setCollapsed(!collapsed)}>
+ {rule.title}
+ {rule.rules.length &&
+
+
+
+ }
+
+ }
+
+ {!collapsed &&
+ {rule.rules.map((rule: any, index: number) =>
+
+ )}
+
+ }
+ >
)
}
-export default Requirement;
\ No newline at end of file
+export default Rule;
\ No newline at end of file
diff --git a/frontend/degree-plan/pages/FourYearPlanPage.tsx b/frontend/degree-plan/pages/FourYearPlanPage.tsx
index 8ac57ec61..348f39475 100644
--- a/frontend/degree-plan/pages/FourYearPlanPage.tsx
+++ b/frontend/degree-plan/pages/FourYearPlanPage.tsx
@@ -151,7 +151,7 @@ const FourYearPlanPage = () => {
-
+
diff --git a/frontend/degree-plan/public/images/empty-state-cal.svg b/frontend/degree-plan/public/images/empty-state-cal.svg
new file mode 100644
index 000000000..bb25c87e8
--- /dev/null
+++ b/frontend/degree-plan/public/images/empty-state-cal.svg
@@ -0,0 +1,49 @@
+
+
+
diff --git a/frontend/degree-plan/types.ts b/frontend/degree-plan/types.ts
index 48b8721d8..ffb76be0f 100644
--- a/frontend/degree-plan/types.ts
+++ b/frontend/degree-plan/types.ts
@@ -2,6 +2,17 @@ export interface DBObject {
id: any;
}
+export interface Rule extends DBObject {
+ id: number;
+ q: string; // could be blank
+ title: string; // could be blank
+ credits: number | null;
+ parent: Rule["id"],
+ num: number | null;
+ concentration: string;
+ rules: Rule[];
+}
+
export interface Degree extends DBObject {
id: number;
year: number;
@@ -9,11 +20,13 @@ export interface Degree extends DBObject {
degree: string;
major: string;
concentration: string;
+ rules: Rule[];
}
export interface DegreePlan extends DBObject {
id: number;
- degree: Degree;
+ degrees: Degree[];
+ degree_ids: number[]; // the ids of the degrees in the degree plan, which we use to mutate the degree plan
name: string;
updated_at: string;
created_at: string;