@capytale/meta-player 0.4.0 → 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,16 +1,13 @@
1
1
  {
2
2
  "name": "@capytale/meta-player",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "dev": "vite",
7
7
  "start": "vite",
8
8
  "build": "tsc && vite build",
9
9
  "preview": "vite preview",
10
- "test": "vitest run",
11
10
  "format": "prettier --write .",
12
- "lint": "eslint .",
13
- "lint:fix": "eslint --fix .",
14
11
  "type-check": "tsc --noEmit"
15
12
  },
16
13
  "overrides": {
@@ -19,7 +16,7 @@
19
16
  }
20
17
  },
21
18
  "dependencies": {
22
- "@capytale/activity.js": "^3.1.7",
19
+ "@capytale/activity.js": "^3.1.10",
23
20
  "@capytale/capytale-anti-triche": "^0.2.1",
24
21
  "@capytale/capytale-rich-text-editor": "^0.4.3",
25
22
  "@reduxjs/toolkit": "^2.0.1",
@@ -39,20 +36,14 @@
39
36
  "@testing-library/user-event": "^14.5.2",
40
37
  "@types/react": "^18.3.8",
41
38
  "@types/react-dom": "^18.3.0",
42
- "@vitejs/plugin-react": "^4.3.1",
43
- "eslint": "^8.56.0",
44
- "eslint-config-prettier": "^9.1.0",
45
- "eslint-config-react-app": "^7.0.1",
46
- "eslint-plugin-prettier": "^5.1.3",
47
- "eslint-plugin-react-hooks": "^4.6.2",
39
+ "@vitejs/plugin-react": "^4.3.4",
48
40
  "jsdom": "^23.2.0",
49
41
  "prettier": "^3.2.1",
50
42
  "react": "^18.3.1",
51
43
  "react-dom": "^18.3.1",
52
44
  "sass": "^1.75.0",
53
45
  "typescript": "^5.3.3",
54
- "vite": "^5.4.6",
55
- "vitest": "^1.2.0"
46
+ "vite": "^6.0.2"
56
47
  },
57
48
  "peerDependencies": {
58
49
  "react": "^18.3.1",
package/src/App.tsx CHANGED
@@ -13,15 +13,17 @@ import {
13
13
  selectOrientation,
14
14
  toggleIsPedagoVisible,
15
15
  } from "./features/layout/layoutSlice";
16
- import { FC, PropsWithChildren } from "react";
16
+ import { FC, KeyboardEvent, PropsWithChildren, useCallback } from "react";
17
17
  import { Tooltip } from "primereact/tooltip";
18
18
  import {
19
19
  selectHasGradingOrComments,
20
20
  selectHasInstructions,
21
+ selectIsDirty,
21
22
  selectMode,
22
23
  } from "./features/activityData/activityDataSlice";
23
24
  import settings from "./settings";
24
25
  import ReviewNavbar from "./features/navbar/ReviewNavbar";
26
+ import { useSave } from "./features/activityData/hooks";
25
27
 
26
28
  type AppProps = PropsWithChildren<{}>;
27
29
 
@@ -32,9 +34,29 @@ const App: FC<AppProps> = (props) => {
32
34
  const isPedagoVisible = useAppSelector(selectIsPedagoVisible) as boolean;
33
35
  const hasInstructions = useAppSelector(selectHasInstructions);
34
36
  const hasGradingOrComments = useAppSelector(selectHasGradingOrComments);
35
- const hasPedago = hasInstructions || hasGradingOrComments;
37
+ const hasPedago = hasInstructions || hasGradingOrComments || mode === "review";
36
38
  const dispatch = useAppDispatch();
37
39
  const showPedago = hasPedago && isPedagoVisible;
40
+ const isDirty = useAppSelector(selectIsDirty);
41
+ const save = useSave();
42
+
43
+ const handleCtrlS = useCallback(
44
+ (e: KeyboardEvent<HTMLDivElement>) => {
45
+ if ((e.ctrlKey || e.metaKey) && e.key === "s") {
46
+ e.preventDefault();
47
+ if (isDirty) {
48
+ save();
49
+ }
50
+ }
51
+ },
52
+ [isDirty, save],
53
+ );
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";
38
60
 
39
61
  return (
40
62
  <div
@@ -43,6 +65,7 @@ const App: FC<AppProps> = (props) => {
43
65
  isDark ? "dark-theme" : "light-theme",
44
66
  isHorizontal ? "layout-horizontal" : "layout-vertical",
45
67
  )}
68
+ onKeyDown={handleCtrlS}
46
69
  >
47
70
  <div>
48
71
  <Navbar />
@@ -63,16 +86,8 @@ const App: FC<AppProps> = (props) => {
63
86
  onClick={
64
87
  hasPedago ? () => dispatch(toggleIsPedagoVisible()) : undefined
65
88
  }
66
- data-pr-tooltip={
67
- hasPedago
68
- ? "Afficher les consignes"
69
- : "Pas de consignes ni de note"
70
- }
71
- aria-label={
72
- hasPedago
73
- ? "Afficher les consignes"
74
- : "Pas de consignes ni de note"
75
- }
89
+ data-pr-tooltip={pedagoOpenLabel}
90
+ aria-label={pedagoOpenLabel}
76
91
  role={hasPedago ? "button" : "note"}
77
92
  >
78
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
+ };
package/tsconfig.json CHANGED
@@ -13,7 +13,6 @@
13
13
  "isolatedModules": true,
14
14
  "noEmit": true,
15
15
  "jsx": "react-jsx",
16
- "types": ["vitest/globals"],
17
16
 
18
17
  /* Linting */
19
18
  "strict": true,
package/vite.config.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { defineConfig } from "vitest/config"
1
+ import { defineConfig } from "vite"
2
2
  import react from "@vitejs/plugin-react"
3
3
 
4
4
  // https://vitejs.dev/config/
@@ -8,10 +8,4 @@ export default defineConfig({
8
8
  server: {
9
9
  open: true,
10
10
  },
11
- test: {
12
- globals: true,
13
- environment: "jsdom",
14
- setupFiles: "src/setupTests",
15
- mockReset: true,
16
- },
17
11
  })
package/.eslintrc.json DELETED
@@ -1,36 +0,0 @@
1
- {
2
- "extends": [
3
- "eslint:recommended",
4
- "react-app",
5
- "plugin:react/jsx-runtime",
6
- "prettier",
7
- "plugin:react-hooks/recommended"
8
- ],
9
- "parser": "@typescript-eslint/parser",
10
- "parserOptions": { "project": true, "tsconfigRootDir": "./" },
11
- "plugins": ["@typescript-eslint"],
12
- "root": true,
13
- "ignorePatterns": ["dist"],
14
- "rules": {
15
- "@typescript-eslint/consistent-type-imports": [
16
- 2,
17
- { "fixStyle": "separate-type-imports" }
18
- ],
19
- "@typescript-eslint/no-restricted-imports": [
20
- 2,
21
- {
22
- "paths": [
23
- {
24
- "name": "react-redux",
25
- "importNames": ["useSelector", "useStore", "useDispatch"],
26
- "message": "Please use pre-typed versions from `src/app/hooks.ts` instead."
27
- }
28
- ]
29
- }
30
- ]
31
- },
32
- "overrides": [
33
- { "files": ["*.{c,m,}{t,j}s", "*.{t,j}sx"] },
34
- { "files": ["*{test,spec}.{t,j}s?(x)"], "env": { "jest": true } }
35
- ]
36
- }
package/src/setupTests.ts DELETED
@@ -1 +0,0 @@
1
- import "@testing-library/jest-dom/vitest"
@@ -1,65 +0,0 @@
1
- import type { RenderOptions } from "@testing-library/react"
2
- import { render } from "@testing-library/react"
3
- import userEvent from "@testing-library/user-event"
4
- import type { PropsWithChildren, ReactElement } from "react"
5
- import { Provider } from "react-redux"
6
- import type { AppStore, RootState } from "../app/store"
7
- import { makeStore } from "../app/store"
8
-
9
- /**
10
- * This type extends the default options for
11
- * React Testing Library's render function. It allows for
12
- * additional configuration such as specifying an initial Redux state and
13
- * a custom store instance.
14
- */
15
- interface ExtendedRenderOptions extends Omit<RenderOptions, "queries"> {
16
- /**
17
- * Defines a specific portion or the entire initial state for the Redux store.
18
- * This is particularly useful for initializing the state in a
19
- * controlled manner during testing, allowing components to be rendered
20
- * with predetermined state conditions.
21
- */
22
- preloadedState?: Partial<RootState>
23
-
24
- /**
25
- * Allows the use of a specific Redux store instance instead of a
26
- * default or global store. This flexibility is beneficial when
27
- * testing components with unique store requirements or when isolating
28
- * tests from a global store state. The custom store should be configured
29
- * to match the structure and middleware of the store used by the application.
30
- *
31
- * @default makeStore(preloadedState)
32
- */
33
- store?: AppStore
34
- }
35
-
36
- /**
37
- * Renders the given React element with Redux Provider and custom store.
38
- * This function is useful for testing components that are connected to the Redux store.
39
- *
40
- * @param ui - The React component or element to render.
41
- * @param extendedRenderOptions - Optional configuration options for rendering. This includes `preloadedState` for initial Redux state and `store` for a specific Redux store instance. Any additional properties are passed to React Testing Library's render function.
42
- * @returns An object containing the Redux store used in the render, User event API for simulating user interactions in tests, and all of React Testing Library's query functions for testing the component.
43
- */
44
- export const renderWithProviders = (
45
- ui: ReactElement,
46
- extendedRenderOptions: ExtendedRenderOptions = {},
47
- ) => {
48
- const {
49
- preloadedState = {},
50
- // Automatically create a store instance if no store was passed in
51
- store = makeStore(preloadedState),
52
- ...renderOptions
53
- } = extendedRenderOptions
54
-
55
- const Wrapper = ({ children }: PropsWithChildren) => (
56
- <Provider store={store}>{children}</Provider>
57
- )
58
-
59
- // Return an object with the store and all of RTL's query functions
60
- return {
61
- store,
62
- user: userEvent.setup(),
63
- ...render(ui, { wrapper: Wrapper, ...renderOptions }),
64
- }
65
- }