Skip to content

Commit

Permalink
Merge pull request #640 from pennlabs/aug/search-panel-dnd
Browse files Browse the repository at this point in the history
add dnd support to search panel, highlight searched rule, UI adjustments
  • Loading branch information
yuntongf authored Apr 11, 2024
2 parents 7c33991 + 91d1c9c commit 2420e54
Show file tree
Hide file tree
Showing 11 changed files with 114 additions and 78 deletions.
20 changes: 8 additions & 12 deletions backend/degree/serializers.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from textwrap import dedent

from django.db.models import Q, Subquery
from django.db.models import Q
from rest_framework import serializers

from courses.models import Course
Expand Down Expand Up @@ -143,17 +143,13 @@ def validate(self, data):
for rule in rules:
# NOTE: we don't do any validation if the course doesn't exist in DB. In future,
# it may be better to prompt user for manual override
if Course.objects.filter(full_code=full_code).exists():
satisfying_courses = Course.objects.filter(rule.get_q_object())
if not (
Course.objects.filter(
full_code=full_code,
topic_id__in=Subquery(satisfying_courses.values("topic_id")),
).exists()
):
raise serializers.ValidationError(
f"Course {full_code} does not satisfy rule {rule.id}"
)
if (
Course.objects.filter(full_code=full_code).exists()
and not Course.objects.filter(rule.get_q_object(), full_code=full_code).exists()
):
raise serializers.ValidationError(
f"Course {full_code} does not satisfy rule {rule.id}"
)

# Check for double count restrictions
double_count_restrictions = DoubleCountRestriction.objects.filter(
Expand Down
14 changes: 5 additions & 9 deletions frontend/degree-plan/components/Dock/Dock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,11 @@ const DockContainer = styled.div<{$isDroppable:boolean, $isOver: boolean}>`
const SearchIconContainer = styled.div`
padding: .25rem 2rem;
padding-left: 0;
border-color: var(--primary-color-xx-dark);
color: var(--primary-color-extra-dark);
border-color: var(--primary-color-extra-dark);
border-width: 0;
border-right-width: 2px;
border-style: solid;
flex-shrink: 0;
display: flex;
gap: 1rem;
`

const DockedCoursesWrapper = styled.div`
Expand Down Expand Up @@ -109,7 +106,7 @@ const Dock = ({ user, login, logout, activeDegreeplanId }: DockProps) => {
// const [courseAdded, setCourseAdded] = React.useState(false);
const { searchPanelOpen, setSearchPanelOpen, setSearchRuleQuery, setSearchRuleId } = useContext(SearchPanelContext)
const { createOrUpdate } = useSWRCrud<DockedCourse>(`/api/degree/docked`, { idKey: 'full_code' });
const { data: dockedCourses = [], isLoading } = useSWR<DockedCourse[]>(user ? `/api/degree/docked` : null);
const {data: dockedCourses = [], isLoading} = useSWR<DockedCourse[]>(user ? `/api/degree/docked` : null);

// Returns a boolean that indiates whether this is the first render
const useIsMount = () => {
Expand All @@ -120,6 +117,8 @@ const Dock = ({ user, login, logout, activeDegreeplanId }: DockProps) => {
return isMountRef.current;
};

const isMount = useIsMount();

const [{ isOver, canDrop }, drop] = useDrop(() => ({
accept: [ItemTypes.COURSE_IN_PLAN, ItemTypes.COURSE_IN_REQ],
drop: (course: DnDCourse) => {
Expand Down Expand Up @@ -161,11 +160,8 @@ const Dock = ({ user, login, logout, activeDegreeplanId }: DockProps) => {
setSearchPanelOpen(!searchPanelOpen);
}}>
<DarkBlueIcon>
<i className="fas fa-plus fa-lg"/>
<i className="fas fa-search fa-lg"/>
</DarkBlueIcon>
<div>
Add Course
</div>
</SearchIconContainer>
<DockedCoursesWrapper>
{isLoading ?
Expand Down
49 changes: 20 additions & 29 deletions frontend/degree-plan/components/FourYearPlan/Semesters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,41 +66,22 @@ const AddButton = styled.div`
gap: 1rem;
`;

const YearInput = styled.input`
width: 9rem;
background-color: transparent;
border-color: #9FB5EF;
color: #C1C1C1;
box-shadow: none;
&:hover {
borderColor: "#9FB5EF";
}
padding: .75rem;
padding-top: .5rem;
padding-bottom: .5rem;
border-style: solid;
border-radius: .25rem;
border-width: 1px;
border-top-left-radius: 0;
border-top-right-radius: 0;
font-size: 1rem;
`

const selectStyles = (topOrBottom: boolean) => ({
control: (provided: any) => ({
...provided,
width: "9rem",
width: "130px",
backgroundColor: "transparent",
borderColor: "#9FB5EF",
color: "#C1C1C1",
boxShadow: "none",
"&:hover": {
borderColor: "#9FB5EF",
},
borderBottomLeftRadius: 0,
borderBottomRightRadius: 0,
borderBottom: 0
...(
topOrBottom ?
{ borderBottomLeftRadius: 0, borderBottomRightRadius: 0, borderBottom: 0 } :
{ borderTopLeftRadius: 0, borderTopRightRadius: 0 }
)
}),
singleValue: (provided: any) => ({
...provided,
Expand Down Expand Up @@ -145,6 +126,14 @@ const ModifySemesters = ({
{ value: "C", label: "Fall" },
];

// TODO: Un-hardcode years
const yearOptions = [
{ value: "2024", label: "2024" },
{ value: "2025", label: "2025" },
{ value: "2026", label: "2026" },
{ value: "2027", label: "2027" },
];

return (
// TODO: add a modal for this
<AddSemesterContainer className={className}>
Expand All @@ -164,10 +153,11 @@ const ModifySemesters = ({
onChange={(option) => setSelectedSeason(option ? option.value : selectedSeason)}
/>

<YearInput
value={selectedYear}
type="number"
onChange={(e) => setSelectedYear(e.target.value)}
<Select
styles={selectStyles(false)}
options={yearOptions}
value={yearOptions.find((option) => option.value === selectedYear)}
onChange={(option) => setSelectedYear(option ? option.value : selectedYear)}
/>
</AddSemesterContainer>
);
Expand Down Expand Up @@ -257,6 +247,7 @@ const Semesters = ({
useEffect(() => {
if (Object.keys(semesters).length == 0 && !isLoading) setEditMode(true);
// if finish loading and no semesters, we go to edit mode for the user to add new semesters
else setEditMode(false);
if (!activeDegreeplan) return;
if (typeof window !== "undefined" && Object.keys(semesters).length) {
localStorage.setItem(
Expand Down
11 changes: 7 additions & 4 deletions frontend/degree-plan/components/FourYearPlanPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ const PanelWrapper = styled(Pane)`
height: 100%;
display: flex;
flex-direction: row;
gap: 2rem;
gap: 0.8rem;
`

const PanelInteriorWrapper = styled.div<{ $maxWidth?: string; $minWidth?: string }>`
Expand Down Expand Up @@ -160,7 +160,10 @@ const FourYearPlanPage = ({
// @ts-ignore */}
<SplitPane
split="vertical"
maxSize={windowWidth ? windowWidth * 0.65 : 1000}
// maxSize={windowWidth ? windowWidth * 0.60 : 1000}
maxSize={searchPanelOpen ?
(windowWidth ? windowWidth : 1000) * 0.5
: (windowWidth ? windowWidth : 1000) * 0.6}
defaultSize="50%"
style={{
padding: "1.5rem",
Expand Down Expand Up @@ -198,8 +201,8 @@ const FourYearPlanPage = ({
/>
</PanelInteriorWrapper>
{searchPanelOpen && (
<PanelInteriorWrapper $minWidth={"40%"} $maxWidth={"45%"}>
<SearchPanel activeDegreeplanId={activeDegreeplan ? activeDegreeplan.id : null} />
<PanelInteriorWrapper $minWidth={"40%"} $maxWidth={"43%"}>
<SearchPanel activeDegreeplanId={activeDegreeplan ? activeDegreeplan.id : null} setSearchedRuleId={setSearchRuleId}/>
</PanelInteriorWrapper>
)}
</PanelWrapper>
Expand Down
39 changes: 32 additions & 7 deletions frontend/degree-plan/components/Requirements/QObject.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,14 +72,17 @@ const Attributes = ({ attributes }: { attributes: string[] }) => {
</AttributeWrapper>
}

const SearchConditionWrapper = styled(BaseCourseContainer)`
const SearchConditionWrapper = styled(BaseCourseContainer)<{$isSearched: boolean}>`
display: flex;
flex-wrap: wrap;
gap: .5rem;
background-color: var(--primary-color-light);
box-shadow: 0px 0px 14px 2px rgba(0, 0, 0, 0.05);
cursor: pointer;
padding: .5rem .75rem;
${props => !!props.$isSearched && `
box-shadow: 0px 0px 10px 2px var(--primary-color-dark);
`}
`

const Wrap = styled.span`
Expand Down Expand Up @@ -176,15 +179,17 @@ interface SearchConditionProps extends SearchConditionInnerProps {
ruleIsSatisfied: boolean,
ruleId: Rule["id"];
ruleQuery: string;
activeDegreeplanId: DegreePlan["id"]
activeDegreeplanId: DegreePlan["id"];
}
const SearchCondition = ({ ruleId, ruleQuery, fulfillments, ruleIsSatisfied, q, activeDegreeplanId}: SearchConditionProps) => {
const { setSearchPanelOpen, setSearchRuleQuery, setSearchRuleId, setSearchFulfillments } = useContext(SearchPanelContext);
const { setSearchPanelOpen, searchRuleId, setSearchRuleQuery, setSearchRuleId, setSearchFulfillments } = useContext(SearchPanelContext);

return (
<SearchConditionWrapper
$isDisabled={false}
$isUsed={false}
$isSearched={searchRuleId == ruleId}

>
<SearchConditionInner q={q} />
<DarkGrayIcon onClick={() => {
Expand Down Expand Up @@ -261,7 +266,7 @@ interface QObjectProps {
satisfied: boolean;
activeDegreePlanId: number;
}
const QObject = ({ q, fulfillments, rule, satisfied, activeDegreePlanId }: QObjectProps) => {
const QObject = ({ q, fulfillments, rule, satisfied, activeDegreePlanId}: QObjectProps) => {

// recursively render
switch (q.type) {
Expand Down Expand Up @@ -300,7 +305,14 @@ const QObject = ({ q, fulfillments, rule, satisfied, activeDegreePlanId }: QObje
const displaySearchConditions = searchConditions.map(search => {
const courses = Array.from(fulfillmentsMap.values())
fulfillmentsMap.clear()
return <SearchCondition fulfillments={courses} q={search.q} ruleIsSatisfied={satisfied} ruleId={rule.id} ruleQuery={rule.q} activeDegreeplanId={activeDegreePlanId}/>
return <SearchCondition
fulfillments={courses}
q={search.q}
ruleIsSatisfied={satisfied}
ruleId={rule.id}
ruleQuery={rule.q}
activeDegreeplanId={activeDegreePlanId}
/>
})

return <Row $wrap>
Expand All @@ -310,7 +322,14 @@ const QObject = ({ q, fulfillments, rule, satisfied, activeDegreePlanId }: QObje
)}
</Row>
case "SEARCH":
return <SearchCondition q={q.q} ruleIsSatisfied={satisfied} fulfillments={fulfillments} ruleId={rule.id} ruleQuery={rule.q} activeDegreeplanId={activeDegreePlanId}/>;
return <SearchCondition
q={q.q}
ruleIsSatisfied={satisfied}
fulfillments={fulfillments}
ruleId={rule.id}
ruleQuery={rule.q}
activeDegreeplanId={activeDegreePlanId}
/>;
case "COURSE":
const [fulfillment] = fulfillments.filter(fulfillment => fulfillment.full_code == q.full_code && (!q.semester || q.semester === fulfillment.semester))
return <CourseInReq course={{...q, rules: fulfillment ? fulfillment.rules : []}} fulfillment={fulfillment} isDisabled={satisfied && !fulfillment} isUsed={!!fulfillment} rule_id={rule.id} activeDegreePlanId={activeDegreePlanId}/>;
Expand Down Expand Up @@ -352,7 +371,13 @@ const RuleLeaf = ({ q_json, fulfillmentsForRule, rule, satisfied, activeDegreePl

return (
<RuleLeafWrapper $wrap>
<QObject q={q_json} fulfillments={fulfillmentsForRule} rule={rule} satisfied={satisfied} activeDegreePlanId={activeDegreePlanId} />
<QObject
q={q_json}
fulfillments={fulfillmentsForRule}
rule={rule}
satisfied={satisfied}
activeDegreePlanId={activeDegreePlanId}
/>
</RuleLeafWrapper>
)
}
Expand Down
2 changes: 1 addition & 1 deletion frontend/degree-plan/components/Requirements/ReqPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ const Degree = ({degree, rulesToFulfillments, activeDegreeplan, editMode, setMod
<DegreeBody>
{degree && degree.rules.map((rule: any) => (
<RuleComponent
{...computeRuleTree({ activeDegreePlanId: activeDegreeplan.id, rule, rulesToFulfillments })}
{...computeRuleTree({ activeDegreePlanId: activeDegreeplan.id, rule, rulesToFulfillments })}
/>
))}
</DegreeBody>}
Expand Down
20 changes: 14 additions & 6 deletions frontend/degree-plan/components/Requirements/Rule.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ const RuleTitle = styled.div`
`

const RuleLeafWrapper = styled.div<{$isDroppable:boolean, $isOver: boolean}>`
margin: .25rem;
padding: .5rem .5rem .5rem 0rem;
margin-left: 0;
display: flex;
justify-content: space-between;
Expand Down Expand Up @@ -77,7 +77,7 @@ const Indented = styled.div`
const Column = styled.div`
display: flex;
flex-direction: column;
gap: .1rem;
gap: 0rem;
`

const PickNWrapper = styled.div`
Expand All @@ -101,7 +101,8 @@ const RuleLeafLabel = styled.div`
`

const RuleLeafContainer = styled(Column)`
margin-top: 0.25rem;
margin: 0.1rem;
}
`


Expand Down Expand Up @@ -147,6 +148,7 @@ export const SkeletonRule: React.FC<React.PropsWithChildren> = ({ children }) =>
/**
* Recursive component to represent a rule.
*/

const RuleComponent = (ruleTree : RuleTree) => {
const { type, activeDegreePlanId, rule, progress } = ruleTree;
const satisfied = progress === 1;
Expand Down Expand Up @@ -181,7 +183,13 @@ const RuleComponent = (ruleTree : RuleTree) => {
<RuleLeafContainer>
<RuleLeafLabel>{rule.title}</RuleLeafLabel>
<RuleLeafWrapper $isDroppable={canDrop} $isOver={isOver} ref={drop}>
<RuleLeaf q_json={rule.q_json} rule={rule} fulfillmentsForRule={fulfillments} satisfied={satisfied} activeDegreePlanId={activeDegreePlanId}/>
<RuleLeaf
q_json={rule.q_json}
rule={rule}
fulfillmentsForRule={fulfillments}
satisfied={satisfied}
activeDegreePlanId={activeDegreePlanId}
/>
<Row>
{!!satisfied && <SatisfiedCheck />}
<Column>
Expand Down Expand Up @@ -210,7 +218,7 @@ const RuleComponent = (ruleTree : RuleTree) => {
</PickNTitle>
{children.map((ruleTree) => (
<div>
<RuleComponent {...ruleTree} />
<RuleComponent {...ruleTree}/>
</div>
))}
</PickNWrapper>
Expand Down Expand Up @@ -239,7 +247,7 @@ const RuleComponent = (ruleTree : RuleTree) => {
<Column>
{children.map((ruleTree) => (
<div>
<RuleComponent {...ruleTree} />
<RuleComponent {...ruleTree}/>
</div>
))}
</Column>
Expand Down
Loading

0 comments on commit 2420e54

Please sign in to comment.