Path: blob/master/src/packages/frontend/course/assignments/assignments-panel.tsx
1503 views
/*1* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.2* License: MS-RSL – see LICENSE.md for details3*/45import { Alert, Button, Col, Row } from "antd";6import { Map, Set } from "immutable";7import { FormattedMessage, useIntl } from "react-intl";8import {9AppRedux,10useActions,11useMemo,12useRedux,13useState,14} from "@cocalc/frontend/app-framework";15import { Gap, Icon, Tip } from "@cocalc/frontend/components";16import ScrollableList from "@cocalc/frontend/components/scrollable-list";17import { course } from "@cocalc/frontend/i18n";18import { cmp_array } from "@cocalc/util/misc";1920import { CourseActions } from "../actions";21import { AddItems, FoldersToolbar } from "../common/folders-tool-bar";22import {23AssignmentRecord,24IsGradingMap,25NBgraderRunInfo,26SortDescription,27StudentRecord,28} from "../store";29import * as styles from "../styles";30import * as util from "../util";31import { Assignment } from "./assignment";3233interface Props {34frame_id?: string;35name: string;36project_id: string;37redux: AppRedux;38actions: CourseActions;39assignments: Map<string, AssignmentRecord>;40students: Map<string, StudentRecord>;41user_map: object;42frameActions;43}4445export function AssignmentsPanel(props: Props) {46const {47frame_id,48name,49project_id,50redux,51assignments,52students,53user_map,54frameActions,55} = props;5657const intl = useIntl();5859const course_actions = useActions<CourseActions>({ name });6061const expanded_assignments: Set<string> = useRedux(62name,63"expanded_assignments",64);65const active_assignment_sort: SortDescription = useRedux(66name,67"active_assignment_sort",68);69const expanded_peer_configs: Set<string> = useRedux(70name,71"expanded_peer_configs",72);73const active_feedback_edits: IsGradingMap = useRedux(74name,75"active_feedback_edits",76);77const nbgrader_run_info: NBgraderRunInfo | undefined = useRedux(78name,79"nbgrader_run_info",80);8182// search query to restrict which assignments are shown.83const pageFilter = useRedux(name, "pageFilter");84const filter = pageFilter?.get("assignments") ?? "";85const setFilter = (filter: string) => {86course_actions.setPageFilter("assignments", filter);87};8889// whether or not to show deleted assignments on the bottom90const [show_deleted, set_show_deleted] = useState<boolean>(false);9192function get_assignment(id: string): AssignmentRecord {93const assignment = assignments.get(id);94if (assignment == undefined) {95console.warn(`Tried to access undefined assignment ${id}`);96}97return assignment as any;98}99100const { shown_assignments, num_omitted, num_deleted } = useMemo((): {101shown_assignments: any[];102num_omitted: number;103num_deleted: number;104} => {105let f, num_deleted, num_omitted;106let list = util.immutable_to_list(assignments, "assignment_id");107108({ list, num_omitted } = util.compute_match_list({109list,110search_key: "path",111search: filter.trim(),112}));113114if (active_assignment_sort.get("column_name") === "due_date") {115f = (a) => [116a.due_date != null ? a.due_date : 0,117a.path != null ? a.path.toLowerCase() : undefined,118];119} else if (active_assignment_sort.get("column_name") === "dir_name") {120f = (a) => [121a.path != null ? a.path.toLowerCase() : undefined,122a.due_date != null ? a.due_date : 0,123];124}125126({ list, num_deleted } = util.order_list({127list,128compare_function: (a, b) => cmp_array(f(a), f(b)),129reverse: active_assignment_sort.get("is_descending"),130include_deleted: show_deleted,131}));132133return {134shown_assignments: list,135num_omitted,136num_deleted,137};138}, [assignments, active_assignment_sort, show_deleted, filter]);139140function render_sort_link(column_name: string, display_name: string) {141return (142<a143href=""144onClick={(e) => {145e.preventDefault();146return course_actions.assignments.set_active_assignment_sort(147column_name,148);149}}150>151{display_name}152<Gap />153{active_assignment_sort.get("column_name") === column_name ? (154<Icon155style={{ marginRight: "10px" }}156name={157active_assignment_sort.get("is_descending")158? "caret-up"159: "caret-down"160}161/>162) : undefined}163</a>164);165}166167function render_assignment_table_header() {168return (169<div style={{ borderBottom: "1px solid #e5e5e5" }}>170<Row style={{ marginRight: "0px" }}>171<Col md={12}>172{render_sort_link(173"dir_name",174intl.formatMessage({175id: "course.assignments-panel.table-header.assignments",176defaultMessage: "Assignment Name",177}),178)}179</Col>180<Col md={12}>181{render_sort_link("due_date", intl.formatMessage(course.due_date))}182</Col>183</Row>184</div>185);186}187188function render_assignment(assignment_id: string, index: number) {189return (190<Assignment191key={assignment_id}192project_id={project_id}193frame_id={frame_id}194name={name}195redux={redux}196assignment={get_assignment(assignment_id)}197background={index % 2 === 0 ? "#eee" : undefined}198students={students}199user_map={user_map}200is_expanded={expanded_assignments.has(assignment_id)}201expand_peer_config={expanded_peer_configs.has(assignment_id)}202active_feedback_edits={active_feedback_edits}203nbgrader_run_info={nbgrader_run_info}204/>205);206}207208function render_assignments(assignments: { assignment_id: string }[]) {209if (assignments.length == 0) {210return render_no_assignments();211}212return (213<ScrollableList214virtualize215rowCount={assignments.length}216rowRenderer={({ key, index }) => render_assignment(key, index)}217rowKey={(index) => assignments[index]?.assignment_id ?? ""}218cacheId={`course-assignments-${name}-${frame_id}`}219/>220);221}222223function render_no_assignments() {224return (225<div>226<Alert227type="info"228style={{229margin: "15px auto",230fontSize: "12pt",231maxWidth: "800px",232}}233message={234<b>235<a onClick={() => frameActions.setModal("add-assignments")}>236<FormattedMessage237id="course.assignments-panel.no_assignments.message"238defaultMessage={"Add Assignments to your Course"}239description={"online course for students"}240/>241</a>242</b>243}244description={245<div>246<FormattedMessage247id="course.assignments-panel.no_assignments.description"248defaultMessage={`249<p>250An assignment is a <i>directory</i> of files somewhere in your251CoCalc project. You copy the assignment to your students and252they work on it; later, you collect it, grade it, and return the253graded version to them.254</p>255<p>256<A>Add assignments to your course</A> by clicking "Add Assignment..." above.257You can create and select one or more directories and they will become assignments258that you can then customize and distribute to your students.259</p>`}260values={{261A: (c) => (262<a onClick={() => frameActions.setModal("add-assignments")}>263{c}264</a>265),266}}267description={"online course for students"}268/>269</div>270}271/>272</div>273);274}275276function render_show_deleted(num_deleted: number, num_shown: number) {277if (show_deleted) {278return (279<Button280style={styles.show_hide_deleted({ needs_margin: num_shown > 0 })}281onClick={() => set_show_deleted(false)}282>283<Tip284placement="left"285title="Hide deleted"286tip="Assignments are never really deleted. Click this button so that deleted assignments aren't included at the bottom of the list. Deleted assignments are always hidden from the list of grades for a student."287>288Hide {num_deleted} deleted assignments289</Tip>290</Button>291);292} else {293return (294<Button295style={styles.show_hide_deleted({ needs_margin: num_shown > 0 })}296onClick={() => {297set_show_deleted(true);298setFilter("");299}}300>301<Tip302placement="left"303title="Show deleted"304tip="Assignments are not deleted forever even after you delete them. Click this button to show any deleted assignments at the bottom of the list of assignments. You can then click on the assignment and click undelete to bring the assignment back."305>306Show {num_deleted} deleted assignments307</Tip>308</Button>309);310}311}312313function header() {314return (315<div style={{ marginBottom: "15px" }}>316<FoldersToolbar317search={filter}318search_change={setFilter}319num_omitted={num_omitted}320project_id={project_id}321items={assignments}322add_folders={course_actions.assignments.addAssignment}323item_name={"assignment"}324plural_item_name={"assignments"}325/>326</div>327);328}329330return (331<div className={"smc-vfill"} style={{ margin: "0" }}>332{header()}333{shown_assignments.length > 0334? render_assignment_table_header()335: undefined}336<div className="smc-vfill">337{render_assignments(shown_assignments)}{" "}338{num_deleted339? render_show_deleted(num_deleted, shown_assignments.length)340: undefined}341</div>342</div>343);344}345346// used for adding assignments outside of the above component.347export function AddAssignments({ name, actions, close }) {348const assignments = useRedux(name, "assignments");349return (350<AddItems351itemName="assignment"352items={assignments}353addItems={(paths) => {354actions.assignments.addAssignment(paths);355close?.();356}}357selectorStyle={{358position: null,359width: "100%",360boxShadow: null,361zIndex: null,362backgroundColor: null,363}}364defaultOpen365closable={false}366/>367);368}369370371