@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.
- package/.eslintrc.json +35 -0
- package/.prettierrc.json +4 -0
- package/README.md +27 -0
- package/index.html +15 -0
- package/package.json +48 -0
- package/public/themes/lara-dark-blue/fonts/Inter-italic.var.woff2 +0 -0
- package/public/themes/lara-dark-blue/fonts/Inter-roman.var.woff2 +0 -0
- package/public/themes/lara-dark-blue/theme.css +7015 -0
- package/public/themes/lara-light-blue/fonts/Inter-italic.var.woff2 +0 -0
- package/public/themes/lara-light-blue/fonts/Inter-roman.var.woff2 +0 -0
- package/public/themes/lara-light-blue/theme.css +7005 -0
- package/src/App.tsx +116 -0
- package/src/AppRedux.css +39 -0
- package/src/MetaPlayer.tsx +51 -0
- package/src/app/createAppSlice.ts +6 -0
- package/src/app/hooks.ts +12 -0
- package/src/app/store.ts +46 -0
- package/src/app.module.scss +56 -0
- package/src/demo.tsx +81 -0
- package/src/features/activityData/IsDirtySetter.tsx +17 -0
- package/src/features/activityData/OptionSetter.tsx +35 -0
- package/src/features/activityData/activityDataSlice.ts +250 -0
- package/src/features/activityData/metaPlayerOptions.ts +17 -0
- package/src/features/activityJS/ActivityJSProvider.tsx +110 -0
- package/src/features/activityJS/AfterSaveAction.tsx +23 -0
- package/src/features/activityJS/BeforeSaveAction.tsx +23 -0
- package/src/features/activityJS/Saver.tsx +147 -0
- package/src/features/activityJS/hooks.ts +93 -0
- package/src/features/activityJS/saverSlice.ts +58 -0
- package/src/features/layout/layoutSlice.ts +76 -0
- package/src/features/navbar/ActivityInfo.tsx +41 -0
- package/src/features/navbar/ActivityMenu.tsx +56 -0
- package/src/features/navbar/ActivityQuickActions.tsx +52 -0
- package/src/features/navbar/ActivitySidebarActions.tsx +52 -0
- package/src/features/navbar/CapytaleMenu.tsx +183 -0
- package/src/features/navbar/GradingNav.tsx +120 -0
- package/src/features/navbar/ReviewNavbar.tsx +18 -0
- package/src/features/navbar/SidebarContent.tsx +125 -0
- package/src/features/navbar/index.tsx +33 -0
- package/src/features/navbar/navbarSlice.ts +51 -0
- package/src/features/navbar/student-utils.ts +11 -0
- package/src/features/navbar/style.module.scss +162 -0
- package/src/features/pedago/AnswerSheetEditor.tsx +65 -0
- package/src/features/pedago/InstructionsEditor.tsx +82 -0
- package/src/features/pedago/index.tsx +219 -0
- package/src/features/pedago/style.module.scss +104 -0
- package/src/features/theming/ThemeSwitcher.tsx +51 -0
- package/src/features/theming/themingSlice.ts +93 -0
- package/src/hooks/index.ts +8 -0
- package/src/index.css +30 -0
- package/src/index.tsx +6 -0
- package/src/logo.svg +1 -0
- package/src/my_json_data.js +4146 -0
- package/src/settings.ts +6 -0
- package/src/setupTests.ts +1 -0
- package/src/utils/ErrorBoundary.tsx +41 -0
- package/src/utils/PopupButton.tsx +135 -0
- package/src/utils/activity.ts +8 -0
- package/src/utils/breakpoints.ts +5 -0
- package/src/utils/clipboard.ts +11 -0
- package/src/utils/test-utils.tsx +65 -0
- package/src/utils/useFullscreen.ts +65 -0
- package/src/vite-env.d.ts +1 -0
- package/tsconfig.json +27 -0
- package/tsconfig.node.json +9 -0
- 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;
|