@capytale/meta-player 0.4.1 → 0.4.3

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.3",
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,
@@ -149,6 +149,7 @@ export const activityDataSlice = createAppSlice({
149
149
  setPlayerSettings: create.reducer(
150
150
  (state, action: PayloadAction<MetaPlayerOptions>) => {
151
151
  state.hasInstructions = action.payload.hasInstructions;
152
+ state.canReset = action.payload.canReset;
152
153
  state.pedagoLayout = action.payload.pedagoLayout;
153
154
  state.supportsLightTheme = action.payload.supportsLightTheme;
154
155
  state.supportsDarkTheme = action.payload.supportsDarkTheme;
@@ -239,6 +240,7 @@ export const activityDataSlice = createAppSlice({
239
240
  selectComments: (data) => data.comments,
240
241
  selectGrading: (data) => data.grading,
241
242
  selectHasInstructions: (data) => data.hasInstructions,
243
+ selectCanReset: (data) => data.canReset,
242
244
  selectPedagoLayout: (data) => data.pedagoLayout,
243
245
  selectHasGradingOrComments: (data) => !!(data.grading || data.comments),
244
246
  selectSaveState: (data) => data.saveState,
@@ -310,6 +312,7 @@ export const {
310
312
  selectComments,
311
313
  selectGrading,
312
314
  selectHasInstructions,
315
+ selectCanReset,
313
316
  selectPedagoLayout,
314
317
  selectHasGradingOrComments,
315
318
  selectSaveState,
@@ -1,5 +1,6 @@
1
1
  export type MetaPlayerOptions = {
2
2
  hasInstructions: boolean;
3
+ canReset: boolean;
3
4
  pedagoLayout:
4
5
  | "horizontal"
5
6
  | "vertical"
@@ -11,6 +12,7 @@ export type MetaPlayerOptions = {
11
12
 
12
13
  export const defaultMetaPlayerOptions: MetaPlayerOptions = {
13
14
  hasInstructions: true,
15
+ canReset: true,
14
16
  pedagoLayout: "default-horizontal",
15
17
  supportsLightTheme: true,
16
18
  supportsDarkTheme: false,
@@ -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,
@@ -1,5 +1,6 @@
1
1
  import { useAppDispatch, useAppSelector } from "../../app/hooks";
2
2
  import {
3
+ selectCanReset,
3
4
  selectMode,
4
5
  selectWorkflow,
5
6
  setSaveState,
@@ -13,6 +14,7 @@ type UseResetProps = {
13
14
 
14
15
  export const useReset = (props: UseResetProps) => {
15
16
  const dispatch = useAppDispatch();
17
+ const canReset = useAppSelector(selectCanReset);
16
18
  const beforeReset = useAppSelector(selectBeforeReset);
17
19
  const afterReset = useAppSelector(selectAfterReset);
18
20
  const activityJs = useActivityJS();
@@ -21,6 +23,9 @@ export const useReset = (props: UseResetProps) => {
21
23
  if (mode !== "assignment" || workflow !== "current") {
22
24
  return null;
23
25
  }
26
+ if (!canReset) {
27
+ return null;
28
+ }
24
29
  return async (reload: boolean = true) => {
25
30
  if (!activityJs.activitySession) {
26
31
  throw new Error("No activity session to reset");
@@ -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;
@@ -34,6 +34,28 @@ export const PedagoCommands = () => {
34
34
  );
35
35
  };
36
36
 
37
+ export const CloseOnlyPedagoCommands: FC = () => {
38
+ const dispatch = useAppDispatch();
39
+ return (
40
+ <div className={styles.closeOnlyPedagoCommands}>
41
+ <Button
42
+ severity="secondary"
43
+ icon="pi pi-times"
44
+ rounded
45
+ text
46
+ aria-label="Masquer les consignes"
47
+ tooltip="Masquer les consignes"
48
+ tooltipOptions={{
49
+ position: "left",
50
+ showDelay: settings.TOOLTIP_SHOW_DELAY,
51
+ }}
52
+ onClick={() => dispatch(toggleIsPedagoVisible())}
53
+ style={{ flexShrink: 0 }}
54
+ />
55
+ </div>
56
+ );
57
+ };
58
+
37
59
  type DocumentSelectorHzItem = {
38
60
  name: string;
39
61
  value: PedagoTab;
@@ -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
- import { PedagoCommands } from "./PedagoCommands";
27
+ import { CloseOnlyPedagoCommands, 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,16 @@ 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.onlyGradingPedago}>
46
+ <CloseOnlyPedagoCommands />
47
+ <div className={styles.gradingPanel}>
48
+ <Grading />
49
+ </div>
50
+ </div>
51
+ );
52
+ }
57
53
 
58
54
  return (
59
55
  // @ts-ignore - Incompatibility for props in TS
@@ -82,56 +78,7 @@ const Pedago: React.FC<DivProps> = ({ className, ...props }) => {
82
78
  size={40}
83
79
  className={styles.gradingPanel}
84
80
  >
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>
81
+ <Grading />
135
82
  </SplitterPanel>,
136
83
  <SplitterPanel
137
84
  key="pedagoPanel"
@@ -162,4 +109,74 @@ const Pedago: React.FC<DivProps> = ({ className, ...props }) => {
162
109
  );
163
110
  };
164
111
 
112
+ const Grading: FC = () => {
113
+ const mode = useAppSelector(selectMode);
114
+ const comments = useAppSelector(selectComments);
115
+ const grading = useAppSelector(selectGrading);
116
+ const dispatch = useAppDispatch();
117
+
118
+ const handleCommentsChange: ChangeEventHandler<HTMLTextAreaElement> = (
119
+ event,
120
+ ) => {
121
+ dispatch(setComments(event.target.value));
122
+ dispatch(setIsMPDirty(true));
123
+ };
124
+
125
+ const handleGradingChange: ChangeEventHandler<HTMLTextAreaElement> = (
126
+ event,
127
+ ) => {
128
+ dispatch(setGrading(event.target.value));
129
+ dispatch(setIsMPDirty(true));
130
+ };
131
+
132
+ return (
133
+ <>
134
+ <Panel
135
+ className={classNames(styles.fullSizePanel, styles.pedagoFeedbackPanel)}
136
+ header="Appréciation"
137
+ >
138
+ <div className={styles.pedagoFeedback}>
139
+ {(comments || mode !== "assignment") && (
140
+ <textarea
141
+ value={comments || ""}
142
+ placeholder={
143
+ mode !== "review" ? "" : "Rédigez ici l'appréciation."
144
+ }
145
+ id="comments"
146
+ name="comments"
147
+ rows={2}
148
+ readOnly={mode !== "review"}
149
+ onChange={handleCommentsChange}
150
+ className={styles.fullTextarea}
151
+ />
152
+ )}
153
+ </div>
154
+ </Panel>
155
+ <Panel
156
+ className={classNames(styles.fullSizePanel, styles.pedagoGradePanel)}
157
+ header="Évaluation"
158
+ >
159
+ <div className={styles.pedagoGrade}>
160
+ {(grading || mode !== "assignment") && (
161
+ <textarea
162
+ value={grading || ""}
163
+ placeholder={
164
+ mode !== "review"
165
+ ? ""
166
+ : "Écrivez ici l'évaluation libre (chiffrée ou non)."
167
+ }
168
+ id="grading"
169
+ name="grading"
170
+ rows={1}
171
+ readOnly={mode !== "review"}
172
+ onChange={handleGradingChange}
173
+ className={styles.fullTextarea}
174
+ />
175
+ )}
176
+ </div>
177
+ </Panel>
178
+ </>
179
+ );
180
+ };
181
+
165
182
  export default Pedago;
@@ -1,3 +1,14 @@
1
+ .onlyGradingPedago {
2
+ display: flex;
3
+ width: 100%;
4
+ :global(.layout-horizontal) & {
5
+ flex-direction: row-reverse;
6
+ }
7
+ :global(.layout-vertical) & {
8
+ flex-direction: column;
9
+ }
10
+ }
11
+
1
12
  .pedago {
2
13
  display: flex;
3
14
  height: 100%;
@@ -10,6 +21,18 @@
10
21
  }
11
22
  }
12
23
 
24
+ .closeOnlyPedagoCommands {
25
+ flex-direction: column;
26
+ flex-shrink: 0;
27
+ display: flex;
28
+ background-color: var(--surface-200);
29
+ :global(.layout-vertical) & {
30
+ gap: 8px;
31
+ flex-direction: row-reverse;
32
+ align-items: center;
33
+ }
34
+ }
35
+
13
36
  .pedagoCommands {
14
37
  flex-direction: column;
15
38
  flex-shrink: 0;
@@ -129,6 +152,7 @@
129
152
  .gradingPanel {
130
153
  display: flex;
131
154
  gap: 4px;
155
+ width: 100%;
132
156
  }
133
157
 
134
158
  *: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
+ };