@capytale/meta-player 0.4.1 → 0.4.2

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capytale/meta-player",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "dev": "vite",
@@ -16,7 +16,7 @@
16
16
  }
17
17
  },
18
18
  "dependencies": {
19
- "@capytale/activity.js": "^3.1.8",
19
+ "@capytale/activity.js": "^3.1.10",
20
20
  "@capytale/capytale-anti-triche": "^0.2.1",
21
21
  "@capytale/capytale-rich-text-editor": "^0.4.3",
22
22
  "@reduxjs/toolkit": "^2.0.1",
package/src/App.tsx CHANGED
@@ -34,7 +34,7 @@ const App: FC<AppProps> = (props) => {
34
34
  const isPedagoVisible = useAppSelector(selectIsPedagoVisible) as boolean;
35
35
  const hasInstructions = useAppSelector(selectHasInstructions);
36
36
  const hasGradingOrComments = useAppSelector(selectHasGradingOrComments);
37
- const hasPedago = hasInstructions || hasGradingOrComments;
37
+ const hasPedago = hasInstructions || hasGradingOrComments || mode === "review";
38
38
  const dispatch = useAppDispatch();
39
39
  const showPedago = hasPedago && isPedagoVisible;
40
40
  const isDirty = useAppSelector(selectIsDirty);
@@ -52,6 +52,12 @@ const App: FC<AppProps> = (props) => {
52
52
  [isDirty, save],
53
53
  );
54
54
 
55
+ const pedagoOpenLabel = hasPedago
56
+ ? "Afficher les consignes"
57
+ : mode === "create"
58
+ ? "Ce type d'activité n'accepte pas de consigne"
59
+ : "Pas de consignes ni de note";
60
+
55
61
  return (
56
62
  <div
57
63
  className={classNames(
@@ -80,16 +86,8 @@ const App: FC<AppProps> = (props) => {
80
86
  onClick={
81
87
  hasPedago ? () => dispatch(toggleIsPedagoVisible()) : undefined
82
88
  }
83
- data-pr-tooltip={
84
- hasPedago
85
- ? "Afficher les consignes"
86
- : "Pas de consignes ni de note"
87
- }
88
- aria-label={
89
- hasPedago
90
- ? "Afficher les consignes"
91
- : "Pas de consignes ni de note"
92
- }
89
+ data-pr-tooltip={pedagoOpenLabel}
90
+ aria-label={pedagoOpenLabel}
93
91
  role={hasPedago ? "button" : "note"}
94
92
  >
95
93
  <i
@@ -33,22 +33,17 @@ import {
33
33
 
34
34
  import { initialState as layoutInitialState } from "./features/layout/layoutSlice";
35
35
  import ExitWarning from "./features/activityData/ExitWarning";
36
-
37
- type AntiCheatOptions = {
38
- preserveDom: boolean;
39
- hasIframes: boolean;
40
- };
41
-
42
- type UIOptions = {
43
- closePedagoByDefault: boolean;
44
- noWorkflow: boolean;
45
- noSaveForStudents: boolean;
46
- };
36
+ import type { AntiCheatOptions, UIOptions } from "./types";
37
+ import {
38
+ AttachedFilesOptions,
39
+ defaultAttachedFilesOptions,
40
+ } from "./features/functionalities/functionalitiesSlice";
47
41
 
48
42
  type MetaPlayerProps = PropsWithChildren<{
49
43
  activityJSOptions?: LoadOptions;
50
44
  antiCheatOptions?: Partial<AntiCheatOptions>;
51
45
  uiOptions?: Partial<UIOptions>;
46
+ attachedFilesOptions?: AttachedFilesOptions;
52
47
  }>;
53
48
 
54
49
  const MetaPlayer: FC<MetaPlayerProps> = (props) => {
@@ -66,6 +61,10 @@ const MetaPlayer: FC<MetaPlayerProps> = (props) => {
66
61
  noSaveForStudents: false,
67
62
  ...props.uiOptions,
68
63
  };
64
+ const attachedFilesOptions: AttachedFilesOptions = {
65
+ ...defaultAttachedFilesOptions,
66
+ ...props.attachedFilesOptions,
67
+ };
69
68
  const store = useMemo(
70
69
  () =>
71
70
  makeStore({
@@ -75,6 +74,9 @@ const MetaPlayer: FC<MetaPlayerProps> = (props) => {
75
74
  showWorkflow: !uiOptions.noWorkflow,
76
75
  showSaveForStudents: !uiOptions.noSaveForStudents,
77
76
  },
77
+ functionalities: {
78
+ attachedFilesOptions,
79
+ },
78
80
  }),
79
81
  [],
80
82
  );
@@ -0,0 +1,5 @@
1
+ import evaluationApi from "@capytale/activity.js/backend/capytale/evaluation";
2
+ import type { Evaluation } from "@capytale/activity.js/activity/evaluation/evaluation";
3
+
4
+ export { evaluationApi };
5
+ export type { Evaluation };
package/src/app/store.ts CHANGED
@@ -7,6 +7,7 @@ import { activityDataSlice } from "../features/activityData/activityDataSlice";
7
7
  import { navbarSlice } from "../features/navbar/navbarSlice";
8
8
  import { saverSlice } from "../features/activityJS/saverSlice";
9
9
  import { activitySettingsSlice } from "../features/activitySettings/activitySettingsSlice";
10
+ import { functionalitiesSlice } from "../features/functionalities/functionalitiesSlice";
10
11
 
11
12
  // `combineSlices` automatically combines the reducers using
12
13
  // their `reducerPath`s, therefore we no longer need to call `combineReducers`.
@@ -17,6 +18,7 @@ const rootReducer = combineSlices(
17
18
  navbarSlice,
18
19
  saverSlice,
19
20
  activitySettingsSlice,
21
+ functionalitiesSlice,
20
22
  );
21
23
  // Infer the `RootState` type from the root reducer
22
24
  export type RootState = ReturnType<typeof rootReducer>;
@@ -27,6 +29,10 @@ export const makeStore = (preloadedState?: Partial<RootState>) => {
27
29
  const store = configureStore({
28
30
  reducer: rootReducer,
29
31
  preloadedState,
32
+ middleware: (getDefaultMiddleware) =>
33
+ getDefaultMiddleware({
34
+ serializableCheck: false
35
+ }),
30
36
  });
31
37
  // configure listeners using the provided defaults
32
38
  // optional, but required for `refetchOnFocus`/`refetchOnReconnect` behaviors
@@ -33,7 +33,7 @@ interface ActivityJSData {
33
33
  mode: ActivityMode;
34
34
  returnUrl: string;
35
35
  helpUrl: string;
36
- code: string;
36
+ code: string | null;
37
37
  nid: number;
38
38
  activityNid: number;
39
39
  accessTrMode: string;
@@ -44,7 +44,7 @@ interface ActivityJSData {
44
44
  pdfInstructions: Blob | null;
45
45
  sharedNotesContent: InitialEditorStateType | null;
46
46
  sharedNotesType: EditorType;
47
- codeLink: string;
47
+ codeLink: string | null;
48
48
  icon: Icon | null;
49
49
  friendlyType: string;
50
50
  comments: string | null;
@@ -77,7 +77,7 @@ const initialState: ActivityDataState = {
77
77
  mode: "view",
78
78
  returnUrl: "",
79
79
  helpUrl: "",
80
- code: "",
80
+ code: null,
81
81
  nid: 0,
82
82
  activityNid: 0,
83
83
  accessTrMode: "",
@@ -92,7 +92,7 @@ const initialState: ActivityDataState = {
92
92
  pdfInstructions: null,
93
93
  sharedNotesContent: null,
94
94
  sharedNotesType: "none",
95
- codeLink: "",
95
+ codeLink: null,
96
96
  icon: null,
97
97
  friendlyType: "",
98
98
  comments: null,
@@ -55,7 +55,8 @@ export const ActivityJSProvider: FC<ActivityJSProviderProps> = (props) => {
55
55
  helpUrl: data.type.helpUrl,
56
56
  nid: ab.mainNode.nid,
57
57
  activityNid: ab.activityNode.nid,
58
- code: ab.code.value || "",
58
+ code: data.code,
59
+ codeLink: data.codeLink,
59
60
  accessTrMode: ab.access_tr_mode.value || "",
60
61
  accessTimerange: ab.access_timerange.value,
61
62
  studentInfo: data.student
@@ -85,7 +86,6 @@ export const ActivityJSProvider: FC<ActivityJSProviderProps> = (props) => {
85
86
  pdfInstructions: null,
86
87
  sharedNotesContent: sharedNotesContent,
87
88
  sharedNotesType: sharedNotesContent == null ? "none" : "rich",
88
- codeLink: data.codeLink || "",
89
89
  icon: data.icon || null,
90
90
  friendlyType: data.friendlyType,
91
91
  comments: ab.assignmentNode?.comments.value || null,
@@ -0,0 +1,63 @@
1
+ import type { PayloadAction } from "@reduxjs/toolkit";
2
+ import { createAppSlice } from "../../app/createAppSlice";
3
+
4
+ export type AttachedFileData = {
5
+ name: string;
6
+ isTemporary: boolean;
7
+ interactionMode: "download" | "preview" | "preview-or-download" | "custom";
8
+ urlOrId: string;
9
+ }
10
+
11
+ export type AttachedFilesOptions = {
12
+ enabled: boolean;
13
+ uploadTemporaryFiles?: {
14
+ mimeTypes: string[];
15
+ handler: (files: File[]) => any;
16
+ };
17
+ middleware?: {
18
+ listFiles: (originalFiles: AttachedFileData[]) => AttachedFileData[];
19
+ handleFileClick?: ((originalFile: AttachedFileData) => any);
20
+ };
21
+ };
22
+
23
+ export interface FunctionalitiesState {
24
+ attachedFilesOptions: AttachedFilesOptions;
25
+ }
26
+
27
+ export const defaultAttachedFilesOptions: AttachedFilesOptions = {
28
+ enabled: false,
29
+ };
30
+
31
+ export const initialState: FunctionalitiesState = {
32
+ attachedFilesOptions: defaultAttachedFilesOptions
33
+ };
34
+
35
+ // If you are not using async thunks you can use the standalone `createSlice`.
36
+ export const functionalitiesSlice = createAppSlice({
37
+ name: "functionalities",
38
+ // `createSlice` will infer the state type from the `initialState` argument
39
+ initialState,
40
+ // The `reducers` field lets us define reducers and generate associated actions
41
+ reducers: (create) => ({
42
+ setAttachedFilesOptions: create.reducer((state, action: PayloadAction<AttachedFilesOptions>) => {
43
+ state.attachedFilesOptions = action.payload;
44
+ }),
45
+ }),
46
+ // You can define your selectors here. These selectors receive the slice
47
+ // state as their first argument.
48
+ selectors: {
49
+ selectAttachedFilesOptions: (state) => state.attachedFilesOptions,
50
+ selectAttachedFilesEnabled: (state) => state.attachedFilesOptions.enabled,
51
+ },
52
+ });
53
+
54
+ // Action creators are generated for each case reducer function.
55
+ export const {
56
+ setAttachedFilesOptions,
57
+ } = functionalitiesSlice.actions;
58
+
59
+ // Selectors returned by `slice.selectors` take the root state as their first argument.
60
+ export const {
61
+ selectAttachedFilesOptions,
62
+ selectAttachedFilesEnabled,
63
+ } = functionalitiesSlice.selectors;
@@ -0,0 +1,36 @@
1
+ import { useMemo } from "react";
2
+ import { useAppSelector } from "../../app/hooks"
3
+ import { useActivityJS } from "../activityJS/ActivityJSProvider";
4
+ import { AttachedFileData, selectAttachedFilesOptions } from "./functionalitiesSlice"
5
+
6
+ export const useAttachedFiles = () => {
7
+ const attachedFilesOptions = useAppSelector(selectAttachedFilesOptions);
8
+
9
+ const activityJS = useActivityJS();
10
+ const files = useMemo(
11
+ () =>
12
+ activityJS.activitySession?.activityBunch.activityNode.attached_files
13
+ .items || [],
14
+ [activityJS],
15
+ );
16
+ const filesData = useMemo<AttachedFileData[]>(() => {
17
+ return files.map((file) => {
18
+ const name = decodeURIComponent(file.split("/").pop() || "");
19
+ return {
20
+ name: name,
21
+ urlOrId: file,
22
+ isTemporary: false,
23
+ interactionMode: "download",
24
+ };
25
+ });
26
+ }, [files]);
27
+
28
+ const treatedFilesData = useMemo(() => {
29
+ if (!attachedFilesOptions.enabled || !attachedFilesOptions.middleware?.listFiles) {
30
+ return filesData;
31
+ }
32
+ return attachedFilesOptions.middleware.listFiles(filesData);
33
+ }, [filesData, attachedFilesOptions]);
34
+
35
+ return treatedFilesData;
36
+ }
@@ -3,7 +3,7 @@ import styles from "./style.module.scss";
3
3
 
4
4
  import { Button } from "primereact/button";
5
5
  import { Sidebar } from "primereact/sidebar";
6
- import SidebarContent from "./SidebarContent";
6
+ import SettingsSidebarContent from "./SettingsSidebarContent";
7
7
  import settings from "../../settings";
8
8
  import ActivityQuickActions from "./ActivityQuickActions";
9
9
  import useFullscreen from "../../utils/useFullscreen";
@@ -12,9 +12,15 @@ import { Dialog } from "primereact/dialog";
12
12
  import capytaleUI from "@capytale/activity.js/backend/capytale/ui";
13
13
  import { useAppSelector } from "../../app/hooks";
14
14
  import { selectHasAntiCheat } from "../activityData/activityDataSlice";
15
+ import AttachedFilesSidebarContent from "./AttachedFilesSidebarContent";
16
+ import { Badge } from "primereact/badge";
17
+ import { useAttachedFiles } from "../functionalities/hooks";
18
+ import { selectAttachedFilesEnabled } from "../functionalities/functionalitiesSlice";
15
19
 
16
20
  const ActivityMenu: React.FC = memo(() => {
17
21
  const [settingsSidebarVisible, setSettingsSidebarVisible] = useState(false);
22
+ const [attachedFilesSidebarVisible, setAttachedFilesSidebarVisible] =
23
+ useState(false);
18
24
  const [wantFullscreen, setWantFullscreen] = useState(false);
19
25
  const isFullscreen = useFullscreen(wantFullscreen, () =>
20
26
  setWantFullscreen(false),
@@ -23,9 +29,32 @@ const ActivityMenu: React.FC = memo(() => {
23
29
  const [helpDialogVisible, setHelpDialogVisible] = useState(false);
24
30
  const activityJS = useActivityJS();
25
31
  const hasAntiCheat = useAppSelector(selectHasAntiCheat);
32
+ const attachedFiles = useAttachedFiles();
33
+ const attachedFilesEnabled = useAppSelector(selectAttachedFilesEnabled);
26
34
  return (
27
35
  <div className={styles.activityMenu}>
28
36
  <ActivityQuickActions />
37
+ {attachedFilesEnabled && (
38
+ <Button
39
+ severity="secondary"
40
+ size="small"
41
+ icon={
42
+ <i className="pi pi-paperclip p-overlay-badge">
43
+ {attachedFiles.length > 0 && (
44
+ <Badge value={attachedFiles.length}></Badge>
45
+ )}
46
+ </i>
47
+ }
48
+ outlined
49
+ tooltip="Fichiers joints"
50
+ tooltipOptions={{
51
+ position: "left",
52
+ showDelay: settings.TOOLTIP_SHOW_DELAY,
53
+ }}
54
+ className="p-overlay-badge"
55
+ onClick={() => setAttachedFilesSidebarVisible(true)}
56
+ />
57
+ )}
29
58
  <Button
30
59
  severity="secondary"
31
60
  size="small"
@@ -72,7 +101,7 @@ const ActivityMenu: React.FC = memo(() => {
72
101
  position="right"
73
102
  onHide={() => setSettingsSidebarVisible(false)}
74
103
  >
75
- <SidebarContent
104
+ <SettingsSidebarContent
76
105
  showHelp={
77
106
  activityJS.activitySession?.type.helpUrl
78
107
  ? () => {
@@ -83,6 +112,14 @@ const ActivityMenu: React.FC = memo(() => {
83
112
  }
84
113
  />
85
114
  </Sidebar>
115
+ <Sidebar
116
+ header="Fichiers joints"
117
+ visible={attachedFilesSidebarVisible}
118
+ position="right"
119
+ onHide={() => setAttachedFilesSidebarVisible(false)}
120
+ >
121
+ <AttachedFilesSidebarContent />
122
+ </Sidebar>
86
123
  {activityJS.activitySession?.type.helpUrl && (
87
124
  <Dialog
88
125
  id="metaPlayerHelpDialog"
@@ -0,0 +1,44 @@
1
+ import { FC } from "react";
2
+ import { useAttachedFiles } from "../functionalities/hooks";
3
+ import { useAppSelector } from "../../app/hooks";
4
+ import { selectAttachedFilesOptions } from "../functionalities/functionalitiesSlice";
5
+
6
+ const AttachedFilesSidebarContent: FC = () => {
7
+ const filesData = useAttachedFiles();
8
+ const attachedFilesOptions = useAppSelector(selectAttachedFilesOptions);
9
+
10
+ return (
11
+ <div>
12
+ {filesData.map((file) => (
13
+ <p key={file.urlOrId}>
14
+ <a
15
+ href={file.urlOrId}
16
+ target="_blank"
17
+ rel="noreferrer"
18
+ download={file.name}
19
+ style={{
20
+ color: file.isTemporary ? "rgb(200, 0, 0)" : "rgb(0, 50, 200)",
21
+ textDecoration:
22
+ file.interactionMode === "download" ? "underline" : "none",
23
+ }}
24
+ onClick={(e) => {
25
+ if (file.interactionMode !== "download") {
26
+ e.preventDefault();
27
+ if (
28
+ file.interactionMode === "custom" &&
29
+ attachedFilesOptions.middleware?.handleFileClick
30
+ ) {
31
+ attachedFilesOptions.middleware.handleFileClick(file);
32
+ }
33
+ }
34
+ }}
35
+ >
36
+ {file.name}
37
+ </a>
38
+ </p>
39
+ ))}
40
+ </div>
41
+ );
42
+ };
43
+
44
+ export default AttachedFilesSidebarContent;
@@ -115,7 +115,7 @@ const CapytaleMenu: React.FC = () => {
115
115
  }} */
116
116
  }
117
117
 
118
- {mode === "create" && (
118
+ {mode === "create" && sharingInfo.code && (
119
119
  <SplitButton
120
120
  label={isLarge ? sharingInfo.code : undefined}
121
121
  severity="secondary"
@@ -130,7 +130,7 @@ const CapytaleMenu: React.FC = () => {
130
130
  },
131
131
  }}
132
132
  onClick={async () => {
133
- await copyToClipboard(sharingInfo.code);
133
+ await copyToClipboard(sharingInfo.code!);
134
134
  toast.current!.show({
135
135
  summary: "Code copié",
136
136
  detail: "Le code de partage a été copié dans le presse-papier.",
@@ -143,7 +143,7 @@ const CapytaleMenu: React.FC = () => {
143
143
  label: "Copier l'URL de partage",
144
144
  icon: "pi pi-link",
145
145
  command: () => {
146
- copyToClipboard(sharingInfo.codeLink).then(() => {
146
+ copyToClipboard(sharingInfo.codeLink!).then(() => {
147
147
  toast.current!.show({
148
148
  summary: "URL copiée",
149
149
  detail:
@@ -25,11 +25,11 @@ import { ConfirmPopup, confirmPopup } from "primereact/confirmpopup";
25
25
  import settings from "../../settings";
26
26
  import { useReset } from "../activityJS/hooks";
27
27
 
28
- type SidebarContentProps = {
28
+ type SettingsSidebarContentProps = {
29
29
  showHelp?: () => void;
30
30
  };
31
31
 
32
- const SidebarContent: FC<SidebarContentProps> = (props) => {
32
+ const SettingsSidebarContent: FC<SettingsSidebarContentProps> = (props) => {
33
33
  const canChooseOrientation = useAppSelector(selectCanChoosePedagoLayout);
34
34
  const canChooseTheme = useAppSelector(selectCanChooseTheme);
35
35
  const orientation = useAppSelector(selectOrientation);
@@ -182,4 +182,4 @@ const SidebarContent: FC<SidebarContentProps> = (props) => {
182
182
  );
183
183
  };
184
184
 
185
- export default SidebarContent;
185
+ export default SettingsSidebarContent;
@@ -15,23 +15,22 @@ import {
15
15
  selectComments,
16
16
  selectGrading,
17
17
  selectHasGradingOrComments,
18
+ selectHasInstructions,
18
19
  selectMode,
19
20
  setComments,
20
21
  setGrading,
21
22
  setIsMPDirty,
22
23
  } from "../activityData/activityDataSlice";
23
- import { ChangeEventHandler } from "react";
24
+ import { ChangeEventHandler, FC } from "react";
24
25
  import { DivProps } from "react-html-props";
25
26
  import SharedNotesEditor from "./SharedNotesEditor";
26
27
  import { PedagoCommands } from "./PedagoCommands";
27
28
  import { PdfEditor } from "./PdfEditor";
28
29
 
29
30
  const Pedago: React.FC<DivProps> = ({ className, ...props }) => {
30
- const dispatch = useAppDispatch();
31
31
  const mode = useAppSelector(selectMode);
32
- const comments = useAppSelector(selectComments);
33
- const grading = useAppSelector(selectGrading);
34
32
  const pedagoTab = useAppSelector(selectPedagoTab);
33
+ const hasInstructions = useAppSelector(selectHasInstructions);
35
34
  const hasGradingOrComments = useAppSelector(selectHasGradingOrComments);
36
35
  const isGradingVisible =
37
36
  useAppSelector(selectIsGradingVisible) &&
@@ -41,19 +40,13 @@ const Pedago: React.FC<DivProps> = ({ className, ...props }) => {
41
40
  ? (tab: Array<any>) => tab.toReversed()
42
41
  : (tab: Array<any>) => tab;
43
42
 
44
- const handleCommentsChange: ChangeEventHandler<HTMLTextAreaElement> = (
45
- event,
46
- ) => {
47
- dispatch(setComments(event.target.value));
48
- dispatch(setIsMPDirty(true));
49
- };
50
-
51
- const handleGradingChange: ChangeEventHandler<HTMLTextAreaElement> = (
52
- event,
53
- ) => {
54
- dispatch(setGrading(event.target.value));
55
- dispatch(setIsMPDirty(true));
56
- };
43
+ if (!hasInstructions) {
44
+ return (
45
+ <div className={styles.gradingPanel}>
46
+ <Grading />
47
+ </div>
48
+ );
49
+ }
57
50
 
58
51
  return (
59
52
  // @ts-ignore - Incompatibility for props in TS
@@ -82,56 +75,7 @@ const Pedago: React.FC<DivProps> = ({ className, ...props }) => {
82
75
  size={40}
83
76
  className={styles.gradingPanel}
84
77
  >
85
- <Panel
86
- className={classNames(
87
- styles.fullSizePanel,
88
- styles.pedagoFeedbackPanel,
89
- )}
90
- header="Appréciation"
91
- >
92
- <div className={styles.pedagoFeedback}>
93
- {(comments || mode !== "assignment") && (
94
- <textarea
95
- value={comments || ""}
96
- placeholder={
97
- mode !== "review" ? "" : "Rédigez ici l'appréciation."
98
- }
99
- id="comments"
100
- name="comments"
101
- rows={2}
102
- readOnly={mode !== "review"}
103
- onChange={handleCommentsChange}
104
- className={styles.fullTextarea}
105
- />
106
- )}
107
- </div>
108
- </Panel>
109
- <Panel
110
- className={classNames(
111
- styles.fullSizePanel,
112
- styles.pedagoGradePanel,
113
- )}
114
- header="Évaluation"
115
- >
116
- <div className={styles.pedagoGrade}>
117
- {(grading || mode !== "assignment") && (
118
- <textarea
119
- value={grading || ""}
120
- placeholder={
121
- mode !== "review"
122
- ? ""
123
- : "Écrivez ici l'évaluation libre (chiffrée ou non)."
124
- }
125
- id="grading"
126
- name="grading"
127
- rows={1}
128
- readOnly={mode !== "review"}
129
- onChange={handleGradingChange}
130
- className={styles.fullTextarea}
131
- />
132
- )}
133
- </div>
134
- </Panel>
78
+ <Grading />
135
79
  </SplitterPanel>,
136
80
  <SplitterPanel
137
81
  key="pedagoPanel"
@@ -162,4 +106,74 @@ const Pedago: React.FC<DivProps> = ({ className, ...props }) => {
162
106
  );
163
107
  };
164
108
 
109
+ const Grading: FC = () => {
110
+ const mode = useAppSelector(selectMode);
111
+ const comments = useAppSelector(selectComments);
112
+ const grading = useAppSelector(selectGrading);
113
+ const dispatch = useAppDispatch();
114
+
115
+ const handleCommentsChange: ChangeEventHandler<HTMLTextAreaElement> = (
116
+ event,
117
+ ) => {
118
+ dispatch(setComments(event.target.value));
119
+ dispatch(setIsMPDirty(true));
120
+ };
121
+
122
+ const handleGradingChange: ChangeEventHandler<HTMLTextAreaElement> = (
123
+ event,
124
+ ) => {
125
+ dispatch(setGrading(event.target.value));
126
+ dispatch(setIsMPDirty(true));
127
+ };
128
+
129
+ return (
130
+ <>
131
+ <Panel
132
+ className={classNames(styles.fullSizePanel, styles.pedagoFeedbackPanel)}
133
+ header="Appréciation"
134
+ >
135
+ <div className={styles.pedagoFeedback}>
136
+ {(comments || mode !== "assignment") && (
137
+ <textarea
138
+ value={comments || ""}
139
+ placeholder={
140
+ mode !== "review" ? "" : "Rédigez ici l'appréciation."
141
+ }
142
+ id="comments"
143
+ name="comments"
144
+ rows={2}
145
+ readOnly={mode !== "review"}
146
+ onChange={handleCommentsChange}
147
+ className={styles.fullTextarea}
148
+ />
149
+ )}
150
+ </div>
151
+ </Panel>
152
+ <Panel
153
+ className={classNames(styles.fullSizePanel, styles.pedagoGradePanel)}
154
+ header="Évaluation"
155
+ >
156
+ <div className={styles.pedagoGrade}>
157
+ {(grading || mode !== "assignment") && (
158
+ <textarea
159
+ value={grading || ""}
160
+ placeholder={
161
+ mode !== "review"
162
+ ? ""
163
+ : "Écrivez ici l'évaluation libre (chiffrée ou non)."
164
+ }
165
+ id="grading"
166
+ name="grading"
167
+ rows={1}
168
+ readOnly={mode !== "review"}
169
+ onChange={handleGradingChange}
170
+ className={styles.fullTextarea}
171
+ />
172
+ )}
173
+ </div>
174
+ </Panel>
175
+ </>
176
+ );
177
+ };
178
+
165
179
  export default Pedago;
@@ -129,6 +129,7 @@
129
129
  .gradingPanel {
130
130
  display: flex;
131
131
  gap: 4px;
132
+ width: 100%;
132
133
  }
133
134
 
134
135
  *:has(> .fullSizePanel) {
package/src/types.ts ADDED
@@ -0,0 +1,10 @@
1
+ export type AntiCheatOptions = {
2
+ preserveDom: boolean;
3
+ hasIframes: boolean;
4
+ };
5
+
6
+ export type UIOptions = {
7
+ closePedagoByDefault: boolean;
8
+ noWorkflow: boolean;
9
+ noSaveForStudents: boolean;
10
+ };