@capytale/meta-player 0.3.4 → 0.3.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -2
- package/src/App.tsx +1 -1
- package/src/MetaPlayer.tsx +29 -2
- package/src/features/activityData/activityDataSlice.ts +47 -12
- package/src/features/activityJS/ActivityJSProvider.tsx +13 -14
- package/src/features/activityJS/Saver.tsx +34 -24
- package/src/features/layout/layoutSlice.ts +16 -2
- package/src/features/navbar/CapytaleMenu.tsx +93 -79
- package/src/features/pedago/InstructionsEditor.tsx +50 -33
- package/src/features/pedago/PdfEditor.tsx +91 -0
- package/src/features/pedago/PedagoCommands.tsx +325 -0
- package/src/features/pedago/SharedNotesEditor.tsx +145 -0
- package/src/features/pedago/index.tsx +19 -74
- package/src/features/pedago/style.module.scss +119 -15
- package/src/hooks/index.ts +9 -1
- package/src/index.css +6 -2
- package/src/index.tsx +2 -1
- package/src/features/pedago/AnswerSheetEditor.tsx +0 -72
|
@@ -2,14 +2,17 @@ import { useCapytaleRichTextEditor } from "@capytale/capytale-rich-text-editor";
|
|
|
2
2
|
import { useAppDispatch, useAppSelector } from "../../app/hooks";
|
|
3
3
|
import {
|
|
4
4
|
selectInstructions,
|
|
5
|
+
selectInstructionsType,
|
|
5
6
|
selectMode,
|
|
6
7
|
setCanSaveInstructions,
|
|
8
|
+
setInstructionsType,
|
|
7
9
|
setIsMPDirty,
|
|
8
10
|
setLexicalInstructionsState,
|
|
9
11
|
} from "../activityData/activityDataSlice";
|
|
10
12
|
import { forwardRef, useImperativeHandle, useMemo, useRef } from "react";
|
|
11
13
|
import { Toast } from "primereact/toast";
|
|
12
14
|
import settings from "../../settings";
|
|
15
|
+
import { Button } from "primereact/button";
|
|
13
16
|
|
|
14
17
|
const InstructionsEditor: React.FC = forwardRef((_props, ref) => {
|
|
15
18
|
const [Editor, getState, _canSave] = useCapytaleRichTextEditor();
|
|
@@ -31,6 +34,7 @@ const InstructionsEditor: React.FC = forwardRef((_props, ref) => {
|
|
|
31
34
|
: undefined;
|
|
32
35
|
}, [initialInstructions]);
|
|
33
36
|
const initialStateOnChangeDone = useRef<boolean>(false);
|
|
37
|
+
const instructionsType = useAppSelector(selectInstructionsType);
|
|
34
38
|
|
|
35
39
|
useImperativeHandle(ref, () => {
|
|
36
40
|
return {
|
|
@@ -44,39 +48,52 @@ const InstructionsEditor: React.FC = forwardRef((_props, ref) => {
|
|
|
44
48
|
return (
|
|
45
49
|
<>
|
|
46
50
|
<Toast position="bottom-right" ref={toast} />
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
51
|
+
<>
|
|
52
|
+
{instructionsType === "none" && (
|
|
53
|
+
<>
|
|
54
|
+
<p>Pas de consigne.</p>
|
|
55
|
+
<Button
|
|
56
|
+
label="Créer une consigne"
|
|
57
|
+
onClick={() => dispatch(setInstructionsType("rich"))}
|
|
58
|
+
/>
|
|
59
|
+
</>
|
|
60
|
+
)}
|
|
61
|
+
{instructionsType === "rich" && (
|
|
62
|
+
<Editor
|
|
63
|
+
placeholderText="Écrivez la consigne ici..."
|
|
64
|
+
htmlInitialContent={htmlInitialContent}
|
|
65
|
+
initialEditorState={initialEditorState}
|
|
66
|
+
jsonSizeLimit={settings.STATEMENT_MAX_SIZE}
|
|
67
|
+
onChange={(editorState) => {
|
|
68
|
+
dispatch(setLexicalInstructionsState(editorState));
|
|
69
|
+
if (initialStateOnChangeDone.current) {
|
|
70
|
+
dispatch(setIsMPDirty(true));
|
|
71
|
+
} else {
|
|
72
|
+
initialStateOnChangeDone.current = true;
|
|
73
|
+
}
|
|
74
|
+
}}
|
|
75
|
+
onJsonSizeLimitExceeded={() => {
|
|
76
|
+
dispatch(setCanSaveInstructions(false));
|
|
77
|
+
toast.current!.show({
|
|
78
|
+
summary: "Erreur",
|
|
79
|
+
detail: `Le contenu de la consigne est trop volumineux. Veuillez le réduire avant de l'enregistrer.\nPeut-être avez-vous inséré une image trop volumineuse ?`,
|
|
80
|
+
severity: "error",
|
|
81
|
+
life: 10000,
|
|
82
|
+
});
|
|
83
|
+
}}
|
|
84
|
+
onJsonSizeLimitMet={() => {
|
|
85
|
+
dispatch(setCanSaveInstructions(true));
|
|
86
|
+
toast.current!.show({
|
|
87
|
+
summary: "Succès",
|
|
88
|
+
detail: `Le contenu de la consigne ne dépasse plus la taille limite.`,
|
|
89
|
+
severity: "success",
|
|
90
|
+
life: 4000,
|
|
91
|
+
});
|
|
92
|
+
}}
|
|
93
|
+
isEditable={isEditable}
|
|
94
|
+
/>
|
|
95
|
+
)}
|
|
96
|
+
</>
|
|
80
97
|
</>
|
|
81
98
|
);
|
|
82
99
|
});
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { FC, useEffect, useMemo } from "react";
|
|
2
|
+
import { useDropzone } from "react-dropzone";
|
|
3
|
+
import styles from "./style.module.scss";
|
|
4
|
+
import { useAppDispatch, useAppSelector } from "../../app/hooks";
|
|
5
|
+
import {
|
|
6
|
+
selectMode,
|
|
7
|
+
selectPdfInstructions,
|
|
8
|
+
setPdfInstructions,
|
|
9
|
+
} from "../activityData/activityDataSlice";
|
|
10
|
+
import { Button } from "primereact/button";
|
|
11
|
+
|
|
12
|
+
export const PdfEditor: FC = () => {
|
|
13
|
+
const dispatch = useAppDispatch();
|
|
14
|
+
const {
|
|
15
|
+
getRootProps,
|
|
16
|
+
getInputProps,
|
|
17
|
+
isFocused,
|
|
18
|
+
isDragAccept,
|
|
19
|
+
isDragReject,
|
|
20
|
+
acceptedFiles,
|
|
21
|
+
} = useDropzone({
|
|
22
|
+
accept: {
|
|
23
|
+
"application/pdf": [".pdf"],
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
if (acceptedFiles.length) {
|
|
28
|
+
dispatch(setPdfInstructions(acceptedFiles[0]));
|
|
29
|
+
}
|
|
30
|
+
}, [acceptedFiles]);
|
|
31
|
+
const mode = useAppSelector(selectMode);
|
|
32
|
+
const pdfInstructions = useAppSelector(selectPdfInstructions);
|
|
33
|
+
const pdfData = useMemo(
|
|
34
|
+
() => (pdfInstructions ? URL.createObjectURL(pdfInstructions) : null),
|
|
35
|
+
[pdfInstructions],
|
|
36
|
+
);
|
|
37
|
+
return (
|
|
38
|
+
<>
|
|
39
|
+
{pdfData && (
|
|
40
|
+
<div
|
|
41
|
+
style={{
|
|
42
|
+
width: "100%",
|
|
43
|
+
height: "100%",
|
|
44
|
+
position: "relative",
|
|
45
|
+
overflow: "hidden",
|
|
46
|
+
}}
|
|
47
|
+
>
|
|
48
|
+
<object
|
|
49
|
+
data={pdfData + "#?navpanes=0"}
|
|
50
|
+
type="application/pdf"
|
|
51
|
+
width="100%"
|
|
52
|
+
height="100%"
|
|
53
|
+
>
|
|
54
|
+
<p>
|
|
55
|
+
Votre navigateur ne supporte pas les PDFs, vous pouvez{" "}
|
|
56
|
+
<a href={pdfData}>télécharger le fichier</a> à la place.
|
|
57
|
+
</p>
|
|
58
|
+
</object>
|
|
59
|
+
<div className="meta-player-content-cover"></div>
|
|
60
|
+
{mode === "create" && (
|
|
61
|
+
<Button
|
|
62
|
+
label="Supprimer le PDF"
|
|
63
|
+
severity="danger"
|
|
64
|
+
icon="pi pi-trash"
|
|
65
|
+
className={styles.deleteButton}
|
|
66
|
+
onClick={() => dispatch(setPdfInstructions(null))}
|
|
67
|
+
style={{ position: "absolute", bottom: "20px", right: "20px" }}
|
|
68
|
+
/>
|
|
69
|
+
)}
|
|
70
|
+
</div>
|
|
71
|
+
)}
|
|
72
|
+
{!pdfData && (
|
|
73
|
+
<div className={styles.dropzone} {...getRootProps()}>
|
|
74
|
+
<div
|
|
75
|
+
className={styles.dropzoneRectangle}
|
|
76
|
+
data-is-focused={isFocused}
|
|
77
|
+
data-is-drag-accept={isDragAccept}
|
|
78
|
+
data-is-drag-reject={isDragReject}
|
|
79
|
+
>
|
|
80
|
+
<input {...getInputProps()} />
|
|
81
|
+
<p>Déposez un PDF ici ou cliquez pour sélectionner un fichier...</p>
|
|
82
|
+
<i
|
|
83
|
+
className="pi pi-file-arrow-up"
|
|
84
|
+
style={{ fontSize: "2.5rem" }}
|
|
85
|
+
></i>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
)}
|
|
89
|
+
</>
|
|
90
|
+
);
|
|
91
|
+
};
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import { useAppDispatch, useAppSelector } from "../../app/hooks";
|
|
2
|
+
import {
|
|
3
|
+
selectHasGradingOrComments,
|
|
4
|
+
selectInstructionsType,
|
|
5
|
+
selectMode,
|
|
6
|
+
selectPdfInstructions,
|
|
7
|
+
selectSharedNotesType,
|
|
8
|
+
setInstructionsType,
|
|
9
|
+
setSharedNotesType,
|
|
10
|
+
} from "../activityData/activityDataSlice";
|
|
11
|
+
import {
|
|
12
|
+
PedagoTab,
|
|
13
|
+
selectIsGradingVisible,
|
|
14
|
+
selectOrientation,
|
|
15
|
+
selectPedagoTab,
|
|
16
|
+
setPedagoTab,
|
|
17
|
+
toggleIsGradingVisible,
|
|
18
|
+
toggleIsPedagoVisible,
|
|
19
|
+
} from "../layout/layoutSlice";
|
|
20
|
+
import styles from "./style.module.scss";
|
|
21
|
+
import { Button } from "primereact/button";
|
|
22
|
+
import settings from "../../settings";
|
|
23
|
+
import { SelectButton } from "primereact/selectbutton";
|
|
24
|
+
import { FC, useMemo } from "react";
|
|
25
|
+
import { TabMenu } from "primereact/tabmenu";
|
|
26
|
+
import { classNames } from "primereact/utils";
|
|
27
|
+
|
|
28
|
+
export const PedagoCommands = () => {
|
|
29
|
+
const isHorizontal = useAppSelector(selectOrientation) === "horizontal";
|
|
30
|
+
return isHorizontal ? (
|
|
31
|
+
<HorizontalPedagoCommands />
|
|
32
|
+
) : (
|
|
33
|
+
<VerticalPedagoCommands />
|
|
34
|
+
);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
type DocumentSelectorHzItem = {
|
|
38
|
+
name: string;
|
|
39
|
+
value: PedagoTab;
|
|
40
|
+
tooltip: string;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
type DocumentSelectorItem = DocumentSelectorHzItem & {
|
|
44
|
+
template: (item: any) => JSX.Element;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const HorizontalPedagoCommands = () => {
|
|
48
|
+
const mode = useAppSelector(selectMode);
|
|
49
|
+
const dispatch = useAppDispatch();
|
|
50
|
+
return (
|
|
51
|
+
<div className={styles.pedagoCommands}>
|
|
52
|
+
<div
|
|
53
|
+
style={{
|
|
54
|
+
display: "flex",
|
|
55
|
+
flexDirection: "row",
|
|
56
|
+
justifyContent: "space-between",
|
|
57
|
+
alignItems: "center",
|
|
58
|
+
width: "100%",
|
|
59
|
+
paddingLeft: "4px",
|
|
60
|
+
}}
|
|
61
|
+
>
|
|
62
|
+
{(mode === "assignment" || mode === "review") && (
|
|
63
|
+
<ShowHideGradingButton />
|
|
64
|
+
)}
|
|
65
|
+
<div></div>
|
|
66
|
+
<Button
|
|
67
|
+
severity="secondary"
|
|
68
|
+
icon="pi pi-times"
|
|
69
|
+
rounded
|
|
70
|
+
text
|
|
71
|
+
aria-label="Masquer les consignes"
|
|
72
|
+
tooltip="Masquer les consignes"
|
|
73
|
+
tooltipOptions={{
|
|
74
|
+
position: "left",
|
|
75
|
+
showDelay: settings.TOOLTIP_SHOW_DELAY,
|
|
76
|
+
}}
|
|
77
|
+
onClick={() => dispatch(toggleIsPedagoVisible())}
|
|
78
|
+
style={{ flexShrink: 0 }}
|
|
79
|
+
/>
|
|
80
|
+
</div>
|
|
81
|
+
<HorizontalDocumentSelector />
|
|
82
|
+
</div>
|
|
83
|
+
);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const DocumentSelectorItemActions: FC<{
|
|
87
|
+
item: DocumentSelectorHzItem;
|
|
88
|
+
}> = ({ item }) => {
|
|
89
|
+
const mode = useAppSelector(selectMode);
|
|
90
|
+
const itemTemplate = (option: any) => {
|
|
91
|
+
return <i className={option.icon}></i>;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const instructionsType = useAppSelector(selectInstructionsType);
|
|
95
|
+
const sharedNotesType = useAppSelector(selectSharedNotesType);
|
|
96
|
+
const pdfInstructions = useAppSelector(selectPdfInstructions);
|
|
97
|
+
const dispatch = useAppDispatch();
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<>
|
|
101
|
+
{mode === "create" && (
|
|
102
|
+
<>
|
|
103
|
+
<div className={styles.pdfActions}>
|
|
104
|
+
{item.value === "pdf" && !pdfInstructions && (
|
|
105
|
+
<i className="pi pi-eye-slash"></i>
|
|
106
|
+
)}
|
|
107
|
+
{item.value === "pdf" && pdfInstructions && (
|
|
108
|
+
<i className="pi pi-file-pdf"></i>
|
|
109
|
+
)}
|
|
110
|
+
</div>
|
|
111
|
+
{item.value !== "pdf" && (
|
|
112
|
+
<SelectButton
|
|
113
|
+
className={styles.smallSelectButton}
|
|
114
|
+
value={
|
|
115
|
+
item.value === "instructions"
|
|
116
|
+
? instructionsType
|
|
117
|
+
: sharedNotesType
|
|
118
|
+
}
|
|
119
|
+
onChange={(e) => {
|
|
120
|
+
if (item.value === "instructions") {
|
|
121
|
+
dispatch(setInstructionsType(e.value));
|
|
122
|
+
} else {
|
|
123
|
+
dispatch(setSharedNotesType(e.value));
|
|
124
|
+
}
|
|
125
|
+
}}
|
|
126
|
+
itemTemplate={itemTemplate}
|
|
127
|
+
optionLabel="value"
|
|
128
|
+
allowEmpty={false}
|
|
129
|
+
options={[
|
|
130
|
+
{
|
|
131
|
+
label: "Éditeur riche",
|
|
132
|
+
value: "rich",
|
|
133
|
+
icon: "pi pi-pen-to-square",
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
label: "Pas de consigne",
|
|
137
|
+
value: "none",
|
|
138
|
+
icon: "pi pi-eye-slash",
|
|
139
|
+
},
|
|
140
|
+
]}
|
|
141
|
+
tooltip={item.tooltip}
|
|
142
|
+
tooltipOptions={{
|
|
143
|
+
position: "left",
|
|
144
|
+
showDelay: settings.TOOLTIP_SHOW_DELAY,
|
|
145
|
+
}}
|
|
146
|
+
/>
|
|
147
|
+
)}
|
|
148
|
+
</>
|
|
149
|
+
)}
|
|
150
|
+
</>
|
|
151
|
+
);
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const HorizontalDocumentSelector = () => {
|
|
155
|
+
const pedagoTab = useAppSelector(selectPedagoTab);
|
|
156
|
+
const instructionsType = useAppSelector(selectInstructionsType);
|
|
157
|
+
const mode = useAppSelector(selectMode);
|
|
158
|
+
// const pdfInstructions = useAppSelector(selectPdfInstructions);
|
|
159
|
+
const sharedNotesType = useAppSelector(selectSharedNotesType);
|
|
160
|
+
const dispatch = useAppDispatch();
|
|
161
|
+
|
|
162
|
+
const items: DocumentSelectorHzItem[] = useMemo(() => {
|
|
163
|
+
const allItems: DocumentSelectorHzItem[] = [];
|
|
164
|
+
if (mode === "create" || instructionsType !== "none") {
|
|
165
|
+
allItems.push({
|
|
166
|
+
name: "Consignes",
|
|
167
|
+
value: "instructions",
|
|
168
|
+
tooltip: "Activer/désactiver les consignes",
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
/* // Removed PDF for now
|
|
172
|
+
if (mode === "create" || pdfInstructions) {
|
|
173
|
+
allItems.push({
|
|
174
|
+
name: "PDF",
|
|
175
|
+
value: "pdf",
|
|
176
|
+
tooltip: "Activer/désactiver le PDF",
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
*/
|
|
180
|
+
if (mode === "create" || sharedNotesType !== "none") {
|
|
181
|
+
allItems.push({
|
|
182
|
+
name: "Notes partagées",
|
|
183
|
+
value: "sharedNotes",
|
|
184
|
+
tooltip: "Activer/désactiver les notes partagées",
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
return allItems;
|
|
188
|
+
}, []);
|
|
189
|
+
|
|
190
|
+
return (
|
|
191
|
+
<div className={styles.pedagoHorizontalTabMenu}>
|
|
192
|
+
{items.map((item) => (
|
|
193
|
+
<div
|
|
194
|
+
className={styles.pedagoHorizontalTab}
|
|
195
|
+
onClick={() => dispatch(setPedagoTab(item.value))}
|
|
196
|
+
data-selected={item.value === pedagoTab}
|
|
197
|
+
key={item.value}
|
|
198
|
+
>
|
|
199
|
+
<span>{item.name}</span>
|
|
200
|
+
<DocumentSelectorItemActions item={item} />
|
|
201
|
+
</div>
|
|
202
|
+
))}
|
|
203
|
+
</div>
|
|
204
|
+
);
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const VerticalPedagoCommands = () => {
|
|
208
|
+
const mode = useAppSelector(selectMode);
|
|
209
|
+
const dispatch = useAppDispatch();
|
|
210
|
+
return (
|
|
211
|
+
<div className={styles.pedagoCommands}>
|
|
212
|
+
<VerticalDocumentSelector />
|
|
213
|
+
<div className={styles.pedagoSeparator}></div>
|
|
214
|
+
{(mode === "assignment" || mode === "review") && (
|
|
215
|
+
<ShowHideGradingButton />
|
|
216
|
+
)}
|
|
217
|
+
<Button
|
|
218
|
+
severity="secondary"
|
|
219
|
+
icon="pi pi-times"
|
|
220
|
+
rounded
|
|
221
|
+
text
|
|
222
|
+
aria-label="Masquer les consignes"
|
|
223
|
+
tooltip="Masquer les consignes"
|
|
224
|
+
tooltipOptions={{
|
|
225
|
+
position: "left",
|
|
226
|
+
showDelay: settings.TOOLTIP_SHOW_DELAY,
|
|
227
|
+
}}
|
|
228
|
+
onClick={() => dispatch(toggleIsPedagoVisible())}
|
|
229
|
+
style={{ flexShrink: 0 }}
|
|
230
|
+
/>
|
|
231
|
+
</div>
|
|
232
|
+
);
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
const VerticalDocumentSelector = () => {
|
|
236
|
+
const pedagoTab = useAppSelector(selectPedagoTab);
|
|
237
|
+
const instructionsType = useAppSelector(selectInstructionsType);
|
|
238
|
+
const mode = useAppSelector(selectMode);
|
|
239
|
+
// const pdfInstructions = useAppSelector(selectPdfInstructions);
|
|
240
|
+
const sharedNotesType = useAppSelector(selectSharedNotesType);
|
|
241
|
+
const dispatch = useAppDispatch();
|
|
242
|
+
|
|
243
|
+
const itemRenderer = (item: DocumentSelectorItem) => (
|
|
244
|
+
<a
|
|
245
|
+
className={classNames("p-menuitem-link", styles.pedagoTab)}
|
|
246
|
+
onClick={() => dispatch(setPedagoTab(item.value))}
|
|
247
|
+
>
|
|
248
|
+
<span>{item.name}</span>
|
|
249
|
+
<DocumentSelectorItemActions item={item} />
|
|
250
|
+
</a>
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
const items: DocumentSelectorItem[] = useMemo(() => {
|
|
254
|
+
const allItems: DocumentSelectorItem[] = [];
|
|
255
|
+
if (mode === "create" || instructionsType !== "none") {
|
|
256
|
+
allItems.push({
|
|
257
|
+
name: "Consignes",
|
|
258
|
+
value: "instructions",
|
|
259
|
+
tooltip: "Activer/désactiver les consignes",
|
|
260
|
+
template: (item: any) => itemRenderer(item),
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
/* // Removed PDF for now
|
|
264
|
+
if (mode === "create" || pdfInstructions) {
|
|
265
|
+
allItems.push({
|
|
266
|
+
name: "PDF",
|
|
267
|
+
value: "pdf",
|
|
268
|
+
tooltip: "Activer/désactiver le PDF",
|
|
269
|
+
template: (item: any) => itemRenderer(item),
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
*/
|
|
273
|
+
if (mode === "create" || sharedNotesType !== "none") {
|
|
274
|
+
allItems.push({
|
|
275
|
+
name: "Notes partagées",
|
|
276
|
+
value: "sharedNotes",
|
|
277
|
+
tooltip: "Activer/désactiver les notes partagées",
|
|
278
|
+
template: (item: any) => itemRenderer(item),
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
return allItems;
|
|
282
|
+
}, []);
|
|
283
|
+
const activeIndex = useMemo(() => {
|
|
284
|
+
const index = items.findIndex((item) => item.value === pedagoTab);
|
|
285
|
+
return index;
|
|
286
|
+
}, [items, pedagoTab]);
|
|
287
|
+
|
|
288
|
+
return (
|
|
289
|
+
<TabMenu
|
|
290
|
+
className={styles.pedagoTabMenu}
|
|
291
|
+
model={items}
|
|
292
|
+
activeIndex={activeIndex}
|
|
293
|
+
onTabChange={(e) => {
|
|
294
|
+
dispatch(setPedagoTab(items[e.index].value));
|
|
295
|
+
}}
|
|
296
|
+
/>
|
|
297
|
+
);
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
const ShowHideGradingButton: React.FC = () => {
|
|
301
|
+
const dispatch = useAppDispatch();
|
|
302
|
+
const mode = useAppSelector(selectMode);
|
|
303
|
+
const hasGradingOrComments = useAppSelector(selectHasGradingOrComments);
|
|
304
|
+
const isGradingVisible = useAppSelector(selectIsGradingVisible);
|
|
305
|
+
const canShowGrading = hasGradingOrComments || mode === "review";
|
|
306
|
+
|
|
307
|
+
return (
|
|
308
|
+
<Button
|
|
309
|
+
size="small"
|
|
310
|
+
icon="pi pi-graduation-cap"
|
|
311
|
+
disabled={!canShowGrading}
|
|
312
|
+
severity={canShowGrading ? "info" : "secondary"}
|
|
313
|
+
aria-label="Afficher/Masquer la notation"
|
|
314
|
+
tooltip={canShowGrading ? "Afficher/Masquer la notation" : "Pas de note"}
|
|
315
|
+
tooltipOptions={{
|
|
316
|
+
position: "left",
|
|
317
|
+
showDelay: settings.TOOLTIP_SHOW_DELAY,
|
|
318
|
+
showOnDisabled: true,
|
|
319
|
+
}}
|
|
320
|
+
onClick={() => dispatch(toggleIsGradingVisible())}
|
|
321
|
+
style={{ flexShrink: 0 }}
|
|
322
|
+
outlined={!isGradingVisible || !canShowGrading}
|
|
323
|
+
/>
|
|
324
|
+
);
|
|
325
|
+
};
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { useCapytaleRichTextEditor } from "@capytale/capytale-rich-text-editor";
|
|
2
|
+
import { useAppDispatch, useAppSelector } from "../../app/hooks";
|
|
3
|
+
import {
|
|
4
|
+
selectSharedNotesContent,
|
|
5
|
+
selectMode,
|
|
6
|
+
setCanSaveSharedNotes,
|
|
7
|
+
setIsMPDirty,
|
|
8
|
+
setLexicalSharedNotesState,
|
|
9
|
+
selectWorkflow,
|
|
10
|
+
selectSharedNotesType,
|
|
11
|
+
setSharedNotesType,
|
|
12
|
+
} from "../activityData/activityDataSlice";
|
|
13
|
+
import { forwardRef, useImperativeHandle, useRef, useState } from "react";
|
|
14
|
+
import { Toast } from "primereact/toast";
|
|
15
|
+
import settings from "../../settings";
|
|
16
|
+
import { Button } from "primereact/button";
|
|
17
|
+
import { Dialog } from "primereact/dialog";
|
|
18
|
+
|
|
19
|
+
const SharedNotesEditor: React.FC = forwardRef((_props, ref) => {
|
|
20
|
+
const [Editor, getState, _canSave] = useCapytaleRichTextEditor();
|
|
21
|
+
const dispatch = useAppDispatch();
|
|
22
|
+
const toast = useRef<Toast>(null);
|
|
23
|
+
const mode = useAppSelector(selectMode);
|
|
24
|
+
const workflow = useAppSelector(selectWorkflow);
|
|
25
|
+
const isEditable =
|
|
26
|
+
mode === "create" ||
|
|
27
|
+
(mode === "assignment" && workflow === "current") ||
|
|
28
|
+
mode === "review";
|
|
29
|
+
const initialEditorState = useAppSelector(selectSharedNotesContent);
|
|
30
|
+
const initialStateOnChangeDone = useRef<boolean>(false);
|
|
31
|
+
const sharedNotesType = useAppSelector(selectSharedNotesType);
|
|
32
|
+
|
|
33
|
+
const [helpDialogVisible, setHelpDialogVisible] = useState(false);
|
|
34
|
+
|
|
35
|
+
useImperativeHandle(ref, () => {
|
|
36
|
+
return {
|
|
37
|
+
save: async () => {
|
|
38
|
+
const state = await getState();
|
|
39
|
+
dispatch(setLexicalSharedNotesState(state.json));
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<>
|
|
46
|
+
<Toast position="bottom-right" ref={toast} />
|
|
47
|
+
<>
|
|
48
|
+
{sharedNotesType === "none" && (
|
|
49
|
+
<>
|
|
50
|
+
<p>Pas de notes partagées.</p>
|
|
51
|
+
<Button
|
|
52
|
+
label="Créer des notes partagées"
|
|
53
|
+
onClick={() => dispatch(setSharedNotesType("rich"))}
|
|
54
|
+
/>
|
|
55
|
+
<div style={{ textAlign: "start", padding: "16px" }}>
|
|
56
|
+
<h3>Que sont les notes partagées ?</h3>
|
|
57
|
+
<p>
|
|
58
|
+
Les notes partagées permettent aux élèves de communiquer avec
|
|
59
|
+
leur enseignant. L'enseignant donne le contenu initial des notes
|
|
60
|
+
partagées, et les élèves peuvent le modifier, notamment pour
|
|
61
|
+
répondre à des questions, expliciter une démarche ou faire des
|
|
62
|
+
remarques sur leur travail. L'enseignant peut également modifier
|
|
63
|
+
les notes partagées pour répondre aux élèves.
|
|
64
|
+
</p>
|
|
65
|
+
<p>
|
|
66
|
+
Les notes partagées peuvent contenir du texte, des images, des
|
|
67
|
+
liens, etc., tout comme les consignes. La différence principale
|
|
68
|
+
avec les consignes est que les élèves peuvent modifier le
|
|
69
|
+
contenu des notes partagées. Elles sont un outil de
|
|
70
|
+
communication supplémentaire entre l'enseignant et les élèves.
|
|
71
|
+
</p>
|
|
72
|
+
</div>
|
|
73
|
+
<Button
|
|
74
|
+
severity="secondary"
|
|
75
|
+
size="small"
|
|
76
|
+
icon={"pi pi-question-circle"}
|
|
77
|
+
onClick={() => {
|
|
78
|
+
setHelpDialogVisible(true);
|
|
79
|
+
}}
|
|
80
|
+
outlined
|
|
81
|
+
label="En savoir plus"
|
|
82
|
+
style={{ marginBottom: "16px" }}
|
|
83
|
+
/>
|
|
84
|
+
<Dialog
|
|
85
|
+
id="metaPlayerHelpDialog"
|
|
86
|
+
header="Documentation"
|
|
87
|
+
visible={helpDialogVisible}
|
|
88
|
+
onHide={() => setHelpDialogVisible(false)}
|
|
89
|
+
maximizable={true}
|
|
90
|
+
>
|
|
91
|
+
<iframe
|
|
92
|
+
src={
|
|
93
|
+
"https://capytale2.ac-paris.fr/wiki/doku.php?id=notes_partagees"
|
|
94
|
+
}
|
|
95
|
+
style={{ width: "100%", height: "100%" }}
|
|
96
|
+
title="Documentation"
|
|
97
|
+
/>
|
|
98
|
+
</Dialog>
|
|
99
|
+
</>
|
|
100
|
+
)}
|
|
101
|
+
{sharedNotesType === "rich" && (
|
|
102
|
+
<Editor
|
|
103
|
+
placeholderText={
|
|
104
|
+
mode === "create"
|
|
105
|
+
? "Écrivez le contenu initial des notes partagées ici..."
|
|
106
|
+
: "Les notes partagées sont vides, vous pouvez les remplir ici..."
|
|
107
|
+
}
|
|
108
|
+
htmlInitialContent={initialEditorState ? undefined : ""}
|
|
109
|
+
initialEditorState={initialEditorState || undefined}
|
|
110
|
+
jsonSizeLimit={settings.STATEMENT_MAX_SIZE}
|
|
111
|
+
onChange={(editorState) => {
|
|
112
|
+
dispatch(setLexicalSharedNotesState(editorState));
|
|
113
|
+
if (initialStateOnChangeDone.current) {
|
|
114
|
+
dispatch(setIsMPDirty(true));
|
|
115
|
+
} else {
|
|
116
|
+
initialStateOnChangeDone.current = true;
|
|
117
|
+
}
|
|
118
|
+
}}
|
|
119
|
+
onJsonSizeLimitExceeded={() => {
|
|
120
|
+
dispatch(setCanSaveSharedNotes(false));
|
|
121
|
+
toast.current!.show({
|
|
122
|
+
summary: "Erreur",
|
|
123
|
+
detail: `Le contenu des notes partagées est trop volumineux. Veuillez le réduire avant de l'enregistrer.\nPeut-être avez-vous inséré une image trop volumineuse ?`,
|
|
124
|
+
severity: "error",
|
|
125
|
+
life: 10000,
|
|
126
|
+
});
|
|
127
|
+
}}
|
|
128
|
+
onJsonSizeLimitMet={() => {
|
|
129
|
+
dispatch(setCanSaveSharedNotes(true));
|
|
130
|
+
toast.current!.show({
|
|
131
|
+
summary: "Succès",
|
|
132
|
+
detail: `Le contenu des notes partagées ne dépasse plus la taille limite.`,
|
|
133
|
+
severity: "success",
|
|
134
|
+
life: 4000,
|
|
135
|
+
});
|
|
136
|
+
}}
|
|
137
|
+
isEditable={isEditable}
|
|
138
|
+
/>
|
|
139
|
+
)}
|
|
140
|
+
</>
|
|
141
|
+
</>
|
|
142
|
+
);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
export default SharedNotesEditor;
|