CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
sagemathinc

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/frontend/course/handouts/handouts-panel.tsx
Views: 687
1
/*
2
* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.
3
* License: MS-RSL – see LICENSE.md for details
4
*/
5
6
import { Alert, Button } from "antd";
7
import { Set } from "immutable";
8
import { useState } from "react";
9
import { FormattedMessage, useIntl } from "react-intl";
10
11
// CoCalc and course components
12
import { useRedux } from "@cocalc/frontend/app-framework";
13
import { Icon, Tip } from "@cocalc/frontend/components";
14
import ScrollableList from "@cocalc/frontend/components/scrollable-list";
15
import { course } from "@cocalc/frontend/i18n";
16
import { UserMap } from "@cocalc/frontend/todo-types";
17
import { cmp } from "@cocalc/util/misc";
18
import { CourseActions } from "../actions";
19
import { AddItems, FoldersToolbar } from "../common/folders-tool-bar";
20
import { HandoutRecord, HandoutsMap, StudentsMap } from "../store";
21
import * as styles from "../styles";
22
import * as util from "../util";
23
import { Handout } from "./handout";
24
25
interface HandoutsPanelReactProps {
26
frame_id?: string;
27
name: string;
28
actions: CourseActions;
29
project_id: string;
30
handouts: HandoutsMap; // handout_id -> handout
31
students: StudentsMap; // student_id -> student
32
user_map: UserMap;
33
frameActions;
34
}
35
36
export function HandoutsPanel({
37
frame_id,
38
name,
39
actions,
40
project_id,
41
handouts,
42
students,
43
user_map,
44
frameActions,
45
}: HandoutsPanelReactProps) {
46
const intl = useIntl();
47
const expanded_handouts: Set<string> | undefined = useRedux(
48
name,
49
"expanded_handouts",
50
);
51
52
const [show_deleted, set_show_deleted] = useState<boolean>(false);
53
54
const pageFilter = useRedux(name, "pageFilter");
55
const filter = pageFilter?.get("handouts") ?? "";
56
const setFilter = (filter: string) => {
57
actions.setPageFilter("handouts", filter);
58
};
59
60
function get_handout(id: string): HandoutRecord {
61
const handout = handouts.get(id);
62
if (handout == undefined) {
63
console.warn(`Tried to access undefined handout ${id}`);
64
}
65
return handout as any;
66
}
67
68
function compute_handouts_list() {
69
let num_deleted, num_omitted;
70
let list = util.immutable_to_list(handouts, "handout_id");
71
72
({ list, num_omitted } = util.compute_match_list({
73
list,
74
search_key: "path",
75
search: filter.trim(),
76
}));
77
78
({ list, num_deleted } = util.order_list({
79
list,
80
compare_function: (a, b) =>
81
cmp(a.path?.toLowerCase(), b.path?.toLowerCase()),
82
reverse: false,
83
include_deleted: show_deleted,
84
}));
85
86
return {
87
shown_handouts: list,
88
num_omitted,
89
num_deleted,
90
};
91
}
92
93
function render_show_deleted_button(num_deleted, num_shown) {
94
const label = intl.formatMessage(
95
{
96
id: "course.handouts-panel.show_deleted_button.label",
97
defaultMessage: `{show_deleted, select, true {Hide} other {Show}} {num_deleted} deleted handouts`,
98
},
99
{ num_deleted, show_deleted },
100
);
101
if (show_deleted) {
102
const tooltip = intl.formatMessage({
103
id: "course.handouts-panel.show_deleted_button.hide.tooltip",
104
defaultMessage: `Handouts are never really deleted.
105
Click this button so that deleted handouts aren't included at the bottom of the list.`,
106
});
107
return (
108
<Button
109
style={styles.show_hide_deleted({ needs_margin: num_shown > 0 })}
110
onClick={() => set_show_deleted(false)}
111
>
112
<Tip placement="left" title="Hide deleted" tip={tooltip}>
113
{label}
114
</Tip>
115
</Button>
116
);
117
} else {
118
const tooltip = intl.formatMessage({
119
id: "course.handouts-panel.show_deleted_button.show.tooltip",
120
defaultMessage: `Handouts are not deleted forever even after you delete them.
121
Click this button to show any deleted handouts at the bottom of the list of handouts.
122
You can then click on the handout and click undelete to bring the handout back.`,
123
});
124
return (
125
<Button
126
style={styles.show_hide_deleted({ needs_margin: num_shown > 0 })}
127
onClick={() => {
128
set_show_deleted(true);
129
setFilter("");
130
}}
131
>
132
<Tip placement="left" title="Show deleted" tip={tooltip}>
133
{label}
134
</Tip>
135
</Button>
136
);
137
}
138
}
139
140
function render_handout(handout_id: string, index: number) {
141
return (
142
<Handout
143
frame_id={frame_id}
144
backgroundColor={index % 2 === 0 ? "#eee" : undefined}
145
key={handout_id}
146
handout={get_handout(handout_id)}
147
project_id={project_id}
148
students={students}
149
user_map={user_map}
150
actions={actions}
151
is_expanded={expanded_handouts?.has(handout_id) ?? false}
152
name={name}
153
/>
154
);
155
}
156
157
function render_handouts(handouts) {
158
if (handouts.length == 0) {
159
return render_no_handouts();
160
}
161
return (
162
<ScrollableList
163
virtualize
164
rowCount={handouts.length}
165
rowRenderer={({ key, index }) => render_handout(key, index)}
166
rowKey={(index) => handouts[index]?.handout_id ?? ""}
167
cacheId={`course-handouts-${name}-${frame_id}`}
168
/>
169
);
170
}
171
172
function render_no_handouts() {
173
return (
174
<div>
175
<Alert
176
type="info"
177
style={{
178
margin: "15px auto",
179
fontSize: "12pt",
180
maxWidth: "800px",
181
}}
182
message={
183
<b>
184
<a onClick={() => frameActions.setModal("add-handouts")}>
185
<FormattedMessage
186
id="course.handouts-panel.no_assignments.message"
187
defaultMessage={"Add Handouts to your Course"}
188
description={"online course for students"}
189
/>
190
</a>
191
</b>
192
}
193
description={
194
<div>
195
<FormattedMessage
196
id="course.handouts-panel.no_assignments.description"
197
description={"online course for students"}
198
defaultMessage={`
199
<p>
200
A handout is a <i>directory</i> of files somewhere in your
201
CoCalc project, which you copy to all of your students. They can
202
then do anything they want with that handout.
203
</p>
204
<p>
205
<A>Add handouts to your course</A> by clicking "Add Handout..." above.
206
You can create or select one or more directories
207
and they will become handouts that you can
208
then customize and distribute to your students.
209
</p>`}
210
values={{
211
A: (c) => (
212
<a onClick={() => frameActions.setModal("add-handouts")}>
213
{c}
214
</a>
215
),
216
}}
217
/>
218
</div>
219
}
220
/>
221
</div>
222
);
223
}
224
225
// Computed data from state changes have to go in render
226
const { shown_handouts, num_omitted, num_deleted } = compute_handouts_list();
227
228
const header = (
229
<FoldersToolbar
230
search={filter}
231
search_change={setFilter}
232
num_omitted={num_omitted}
233
project_id={project_id}
234
items={handouts}
235
add_folders={actions.handouts.addHandout}
236
item_name={"handout"}
237
plural_item_name={"handouts"}
238
/>
239
);
240
241
return (
242
<div className={"smc-vfill"} style={{ margin: "0" }}>
243
{header}
244
<div style={{ marginTop: "5px" }} />
245
{render_handouts(shown_handouts)}
246
{num_deleted > 0
247
? render_show_deleted_button(
248
num_deleted,
249
shown_handouts.length != null ? shown_handouts.length : 0,
250
)
251
: undefined}
252
</div>
253
);
254
}
255
256
export function HandoutsPanelHeader(props: { n: number }) {
257
const intl = useIntl();
258
259
return (
260
<Tip
261
delayShow={1300}
262
title="Handouts"
263
tip={intl.formatMessage({
264
id: "course.handouts-panel.header.tooltip",
265
defaultMessage:
266
"This tab lists all of the handouts associated with your course.",
267
description: "online course for students",
268
})}
269
>
270
<span>
271
<Icon name="files" /> {intl.formatMessage(course.handouts)}{" "}
272
{props.n != null ? ` (${props.n})` : ""}
273
</span>
274
</Tip>
275
);
276
}
277
278
// used for adding assignments outside of the above component.
279
export function AddHandouts({ name, actions, close }) {
280
const handouts = useRedux(name, "handouts");
281
return (
282
<AddItems
283
itemName="handout"
284
items={handouts}
285
addItems={(paths) => {
286
actions.handouts.addHandout(paths);
287
close?.();
288
}}
289
selectorStyle={{
290
position: null,
291
width: "100%",
292
boxShadow: null,
293
zIndex: null,
294
backgroundColor: null,
295
}}
296
defaultOpen
297
closable={false}
298
/>
299
);
300
}
301
302