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 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
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,9 +12,11 @@ import { useProfile } from "../../hooks/useProfile";
import CourseRosterPage from "./CourseRosterPage";
import { SettingsPanelAvatar } from "./SettingsSharedComponents";
import TACheckInCheckOutTimes from "./TACheckInCheckOutTimes";
import CourseInformation from "./CourseInformation";

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

Expand Down Expand Up @@ -80,11 +83,17 @@ export default function CourseAdminPanel({
<Menu.Item key={CourseAdminOptions.ROSTER} icon={<BellOutlined />}>
Course Roster
</Menu.Item>
<Menu.Item key={CourseAdminOptions.INFO} icon={<BookOutlined />}>
Course Information
</Menu.Item>
</Menu>
</Col>
<VerticalDivider />
<Space direction="vertical" size={40} style={{ flexGrow: 1 }}>
<Col span={20}>
{currentSettings === CourseAdminOptions.INFO && (
<CourseInformation courseId={courseId} />
)}
{currentSettings === CourseAdminOptions.CHECK_IN && (
<TACheckInCheckOutTimes courseId={courseId} />
)}
Expand Down
173 changes: 173 additions & 0 deletions packages/app/components/Settings/CourseInformation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
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 styled from "styled-components";
import { useCourse } from "../../hooks/useCourse";

type CourseOverrideSettingsProps = { courseId: number };

const CRNTag = styled(Tag)`
font-size: 15px;
padding: 5px 10px;
`;

const AddCRNTag = styled(CRNTag)`
cursor: pointer;
`;

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
if (crns.includes(inputCRN)) {
message.error(`The CRN ${inputCRN} already exists.`);
} else {
setCrns([...crns, inputCRN]);
}
}
setShowCRNInput(false);
setInputCRN(null);
};

const handleCRNDelete = (crn) => {
setCrns(crns.filter((c) => c !== crn));
};

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,
pattern: new RegExp("https://.*.ics"),
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">
{crns.map((crn) => (
<CRNTag
closeIcon={
<DeleteTwoTone
twoToneColor="#F76C6C"
style={{ fontSize: "18px" }}
/>
}
key={crn}
closable={true}
onClose={() => handleCRNDelete(crn)}
>
{crn}
</CRNTag>
))}
{showCRNInput ? (
<InputNumber<string>
tiingweii-shii marked this conversation as resolved.
Show resolved Hide resolved
className="tag-input"
value={inputCRN}
maxLength={5}
min={"00000"}
onChange={(evt) => setInputCRN(evt.padStart(5, "0"))}
onBlur={handleCRNAdd}
onPressEnter={handleCRNAdd}
stringMode
/>
) : (
<AddCRNTag
icon={<PlusCircleOutlined style={{ fontSize: "15px" }} />}
color="#408FEA"
className="add-crn"
onClick={showInput}
>
Add CRN
</AddCRNTag>
)}
</Form.Item>
</Form>

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

<Button onClick={handleSaveChanges} type="primary">
Save Changes
</Button>
</Space>
</div>
);
}
6 changes: 5 additions & 1 deletion packages/app/hooks/useQueue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,11 @@ export function useQueue(qid: number, onUpdate?: OnUpdate): UseQueueReturn {
)
);

const { data: queue, error: queueError, mutate: mutateQueue } = useSWR(
const {
data: queue,
error: queueError,
mutate: mutateQueue,
} = useSWR(
key,
useCallback(async () => API.queues.get(Number(qid)), [qid]),
{
Expand Down
3 changes: 0 additions & 3 deletions packages/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -692,9 +692,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 @@ -149,7 +149,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
20 changes: 19 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,26 @@ export class CourseService {
);
}

let courseCrnMaps = await CourseSectionMappingModel.find({ courseId });
for (const courseCrnMap of courseCrnMaps) {
if (!coursePatch.crns.includes(courseCrnMap.crn)) {
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 @@ -857,11 +857,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 @@ -882,6 +881,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 @@ -916,7 +919,6 @@ describe('Course Integration', () => {
});

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

Expand Down