@capytale/meta-player 0.0.1

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.
Files changed (66) hide show
  1. package/.eslintrc.json +35 -0
  2. package/.prettierrc.json +4 -0
  3. package/README.md +27 -0
  4. package/index.html +15 -0
  5. package/package.json +48 -0
  6. package/public/themes/lara-dark-blue/fonts/Inter-italic.var.woff2 +0 -0
  7. package/public/themes/lara-dark-blue/fonts/Inter-roman.var.woff2 +0 -0
  8. package/public/themes/lara-dark-blue/theme.css +7015 -0
  9. package/public/themes/lara-light-blue/fonts/Inter-italic.var.woff2 +0 -0
  10. package/public/themes/lara-light-blue/fonts/Inter-roman.var.woff2 +0 -0
  11. package/public/themes/lara-light-blue/theme.css +7005 -0
  12. package/src/App.tsx +116 -0
  13. package/src/AppRedux.css +39 -0
  14. package/src/MetaPlayer.tsx +51 -0
  15. package/src/app/createAppSlice.ts +6 -0
  16. package/src/app/hooks.ts +12 -0
  17. package/src/app/store.ts +46 -0
  18. package/src/app.module.scss +56 -0
  19. package/src/demo.tsx +81 -0
  20. package/src/features/activityData/IsDirtySetter.tsx +17 -0
  21. package/src/features/activityData/OptionSetter.tsx +35 -0
  22. package/src/features/activityData/activityDataSlice.ts +250 -0
  23. package/src/features/activityData/metaPlayerOptions.ts +17 -0
  24. package/src/features/activityJS/ActivityJSProvider.tsx +110 -0
  25. package/src/features/activityJS/AfterSaveAction.tsx +23 -0
  26. package/src/features/activityJS/BeforeSaveAction.tsx +23 -0
  27. package/src/features/activityJS/Saver.tsx +147 -0
  28. package/src/features/activityJS/hooks.ts +93 -0
  29. package/src/features/activityJS/saverSlice.ts +58 -0
  30. package/src/features/layout/layoutSlice.ts +76 -0
  31. package/src/features/navbar/ActivityInfo.tsx +41 -0
  32. package/src/features/navbar/ActivityMenu.tsx +56 -0
  33. package/src/features/navbar/ActivityQuickActions.tsx +52 -0
  34. package/src/features/navbar/ActivitySidebarActions.tsx +52 -0
  35. package/src/features/navbar/CapytaleMenu.tsx +183 -0
  36. package/src/features/navbar/GradingNav.tsx +120 -0
  37. package/src/features/navbar/ReviewNavbar.tsx +18 -0
  38. package/src/features/navbar/SidebarContent.tsx +125 -0
  39. package/src/features/navbar/index.tsx +33 -0
  40. package/src/features/navbar/navbarSlice.ts +51 -0
  41. package/src/features/navbar/student-utils.ts +11 -0
  42. package/src/features/navbar/style.module.scss +162 -0
  43. package/src/features/pedago/AnswerSheetEditor.tsx +65 -0
  44. package/src/features/pedago/InstructionsEditor.tsx +82 -0
  45. package/src/features/pedago/index.tsx +219 -0
  46. package/src/features/pedago/style.module.scss +104 -0
  47. package/src/features/theming/ThemeSwitcher.tsx +51 -0
  48. package/src/features/theming/themingSlice.ts +93 -0
  49. package/src/hooks/index.ts +8 -0
  50. package/src/index.css +30 -0
  51. package/src/index.tsx +6 -0
  52. package/src/logo.svg +1 -0
  53. package/src/my_json_data.js +4146 -0
  54. package/src/settings.ts +6 -0
  55. package/src/setupTests.ts +1 -0
  56. package/src/utils/ErrorBoundary.tsx +41 -0
  57. package/src/utils/PopupButton.tsx +135 -0
  58. package/src/utils/activity.ts +8 -0
  59. package/src/utils/breakpoints.ts +5 -0
  60. package/src/utils/clipboard.ts +11 -0
  61. package/src/utils/test-utils.tsx +65 -0
  62. package/src/utils/useFullscreen.ts +65 -0
  63. package/src/vite-env.d.ts +1 -0
  64. package/tsconfig.json +27 -0
  65. package/tsconfig.node.json +9 -0
  66. package/vite.config.ts +17 -0
@@ -0,0 +1,58 @@
1
+ import type { PayloadAction } from "@reduxjs/toolkit";
2
+ import { createAppSlice } from "../../app/createAppSlice";
3
+
4
+ type CallbackType = () => void | Promise<void>;
5
+
6
+ export type SaveCallback = {
7
+ name: string;
8
+ callback: CallbackType;
9
+ };
10
+
11
+ export interface SaverState {
12
+ beforeSave: { [key: string]: CallbackType };
13
+ afterSave: { [key: string]: CallbackType };
14
+ }
15
+
16
+ const initialState: SaverState = {
17
+ beforeSave: {},
18
+ afterSave: {},
19
+ };
20
+
21
+ // If you are not using async thunks you can use the standalone `createSlice`.
22
+ export const saverSlice = createAppSlice({
23
+ name: "saver",
24
+ // `createSlice` will infer the state type from the `initialState` argument
25
+ initialState,
26
+ // The `reducers` field lets us define reducers and generate associated actions
27
+ reducers: (create) => ({
28
+ // Use the `PayloadAction` type to declare the contents of `action.payload`
29
+ addBeforeSave: create.reducer(
30
+ (state, action: PayloadAction<SaveCallback>) => {
31
+ state.beforeSave[action.payload.name] = action.payload.callback;
32
+ },
33
+ ),
34
+ addAfterSave: create.reducer(
35
+ (state, action: PayloadAction<SaveCallback>) => {
36
+ state.afterSave[action.payload.name] = action.payload.callback;
37
+ },
38
+ ),
39
+ removeBeforeSave: create.reducer((state, action: PayloadAction<string>) => {
40
+ delete state.beforeSave[action.payload];
41
+ }),
42
+ removeAfterSave: create.reducer((state, action: PayloadAction<string>) => {
43
+ delete state.afterSave[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
+ selectBeforeSave: (saver) => saver.beforeSave,
50
+ selectAfterSave: (saver) => saver.afterSave,
51
+ },
52
+ });
53
+
54
+ // Action creators are generated for each case reducer function.
55
+ export const { addBeforeSave, removeBeforeSave, addAfterSave, removeAfterSave } = saverSlice.actions;
56
+
57
+ // Selectors returned by `slice.selectors` take the root state as their first argument.
58
+ export const { selectBeforeSave, selectAfterSave } = saverSlice.selectors;
@@ -0,0 +1,76 @@
1
+ import type { PayloadAction } from "@reduxjs/toolkit";
2
+ import { createAppSlice } from "../../app/createAppSlice";
3
+
4
+ export type Orientation = "horizontal" | "vertical";
5
+
6
+ type PedagoTab = "instructions" | "answerSheet";
7
+
8
+ export interface LayoutState {
9
+ orientation: Orientation;
10
+ isPedagoVisible: boolean;
11
+ isGradingVisible: boolean;
12
+ pedagoTab: PedagoTab;
13
+ }
14
+
15
+ const initialState: LayoutState = {
16
+ orientation: "horizontal",
17
+ isPedagoVisible: true,
18
+ isGradingVisible: true,
19
+ pedagoTab: "instructions",
20
+ };
21
+
22
+ // If you are not using async thunks you can use the standalone `createSlice`.
23
+ export const layoutSlice = createAppSlice({
24
+ name: "layout",
25
+ // `createSlice` will infer the state type from the `initialState` argument
26
+ initialState,
27
+ // The `reducers` field lets us define reducers and generate associated actions
28
+ reducers: (create) => ({
29
+ toggleLayout: create.reducer((state) => {
30
+ // Redux Toolkit allows us to write "mutating" logic in reducers. It
31
+ // doesn't actually mutate the state because it uses the Immer library,
32
+ // which detects changes to a "draft state" and produces a brand new
33
+ // immutable state based off those changes
34
+ state.orientation =
35
+ state.orientation === "horizontal" ? "vertical" : "horizontal";
36
+ }),
37
+ // Use the `PayloadAction` type to declare the contents of `action.payload`
38
+ setLayout: create.reducer((state, action: PayloadAction<Orientation>) => {
39
+ state.orientation = action.payload;
40
+ }),
41
+ toggleIsPedagoVisible: create.reducer((state) => {
42
+ state.isPedagoVisible = !state.isPedagoVisible;
43
+ }),
44
+ toggleIsGradingVisible: create.reducer((state) => {
45
+ state.isGradingVisible = !state.isGradingVisible;
46
+ }),
47
+ setPedagoTab: create.reducer((state, action: PayloadAction<PedagoTab>) => {
48
+ state.pedagoTab = action.payload;
49
+ }),
50
+ }),
51
+ // You can define your selectors here. These selectors receive the slice
52
+ // state as their first argument.
53
+ selectors: {
54
+ selectOrientation: (layout) => layout.orientation,
55
+ selectIsPedagoVisible: (layout) => layout.isPedagoVisible,
56
+ selectIsGradingVisible: (layout) => layout.isGradingVisible,
57
+ selectPedagoTab: (layout) => layout.pedagoTab,
58
+ },
59
+ });
60
+
61
+ // Action creators are generated for each case reducer function.
62
+ export const {
63
+ toggleLayout,
64
+ setLayout,
65
+ toggleIsPedagoVisible,
66
+ toggleIsGradingVisible,
67
+ setPedagoTab,
68
+ } = layoutSlice.actions;
69
+
70
+ // Selectors returned by `slice.selectors` take the root state as their first argument.
71
+ export const {
72
+ selectOrientation,
73
+ selectIsPedagoVisible,
74
+ selectIsGradingVisible,
75
+ selectPedagoTab,
76
+ } = layoutSlice.selectors;
@@ -0,0 +1,41 @@
1
+ import { useAppSelector } from "../../app/hooks";
2
+ import {
3
+ selectActivityInfo,
4
+ selectIcon,
5
+ selectMode,
6
+ } from "../activityData/activityDataSlice";
7
+ import { studentNameFromInfo } from "./student-utils";
8
+ import styles from "./style.module.scss";
9
+
10
+ const ActivityInfo: React.FC = () => {
11
+ const icon = useAppSelector(selectIcon);
12
+ const mode = useAppSelector(selectMode);
13
+ const activityInfo = useAppSelector(selectActivityInfo);
14
+ const studentName = studentNameFromInfo(activityInfo.studentInfo);
15
+ return (
16
+ <div className={styles.activityInfo}>
17
+ <div className={styles.activityInfoText}>
18
+ <div className={styles.activityInfoTitle}>{activityInfo.title}</div>
19
+ {mode === "assignment" && studentName && (
20
+ <div className={styles.activityInfoStudentName}>{studentName}</div>
21
+ )}
22
+ {mode === "create" && (
23
+ <div className={styles.activityInfoStudentName}>
24
+ Création d'activité
25
+ </div>
26
+ )}
27
+ </div>
28
+ {icon?.path && (
29
+ <img
30
+ className={styles.activityLogo}
31
+ src={icon.path}
32
+ style={{
33
+ backgroundColor: icon.style["background-color"],
34
+ }}
35
+ />
36
+ )}
37
+ </div>
38
+ );
39
+ };
40
+
41
+ export default ActivityInfo;
@@ -0,0 +1,56 @@
1
+ import { memo, useState } from "react";
2
+ import styles from "./style.module.scss";
3
+
4
+ import { Button } from "primereact/button";
5
+ import { Sidebar } from "primereact/sidebar";
6
+ import SidebarContent from "./SidebarContent";
7
+ import settings from "../../settings";
8
+ import ActivityQuickActions from "./ActivityQuickActions";
9
+ import useFullscreen from "../../utils/useFullscreen";
10
+
11
+ const ActivityMenu: React.FC = memo(() => {
12
+ const [settingsSidebarVisible, setSettingsSidebarVisible] = useState(false);
13
+ const [wantFullscreen, setWantFullscreen] = useState(false);
14
+ const isFullscreen = useFullscreen(wantFullscreen, () =>
15
+ setWantFullscreen(false),
16
+ );
17
+ return (
18
+ <div className={styles.activityMenu}>
19
+ <ActivityQuickActions />
20
+ <Button
21
+ severity="secondary"
22
+ size="small"
23
+ icon={isFullscreen ? "pi pi-expand" : "pi pi-expand" }
24
+ text
25
+ tooltip="Plein écran"
26
+ tooltipOptions={{
27
+ position: "bottom",
28
+ showDelay: settings.TOOLTIP_SHOW_DELAY,
29
+ }}
30
+ onClick={() => setWantFullscreen((prev) => !prev)}
31
+ />
32
+ <Button
33
+ severity="secondary"
34
+ size="small"
35
+ icon="pi pi-cog" // pi-ellipsis-v
36
+ outlined
37
+ tooltip="Paramètres"
38
+ tooltipOptions={{
39
+ position: "left",
40
+ showDelay: settings.TOOLTIP_SHOW_DELAY,
41
+ }}
42
+ onClick={() => setSettingsSidebarVisible(true)}
43
+ />
44
+ <Sidebar
45
+ header="Paramètres"
46
+ visible={settingsSidebarVisible}
47
+ position="right"
48
+ onHide={() => setSettingsSidebarVisible(false)}
49
+ >
50
+ <SidebarContent />
51
+ </Sidebar>
52
+ </div>
53
+ );
54
+ });
55
+
56
+ export default ActivityMenu;
@@ -0,0 +1,52 @@
1
+ import { FC, useEffect } from "react";
2
+ import { useAppDispatch, useAppSelector } from "../../app/hooks";
3
+ import {
4
+ QuickAction,
5
+ selectQuickActions,
6
+ setQuickActions,
7
+ } from "./navbarSlice";
8
+ import { Button } from "primereact/button";
9
+ import settings from "../../settings";
10
+
11
+ const ActivityQuickActions: FC<{}> = () => {
12
+ const quickActions = useAppSelector(selectQuickActions);
13
+ return (
14
+ <>
15
+ {quickActions.map((action) => (
16
+ <Button
17
+ key={action.title}
18
+ severity="secondary"
19
+ size="small"
20
+ icon={action.icon}
21
+ onClick={action.action}
22
+ outlined
23
+ tooltip={action.title}
24
+ tooltipOptions={{
25
+ position: "bottom",
26
+ showDelay: settings.TOOLTIP_SHOW_DELAY,
27
+ }}
28
+ />
29
+ ))}
30
+ </>
31
+ );
32
+ };
33
+
34
+ type ActivityQuickActionsSetterProps = {
35
+ actions: QuickAction[];
36
+ };
37
+
38
+ export const ActivityQuickActionsSetter: FC<ActivityQuickActionsSetterProps> = (
39
+ props,
40
+ ) => {
41
+ const dispatch = useAppDispatch();
42
+ useEffect(() => {
43
+ dispatch(setQuickActions(props.actions));
44
+ return () => {
45
+ dispatch(setQuickActions([]));
46
+ };
47
+ }, [props.actions]);
48
+
49
+ return <></>;
50
+ };
51
+
52
+ export default ActivityQuickActions;
@@ -0,0 +1,52 @@
1
+ import { FC, useEffect } from "react";
2
+ import { useAppDispatch, useAppSelector } from "../../app/hooks";
3
+ import {
4
+ QuickAction,
5
+ selectSidebarActions,
6
+ setSidebarActions,
7
+ } from "./navbarSlice";
8
+ import { Button } from "primereact/button";
9
+ import settings from "../../settings";
10
+
11
+ const ActivitySidebarActions: FC<{}> = () => {
12
+ const sidebarActions = useAppSelector(selectSidebarActions);
13
+ return (
14
+ <>
15
+ {sidebarActions.map((action) => (
16
+ <Button
17
+ key={action.title}
18
+ severity="secondary"
19
+ size="small"
20
+ icon={action.icon}
21
+ onClick={action.action}
22
+ outlined
23
+ label={action.title}
24
+ tooltipOptions={{
25
+ position: "left",
26
+ showDelay: settings.TOOLTIP_SHOW_DELAY,
27
+ }}
28
+ />
29
+ ))}
30
+ </>
31
+ );
32
+ };
33
+
34
+ type ActivitySidebarActionsSetterProps = {
35
+ actions: QuickAction[];
36
+ };
37
+
38
+ export const ActivitySidebarActionsSetter: FC<
39
+ ActivitySidebarActionsSetterProps
40
+ > = (props) => {
41
+ const dispatch = useAppDispatch();
42
+ useEffect(() => {
43
+ dispatch(setSidebarActions(props.actions));
44
+ return () => {
45
+ dispatch(setSidebarActions([]));
46
+ };
47
+ }, [props.actions]);
48
+
49
+ return <></>;
50
+ };
51
+
52
+ export default ActivitySidebarActions;
@@ -0,0 +1,183 @@
1
+ import { Button } from "primereact/button";
2
+ import { SplitButton } from "primereact/splitbutton";
3
+ import { ConfirmDialog, confirmDialog } from "primereact/confirmdialog";
4
+
5
+ import styles from "./style.module.scss";
6
+ import { useMemo } from "react";
7
+ import { useWindowSize } from "@uidotdev/usehooks";
8
+ import { ML, XL } from "../../utils/breakpoints";
9
+ import { useAppDispatch, useAppSelector } from "../../app/hooks";
10
+ import {
11
+ selectIsDirty,
12
+ selectMode,
13
+ selectSaveState,
14
+ selectSharingInfo,
15
+ selectWorkflow,
16
+ setSaveState,
17
+ setWorkflow,
18
+ } from "../activityData/activityDataSlice";
19
+ import { copyToClipboard } from "../../utils/clipboard";
20
+ import settings from "../../settings";
21
+ import { wf } from "@capytale/activity.js/activity/field/workflow";
22
+
23
+ const CapytaleMenu: React.FC = () => {
24
+ const dispatch = useAppDispatch();
25
+ const sharingInfo = useAppSelector(selectSharingInfo);
26
+ const saveState = useAppSelector(selectSaveState);
27
+ const windowsSize = useWindowSize();
28
+ const mode = useAppSelector(selectMode);
29
+ const workflow = useAppSelector(selectWorkflow);
30
+ const isDirty = useAppSelector(selectIsDirty);
31
+ const isLarge = useMemo(
32
+ () => windowsSize.width && windowsSize.width >= XL,
33
+ [windowsSize.width],
34
+ );
35
+ const isQuiteSmall = useMemo(
36
+ () => windowsSize.width && windowsSize.width < ML,
37
+ [windowsSize.width],
38
+ );
39
+ const changeWorkflow = (value: wf) => {
40
+ dispatch(setWorkflow(value));
41
+ dispatch(setSaveState("should-save"));
42
+ };
43
+ const confirmFinishAssignment = () => {
44
+ confirmDialog({
45
+ message:
46
+ "Votre copie va être rendue et vous ne pourrez plus sauvegarder les modifications ultérieures.",
47
+ header: "Attention",
48
+ icon: "pi pi-exclamation-triangle",
49
+ defaultFocus: "accept",
50
+ acceptLabel: "Confirmer",
51
+ rejectLabel: "Annuler",
52
+ accept: () => changeWorkflow("finished"),
53
+ });
54
+ };
55
+ return (
56
+ <div className={styles.capytaleMenu}>
57
+ <Button
58
+ label={isQuiteSmall ? undefined : "Enregistrer"}
59
+ severity={isDirty ? "danger" : "warning"}
60
+ size="small"
61
+ icon="pi pi-save"
62
+ loading={saveState !== "idle"}
63
+ tooltip="Enregistrer l'activité"
64
+ tooltipOptions={{
65
+ position: "bottom",
66
+ showDelay: settings.TOOLTIP_SHOW_DELAY,
67
+ }}
68
+ onClick={() => {
69
+ dispatch(setSaveState("should-save"));
70
+ }}
71
+ />
72
+ {mode === "create" && (
73
+ <SplitButton
74
+ label={isLarge ? sharingInfo.code : undefined}
75
+ severity="secondary"
76
+ size="small"
77
+ icon="pi pi-share-alt"
78
+ outlined
79
+ buttonProps={{
80
+ tooltip: "Copier le code de partage",
81
+ tooltipOptions: {
82
+ position: "bottom",
83
+ showDelay: settings.TOOLTIP_SHOW_DELAY,
84
+ },
85
+ }}
86
+ onClick={() => {
87
+ copyToClipboard(sharingInfo.code);
88
+ }}
89
+ model={[
90
+ {
91
+ label: "Copier l'URL de partage",
92
+ icon: "pi pi-link",
93
+ command: () => {
94
+ copyToClipboard(sharingInfo.codeLink);
95
+ },
96
+ },
97
+ ]}
98
+ />
99
+ )}
100
+ {mode === "assignment" && workflow === "current" && (
101
+ <Button
102
+ outlined
103
+ label="Rendre"
104
+ icon="pi pi-envelope"
105
+ onClick={() => {
106
+ console.log("Hey");
107
+ confirmFinishAssignment();
108
+ }}
109
+ />
110
+ )}
111
+ {mode === "assignment" && workflow !== "current" && (
112
+ <Button
113
+ outlined
114
+ label={workflow === "finished" ? "Rendue" : "Corrigée"}
115
+ disabled
116
+ icon={
117
+ workflow === "finished" ? "pi pi-envelope" : "pi pi-check-square"
118
+ }
119
+ tooltip="Copie déjà rendue"
120
+ />
121
+ )}
122
+ {mode === "review" && (
123
+ <SplitButton
124
+ label={
125
+ workflow === "current"
126
+ ? "En cours"
127
+ : workflow === "finished"
128
+ ? "Rendue"
129
+ : "Corrigée"
130
+ }
131
+ severity="secondary"
132
+ size="small"
133
+ icon={
134
+ workflow === "current"
135
+ ? "pi pi-pencil"
136
+ : workflow === "finished"
137
+ ? "pi pi-envelope"
138
+ : "pi pi-check-square"
139
+ }
140
+ outlined
141
+ model={[
142
+ ...(workflow === "current"
143
+ ? []
144
+ : [
145
+ {
146
+ label: "Réautoriser la modification",
147
+ icon: "pi pi-pencil",
148
+ command: () => {
149
+ changeWorkflow("current");
150
+ },
151
+ },
152
+ ]),
153
+ ...(workflow === "finished"
154
+ ? []
155
+ : [
156
+ {
157
+ label: "Marquer comme rendue",
158
+ icon: "pi pi-envelope",
159
+ command: () => {
160
+ changeWorkflow("finished");
161
+ },
162
+ },
163
+ ]),
164
+ ...(workflow === "corrected"
165
+ ? []
166
+ : [
167
+ {
168
+ label: "Marquer comme corrigée",
169
+ icon: "pi pi-check-square",
170
+ command: () => {
171
+ changeWorkflow("corrected");
172
+ },
173
+ },
174
+ ]),
175
+ ]}
176
+ />
177
+ )}
178
+ <ConfirmDialog />
179
+ </div>
180
+ );
181
+ };
182
+
183
+ export default CapytaleMenu;
@@ -0,0 +1,120 @@
1
+ import { useState, useEffect, useMemo } from "react";
2
+ import evalApi from "@capytale/activity.js/backend/capytale/evaluation";
3
+ import { Dropdown } from "primereact/dropdown";
4
+
5
+ import { useAppSelector } from "../../app/hooks";
6
+ import {
7
+ selectActivityNid,
8
+ selectMode,
9
+ selectNid,
10
+ } from "../activityData/activityDataSlice";
11
+ import { studentNameFromInfo } from "./student-utils";
12
+
13
+ import { SaInfo } from "@capytale/activity.js/activity/evaluation/evaluation";
14
+ import { SelectItem, SelectItemOptionsType } from "primereact/selectitem";
15
+ import { Button } from "primereact/button";
16
+
17
+ const GradingNav: React.FC = () => {
18
+ const [studentList, setStudentList] = useState<SaInfo[]>([]);
19
+ const nid = useAppSelector(selectNid) as number;
20
+ const mode = useAppSelector(selectMode);
21
+ const activityNid = useAppSelector(selectActivityNid) as number;
22
+ console.log("Le nid est : ", nid);
23
+ useEffect(() => {
24
+ evalApi.listSa(activityNid).then((j) => {
25
+ setStudentList(j);
26
+ });
27
+ }, []);
28
+
29
+ const goToStudent = (snid: string | number) => {
30
+ const redir = new URL(window.location.href);
31
+ redir.searchParams.set("id", snid.toString());
32
+ redir.searchParams.set("mode", mode);
33
+ location.href = redir.toString();
34
+ };
35
+
36
+ const handlePrev = () => {
37
+ if (studentList == null) return;
38
+ const i = studentList.findIndex((el) => (el.nid as number) == nid);
39
+ if (i <= 0) return;
40
+ goToStudent(studentList[i - 1].nid);
41
+ };
42
+ const handleNext = () => {
43
+ if (studentList == null) return;
44
+ const i = studentList.findIndex((el) => el.nid == nid);
45
+ if (i < 0 || i >= studentList.length - 1) return;
46
+ goToStudent(studentList[i + 1].nid);
47
+ };
48
+
49
+ function getWf(el: SaInfo) {
50
+ if (el.workflow == "100") return { icon: "pi pi-pencil", color: "blue" };
51
+ if (el.workflow == "200")
52
+ return { icon: "pi pi-envelope", color: "orange" };
53
+ if (el.workflow == "300")
54
+ return { icon: "pi pi-list-check", color: "green" };
55
+ throw new Error("Unknown workflow");
56
+ }
57
+
58
+ const options: SelectItemOptionsType = useMemo(() => {
59
+ return studentList.map((el) => {
60
+ const item: SelectItem = {
61
+ label:
62
+ studentNameFromInfo({
63
+ firstName: el.firstname,
64
+ lastName: el.lastname,
65
+ class: el.classe,
66
+ }) || undefined,
67
+ value: el.nid,
68
+ icon: getWf(el).icon,
69
+ };
70
+ return item;
71
+ });
72
+ }, [studentList]);
73
+
74
+ if (studentList.length === 0) {
75
+ return <Dropdown loading placeholder="Chargement..." />;
76
+ }
77
+
78
+ const firstNid = studentList[0].nid as number;
79
+ const lastNid = studentList[Object.keys(studentList).length - 1]
80
+ .nid as number;
81
+
82
+ return (
83
+ <div className="p-inputgroup flex-1">
84
+ <Button
85
+ size="small"
86
+ icon="pi pi-chevron-left"
87
+ severity="secondary"
88
+ onClick={handlePrev}
89
+ tooltip="Copie précédente"
90
+ tooltipOptions={{
91
+ position: "left"
92
+ }}
93
+ disabled={nid == firstNid}
94
+ outlined
95
+ />
96
+ <Dropdown
97
+ value={nid}
98
+ options={options}
99
+ filter
100
+ onChange={(e) => {
101
+ goToStudent(e.value);
102
+ }}
103
+ />
104
+ <Button
105
+ size="small"
106
+ icon="pi pi-chevron-right"
107
+ severity="secondary"
108
+ onClick={handleNext}
109
+ tooltip="Copie suivante"
110
+ tooltipOptions={{
111
+ position: "right"
112
+ }}
113
+ disabled={nid == lastNid}
114
+ outlined
115
+ />
116
+ </div>
117
+ );
118
+ };
119
+
120
+ export default GradingNav;
@@ -0,0 +1,18 @@
1
+ import { FC } from "react";
2
+ import GradingNav from "./GradingNav";
3
+
4
+ import styles from "./style.module.scss";
5
+
6
+ const ReviewNavbar: FC = () => {
7
+ return (
8
+ <div className={styles.reviewNavbar}>
9
+ <div>Mode correction</div>
10
+ <div>
11
+ <GradingNav />
12
+ </div>
13
+ <div></div>
14
+ </div>
15
+ );
16
+ };
17
+
18
+ export default ReviewNavbar;