Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Admin Edit Course Info Page Frontend #884

Merged
merged 10 commits into from
Jul 26, 2023
Merged
Show file tree
Hide file tree
Changes from 7 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
9 changes: 9 additions & 0 deletions packages/app/components/Settings/CourseAdminPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
BellOutlined,
EditOutlined,
QuestionCircleOutlined,
BookOutlined,
} from "@ant-design/icons";
import { Col, Menu, Row, Space, Tooltip } from "antd";
import { useRouter } from "next/router";
Expand All @@ -11,10 +12,12 @@ import { useProfile } from "../../hooks/useProfile";
import CourseOverrideSettings from "./CourseOverrideSettings";
import { SettingsPanelAvatar } from "./SettingsSharedComponents";
import TACheckInCheckOutTimes from "./TACheckInCheckOutTimes";
import CourseInformation from "./CourseInformation";

export enum CourseAdminOptions {
CHECK_IN = "CHECK_IN",
OVERRIDES = "OVERRIDES",
INFO = "INFO",
}

interface CourseAdminPageProps {
Expand Down Expand Up @@ -80,6 +83,9 @@ export default function CourseAdminPanel({
<Menu.Item key={CourseAdminOptions.OVERRIDES} icon={<BellOutlined />}>
Course Overrides
</Menu.Item>
<Menu.Item key={CourseAdminOptions.INFO} icon={<BookOutlined />}>
Course Information
</Menu.Item>
</Menu>
</Col>
<VerticalDivider />
Expand All @@ -91,6 +97,9 @@ export default function CourseAdminPanel({
{currentSettings === CourseAdminOptions.OVERRIDES && (
<CourseOverrideSettings courseId={courseId} />
)}
{currentSettings === CourseAdminOptions.INFO && (
<CourseInformation courseId={courseId} />
)}
</Col>
</Space>
</Row>
Expand Down
163 changes: 163 additions & 0 deletions packages/app/components/Settings/CourseInformation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import { DeleteTwoTone, PlusCircleOutlined } from "@ant-design/icons";
import { API } from "@koh/api-client";
import { Form, Input, InputNumber, Tag, Button, Space, message } from "antd";
import React, { ReactElement, useState } from "react";
import { useCourse } from "../../hooks/useCourse";

type CourseOverrideSettingsProps = { courseId: number };

export default function CourseInfo({
courseId,
}: CourseOverrideSettingsProps): ReactElement {
const [form] = Form.useForm();
const [showCRNInput, setShowCRNInput] = useState(false);
const { course } = useCourse(courseId);
const [crns, setCrns] = useState(
course.crns.map((c) => c.toString().padStart(5, "0"))
);
const [inputCRN, setInputCRN] = useState<string | null>(null);

const showInput = () => {
setShowCRNInput(true);
};

const handleDiscardChanges = async () => {
form.setFieldsValue({ ...course });
setCrns(course.crns.map((c) => c.toString().padStart(5, "0")));
};

const handleSaveChanges = async () => {
const value = await form.validateFields();
value.crns = Array.from(new Set(crns));

try {
await API.course.editCourseInfo(course.id, value);
message.success("Successfully updated course information.");
} catch (e) {
message.error(e.response?.data?.message);
}
};

const handleCRNAdd = () => {
if (inputCRN) {
tiingweii-shii marked this conversation as resolved.
Show resolved Hide resolved
setCrns([...crns, inputCRN]);
}
setShowCRNInput(false);
setInputCRN(null);
};

const handleCRNDelete = (crn) => {
crns.splice(crns.indexOf(crn), 1);
setCrns(crns);
tiingweii-shii marked this conversation as resolved.
Show resolved Hide resolved
};

return (
<div>
<Form form={form} layout="vertical" initialValues={course}>
<Space style={{ marginTop: "25px" }}>
<Form.Item
name="name"
label="Course Display Name"
tooltip="This is the course name that will be displayed within the app"
rules={[
{ required: true, message: "Please input a display name." },
]}
>
<Input placeholder="ex: CS 2500" maxLength={20} />
</Form.Item>
</Space>

<Form.Item
name="coordinator_email"
label="Coordinator Email"
rules={[
{
required: true,
type: "email",
message: "Please input your email.",
},
]}
>
<Input placeholder="[email protected]" />
</Form.Item>

<Form.Item
label="Office Hours Calendar URL"
tooltip={
<div>
See{" "}
<a
target="_blank"
rel="noopener noreferrer"
href="https://info.khouryofficehours.com/coordinators-manual"
>
here
</a>{" "}
to create your office hours calendar
</div>
}
name="icalURL"
rules={[
{
required: true,
type: "url",
message: "Please input your office hours calendar URL.",
},
]}
>
<Input placeholder="https://calendar.google.com/calendar/ical/.../basic.ics" />
</Form.Item>

<Form.Item label="Registered CRNs" name="crns">
tiingweii-shii marked this conversation as resolved.
Show resolved Hide resolved
{crns.map((crn) => (
<Tag
style={{ fontSize: "15px" }}
closeIcon={
<DeleteTwoTone
twoToneColor="#F76C6C"
style={{ fontSize: "15px" }}
/>
}
key={crn}
closable={true}
onClose={() => handleCRNDelete(crn)}
>
{crn}
</Tag>
))}
{showCRNInput ? (
<InputNumber<string>
tiingweii-shii marked this conversation as resolved.
Show resolved Hide resolved
size={"small"}
className="tag-input"
value={inputCRN}
maxLength={5}
min={"00000"}
onChange={(evt) => setInputCRN(evt.padStart(5, "0"))}
onBlur={handleCRNAdd}
onPressEnter={handleCRNAdd}
stringMode
/>
) : (
<Tag
tiingweii-shii marked this conversation as resolved.
Show resolved Hide resolved
icon={<PlusCircleOutlined style={{ fontSize: "15px" }} />}
style={{ fontSize: "14px" }}
color="#408FEA"
className="add-crn"
onClick={showInput}
>
Add CRN
</Tag>
)}
</Form.Item>
</Form>

<Space style={{ marginTop: "5px" }}>
<Button onClick={handleDiscardChanges}>Discard Changes</Button>

<Button onClick={handleSaveChanges} type="primary">
Save Changes
</Button>
</Space>
</div>
);
}
3 changes: 0 additions & 3 deletions packages/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -715,9 +715,6 @@ export class RegisterCourseParams {
}

export class EditCourseInfoParams {
@IsNumber()
courseId!: number;

@IsString()
@IsOptional()
name?: string;
Expand Down
5 changes: 4 additions & 1 deletion packages/server/src/course/course.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,10 @@ export class CourseController {

const course_response = { ...course, crns: null };
try {
course_response.crns = await CourseSectionMappingModel.find({ course });
const mappings = await CourseSectionMappingModel.find({
courseId: course.id,
});
course_response.crns = mappings.map((mapping) => mapping.crn);
} catch (err) {
console.error(
ERROR_MESSAGES.courseController.courseOfficeHourError +
Expand Down
24 changes: 23 additions & 1 deletion packages/server/src/course/course.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,8 +140,30 @@ export class CourseService {
);
}

let courseCrnMaps = await CourseSectionMappingModel.find({ courseId });
for (const courseCrnMap of courseCrnMaps) {
const conflictCourse = await CourseModel.findOne(courseCrnMap.courseId);
tiingweii-shii marked this conversation as resolved.
Show resolved Hide resolved
if (
!coursePatch.crns.includes(courseCrnMap.crn) &&
conflictCourse.semesterId === course.semesterId
) {
try {
await CourseSectionMappingModel.delete({
crn: courseCrnMap.crn,
courseId: course.id,
});
} catch (err) {
console.error(err);
throw new HttpException(
ERROR_MESSAGES.courseController.createCourseMappings,
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
}

for (const crn of new Set(coursePatch.crns)) {
const courseCrnMaps = await CourseSectionMappingModel.find({
courseCrnMaps = await CourseSectionMappingModel.find({
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is my bad for not getting to review the earlier backend PR, and I might be wrong - but I don't think courseCrnMaps has to be requeried once per loop. I think you can maybe just get away with querying it the once at line 143 and using that array throughout. It won't be the most up to date CRNs for that course on each iteration, but I don't think you need it to be up to date each time. You've already made the patched crns into a set so there won't be any conflicts here, and any crns removed from above shouldn't be any reason for conflict either.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This courseCrnMaps here is for any mappings with that crn, which is different from line 143 (mappings for that course), so I think it is still necessary here? Let me know if I misunderstood anything (and sorry for just addressing this after 7 months ∠( ᐛ 」∠)_ )

Thank you for reviewing my PR and leaving so many great comments (。・ω・。) (I have addressed all other ones!)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ahh yes i think i understand. ive also mostly forgotten what all of this code does but regardless i catch the vibes here

crn: crn,
});

Expand Down
8 changes: 5 additions & 3 deletions packages/server/test/course.integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -587,11 +587,10 @@ describe('Course Integration', () => {
});

const editCourseTomato = {
courseId: course.id,
name: 'Tomato',
icalURL: 'https://calendar.google.com/calendar/ical/tomato/basic.ics',
coordinator_email: '[email protected]',
crns: [30303, 67890],
crns: [67890],
};

// update crns, coordinator email, name, icalURL
Expand All @@ -612,6 +611,10 @@ describe('Course Integration', () => {
where: { crn: 67890, courseId: course.id },
});
expect(crnCourseMap).toBeDefined();
const crnCourseMapDeleted = await CourseSectionMappingModel.findOne({
where: { crn: 30303, courseId: course.id },
});
expect(crnCourseMapDeleted).toBeUndefined();
});

it('test crn mapped to another course for a different semester', async () => {
Expand Down Expand Up @@ -646,7 +649,6 @@ describe('Course Integration', () => {
});

const editCourseCrn = {
courseId: potato.id,
crns: [CRN],
};

Expand Down