@capytale/meta-player 0.1.2 → 0.1.4
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 +4 -4
- package/src/MetaPlayer.tsx +1 -1
- package/src/features/activityJS/ActivityJSProvider.tsx +1 -1
- package/src/features/activityJS/BeforeResetAction.tsx +23 -0
- package/src/features/activityJS/hooks.ts +59 -91
- package/src/features/activityJS/internal-hooks.ts +93 -0
- package/src/features/activityJS/saverSlice.ts +23 -2
- package/src/features/navbar/ActivityMenu.tsx +17 -4
- package/src/features/navbar/CapytaleMenu.tsx +78 -54
- package/src/features/navbar/SidebarContent.tsx +37 -4
- package/src/features/navbar/index.tsx +3 -1
- package/src/features/navbar/style.module.scss +7 -0
- package/src/utils/ButtonDoubleIcon.tsx +35 -0
- package/src/utils/CardSelector.tsx +39 -0
- package/src/utils/style.module.scss +46 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@capytale/meta-player",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"dev": "vite",
|
|
@@ -14,12 +14,12 @@
|
|
|
14
14
|
"type-check": "tsc --noEmit"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@capytale/activity.js": "^3.0
|
|
17
|
+
"@capytale/activity.js": "^3.1.0",
|
|
18
18
|
"@capytale/capytale-rich-text-editor": "^0.4.2",
|
|
19
19
|
"@reduxjs/toolkit": "^2.0.1",
|
|
20
20
|
"@uidotdev/usehooks": "^2.4.1",
|
|
21
21
|
"primeicons": "^7.0.0",
|
|
22
|
-
"primereact": "^10.
|
|
22
|
+
"primereact": "^10.8.3",
|
|
23
23
|
"react": "^18.2.0",
|
|
24
24
|
"react-dom": "^18.2.0",
|
|
25
25
|
"react-html-props": "^2.0.9",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"prettier": "^3.2.1",
|
|
43
43
|
"sass": "^1.75.0",
|
|
44
44
|
"typescript": "^5.3.3",
|
|
45
|
-
"vite": "^5.
|
|
45
|
+
"vite": "^5.4.6",
|
|
46
46
|
"vitest": "^1.2.0"
|
|
47
47
|
}
|
|
48
48
|
}
|
package/src/MetaPlayer.tsx
CHANGED
|
@@ -10,7 +10,7 @@ import { store } from "./app/store";
|
|
|
10
10
|
import "./index.css";
|
|
11
11
|
import ThemeSwitcher from "./features/theming/ThemeSwitcher";
|
|
12
12
|
import { ActivityJSProvider } from "./features/activityJS/ActivityJSProvider";
|
|
13
|
-
import { LoadOptions } from "./features/activityJS/hooks";
|
|
13
|
+
import { LoadOptions } from "./features/activityJS/internal-hooks";
|
|
14
14
|
import { ErrorBoundary } from "./utils/ErrorBoundary";
|
|
15
15
|
import Saver from "./features/activityJS/Saver";
|
|
16
16
|
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
ActivitySessionLoaderReturnValue,
|
|
13
13
|
LoadOptions,
|
|
14
14
|
useActivitySessionLoader,
|
|
15
|
-
} from "./hooks";
|
|
15
|
+
} from "./internal-hooks";
|
|
16
16
|
import { getIdFromUrl } from "../../utils/activity";
|
|
17
17
|
import { useAppDispatch } from "../../app/hooks";
|
|
18
18
|
import ActivitySession from "@capytale/activity.js/activity/activitySession";
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { FC, useEffect } from "react";
|
|
2
|
+
import {
|
|
3
|
+
SaveCallback,
|
|
4
|
+
addBeforeReset,
|
|
5
|
+
removeBeforeReset,
|
|
6
|
+
} from "./saverSlice";
|
|
7
|
+
import { useAppDispatch } from "../../app/hooks";
|
|
8
|
+
|
|
9
|
+
type BeforeResetActionProps = SaveCallback;
|
|
10
|
+
|
|
11
|
+
const BeforeResetAction: FC<BeforeResetActionProps> = (props) => {
|
|
12
|
+
const dispatch = useAppDispatch();
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
dispatch(addBeforeReset(props));
|
|
15
|
+
return () => {
|
|
16
|
+
// Remove the callback when the component is unmounted
|
|
17
|
+
dispatch(removeBeforeReset(props.name));
|
|
18
|
+
};
|
|
19
|
+
}, [props]);
|
|
20
|
+
return null;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export default BeforeResetAction;
|
|
@@ -1,93 +1,61 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
import
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
state: "loading" | "loaded" | "error";
|
|
13
|
-
activitySession: ActivitySession | null;
|
|
14
|
-
error?: any;
|
|
1
|
+
import { useAppDispatch, useAppSelector } from "../../app/hooks";
|
|
2
|
+
import {
|
|
3
|
+
selectMode,
|
|
4
|
+
selectWorkflow,
|
|
5
|
+
setSaveState,
|
|
6
|
+
} from "../activityData/activityDataSlice";
|
|
7
|
+
import { useActivityJS } from "./ActivityJSProvider";
|
|
8
|
+
import { selectBeforeReset } from "./saverSlice";
|
|
9
|
+
|
|
10
|
+
type UseResetProps = {
|
|
11
|
+
onError: (errorMessage: string) => void;
|
|
15
12
|
};
|
|
16
13
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
return state;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Si un handler est passé en props à un composant, il peut changer. Il n'y a pas trop de raison mais
|
|
70
|
-
* rien ne l'interdit.
|
|
71
|
-
* Si un useEffect invoque ce handler, il faudrait que le handler figure dans les dépendances du useEffect.
|
|
72
|
-
* Du coup, si le handler change, le useEffect est ré-exécuté.
|
|
73
|
-
* Ce hook crée un wrapper immuable autour du handler passé en props. Ce wrapper ne change jamais et n'a donc
|
|
74
|
-
* pas besoin de figurer dans les dépendances du useEffect.
|
|
75
|
-
*
|
|
76
|
-
* @param handler
|
|
77
|
-
*/
|
|
78
|
-
export function useHandlerWrapper<H extends () => any>(
|
|
79
|
-
handler?: H,
|
|
80
|
-
): () => ReturnType<H> | void;
|
|
81
|
-
export function useHandlerWrapper<H extends (...args: any[]) => any>(
|
|
82
|
-
handler?: H,
|
|
83
|
-
): (...p: Parameters<H>) => ReturnType<H> | void;
|
|
84
|
-
export function useHandlerWrapper<H extends (...args: any[]) => any>(
|
|
85
|
-
handler?: H,
|
|
86
|
-
): (...p: Parameters<H>) => ReturnType<H> | void {
|
|
87
|
-
const handlerRef = useRef<H>();
|
|
88
|
-
handlerRef.current = handler;
|
|
89
|
-
return useRef((...p: Parameters<H>) => {
|
|
90
|
-
if (null == handlerRef.current) return;
|
|
91
|
-
return handlerRef.current(...p);
|
|
92
|
-
}).current;
|
|
93
|
-
}
|
|
14
|
+
export const useReset = (props: UseResetProps) => {
|
|
15
|
+
const dispatch = useAppDispatch();
|
|
16
|
+
const beforeReset = useAppSelector(selectBeforeReset);
|
|
17
|
+
const activityJs = useActivityJS();
|
|
18
|
+
const mode = useAppSelector(selectMode);
|
|
19
|
+
const workflow = useAppSelector(selectWorkflow);
|
|
20
|
+
if (mode !== "assignment" || workflow !== "current") {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
return async () => {
|
|
24
|
+
if (!activityJs.activitySession) {
|
|
25
|
+
throw new Error("No activity session to reset");
|
|
26
|
+
}
|
|
27
|
+
for (const callback of Object.values(beforeReset)) {
|
|
28
|
+
try {
|
|
29
|
+
const v = callback();
|
|
30
|
+
if (v instanceof Promise) {
|
|
31
|
+
await v;
|
|
32
|
+
}
|
|
33
|
+
} catch (e) {
|
|
34
|
+
console.error("Error in beforeReset callback", e);
|
|
35
|
+
props.onError((e as any).toString());
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
dispatch(setSaveState("saving"));
|
|
41
|
+
const ab = activityJs.activitySession.activityBunch;
|
|
42
|
+
// @ts-expect-error
|
|
43
|
+
ab.assignmentNode.content.value = null;
|
|
44
|
+
// @ts-expect-error
|
|
45
|
+
ab.assignmentNode.binaryData.value = null;
|
|
46
|
+
try {
|
|
47
|
+
const saveData = await ab.save();
|
|
48
|
+
console.log("Save return data", saveData);
|
|
49
|
+
location.reload();
|
|
50
|
+
} catch (e) {
|
|
51
|
+
console.error("Error in reset", e);
|
|
52
|
+
props.onError((e as any).toString());
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
dispatch(setSaveState("idle"));
|
|
56
|
+
} catch (e) {
|
|
57
|
+
console.error("Error in reset process", e);
|
|
58
|
+
props.onError((e as any).toString());
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { useEffect, useState, useRef } from "react";
|
|
2
|
+
|
|
3
|
+
import { autoLoad } from "@capytale/activity.js";
|
|
4
|
+
|
|
5
|
+
import type ActivitySession from "@capytale/activity.js/activity/activitySession/uni";
|
|
6
|
+
|
|
7
|
+
import type { ActivityBunchOptions as LoadOptions } from "@capytale/activity.js/activity/activityBunch/uni/backend";
|
|
8
|
+
|
|
9
|
+
export type { LoadOptions };
|
|
10
|
+
|
|
11
|
+
export type ActivitySessionLoaderReturnValue = {
|
|
12
|
+
state: "loading" | "loaded" | "error";
|
|
13
|
+
activitySession: ActivitySession | null;
|
|
14
|
+
error?: any;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Un hook pour utiliser activity.js
|
|
19
|
+
* Charge une activité de façon réactive.
|
|
20
|
+
*
|
|
21
|
+
* @param id id de l'activité à charger
|
|
22
|
+
* @param loadOptions les options de chargement
|
|
23
|
+
* @param callback une callback appelée avec l'activité chargée
|
|
24
|
+
* @returns un objet contenant l'état de chargement de l'activité et l'activité dès qu'elle est chargée
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
export function useActivitySessionLoader(
|
|
28
|
+
id: number,
|
|
29
|
+
loadOptions?: LoadOptions,
|
|
30
|
+
callback?: (activitySession: ActivitySession) => void,
|
|
31
|
+
): ActivitySessionLoaderReturnValue {
|
|
32
|
+
const callbackW = useHandlerWrapper(callback);
|
|
33
|
+
const [state, setState] = useState<ActivitySessionLoaderReturnValue>({
|
|
34
|
+
state: "loading",
|
|
35
|
+
activitySession: null,
|
|
36
|
+
});
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
let cancelled = false;
|
|
39
|
+
autoLoad(loadOptions)
|
|
40
|
+
.then((data) => {
|
|
41
|
+
if (cancelled) return;
|
|
42
|
+
callbackW(data);
|
|
43
|
+
setState({
|
|
44
|
+
state: "loaded",
|
|
45
|
+
activitySession: data,
|
|
46
|
+
});
|
|
47
|
+
(window as any).capy = data;
|
|
48
|
+
})
|
|
49
|
+
.catch((error) => {
|
|
50
|
+
if (cancelled) return;
|
|
51
|
+
setState({
|
|
52
|
+
state: "error",
|
|
53
|
+
activitySession: null,
|
|
54
|
+
error,
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
return () => {
|
|
58
|
+
cancelled = true;
|
|
59
|
+
setState({
|
|
60
|
+
state: "loading",
|
|
61
|
+
activitySession: null,
|
|
62
|
+
});
|
|
63
|
+
};
|
|
64
|
+
}, [id, loadOptions?.binaryDataType, loadOptions?.readOnly]);
|
|
65
|
+
return state;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Si un handler est passé en props à un composant, il peut changer. Il n'y a pas trop de raison mais
|
|
70
|
+
* rien ne l'interdit.
|
|
71
|
+
* Si un useEffect invoque ce handler, il faudrait que le handler figure dans les dépendances du useEffect.
|
|
72
|
+
* Du coup, si le handler change, le useEffect est ré-exécuté.
|
|
73
|
+
* Ce hook crée un wrapper immuable autour du handler passé en props. Ce wrapper ne change jamais et n'a donc
|
|
74
|
+
* pas besoin de figurer dans les dépendances du useEffect.
|
|
75
|
+
*
|
|
76
|
+
* @param handler
|
|
77
|
+
*/
|
|
78
|
+
export function useHandlerWrapper<H extends () => any>(
|
|
79
|
+
handler?: H,
|
|
80
|
+
): () => ReturnType<H> | void;
|
|
81
|
+
export function useHandlerWrapper<H extends (...args: any[]) => any>(
|
|
82
|
+
handler?: H,
|
|
83
|
+
): (...p: Parameters<H>) => ReturnType<H> | void;
|
|
84
|
+
export function useHandlerWrapper<H extends (...args: any[]) => any>(
|
|
85
|
+
handler?: H,
|
|
86
|
+
): (...p: Parameters<H>) => ReturnType<H> | void {
|
|
87
|
+
const handlerRef = useRef<H>();
|
|
88
|
+
handlerRef.current = handler;
|
|
89
|
+
return useRef((...p: Parameters<H>) => {
|
|
90
|
+
if (null == handlerRef.current) return;
|
|
91
|
+
return handlerRef.current(...p);
|
|
92
|
+
}).current;
|
|
93
|
+
}
|
|
@@ -11,11 +11,13 @@ export type SaveCallback = {
|
|
|
11
11
|
export interface SaverState {
|
|
12
12
|
beforeSave: { [key: string]: CallbackType };
|
|
13
13
|
afterSave: { [key: string]: CallbackType };
|
|
14
|
+
beforeReset: { [key: string]: CallbackType };
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
const initialState: SaverState = {
|
|
17
18
|
beforeSave: {},
|
|
18
19
|
afterSave: {},
|
|
20
|
+
beforeReset: {},
|
|
19
21
|
};
|
|
20
22
|
|
|
21
23
|
// If you are not using async thunks you can use the standalone `createSlice`.
|
|
@@ -36,23 +38,42 @@ export const saverSlice = createAppSlice({
|
|
|
36
38
|
state.afterSave[action.payload.name] = action.payload.callback;
|
|
37
39
|
},
|
|
38
40
|
),
|
|
41
|
+
addBeforeReset: create.reducer(
|
|
42
|
+
(state, action: PayloadAction<SaveCallback>) => {
|
|
43
|
+
state.beforeReset[action.payload.name] = action.payload.callback;
|
|
44
|
+
},
|
|
45
|
+
),
|
|
39
46
|
removeBeforeSave: create.reducer((state, action: PayloadAction<string>) => {
|
|
40
47
|
delete state.beforeSave[action.payload];
|
|
41
48
|
}),
|
|
42
49
|
removeAfterSave: create.reducer((state, action: PayloadAction<string>) => {
|
|
43
50
|
delete state.afterSave[action.payload];
|
|
44
51
|
}),
|
|
52
|
+
removeBeforeReset: create.reducer(
|
|
53
|
+
(state, action: PayloadAction<string>) => {
|
|
54
|
+
delete state.beforeReset[action.payload];
|
|
55
|
+
},
|
|
56
|
+
),
|
|
45
57
|
}),
|
|
46
58
|
// You can define your selectors here. These selectors receive the slice
|
|
47
59
|
// state as their first argument.
|
|
48
60
|
selectors: {
|
|
49
61
|
selectBeforeSave: (saver) => saver.beforeSave,
|
|
50
62
|
selectAfterSave: (saver) => saver.afterSave,
|
|
63
|
+
selectBeforeReset: (saver) => saver.beforeReset,
|
|
51
64
|
},
|
|
52
65
|
});
|
|
53
66
|
|
|
54
67
|
// Action creators are generated for each case reducer function.
|
|
55
|
-
export const {
|
|
68
|
+
export const {
|
|
69
|
+
addBeforeSave,
|
|
70
|
+
removeBeforeSave,
|
|
71
|
+
addAfterSave,
|
|
72
|
+
removeAfterSave,
|
|
73
|
+
addBeforeReset,
|
|
74
|
+
removeBeforeReset,
|
|
75
|
+
} = saverSlice.actions;
|
|
56
76
|
|
|
57
77
|
// Selectors returned by `slice.selectors` take the root state as their first argument.
|
|
58
|
-
export const { selectBeforeSave, selectAfterSave } =
|
|
78
|
+
export const { selectBeforeSave, selectAfterSave, selectBeforeReset } =
|
|
79
|
+
saverSlice.selectors;
|
|
@@ -9,6 +9,7 @@ import ActivityQuickActions from "./ActivityQuickActions";
|
|
|
9
9
|
import useFullscreen from "../../utils/useFullscreen";
|
|
10
10
|
import { useActivityJS } from "../activityJS/ActivityJSProvider";
|
|
11
11
|
import { Dialog } from "primereact/dialog";
|
|
12
|
+
import capytaleUI from "@capytale/activity.js/backend/capytale/ui";
|
|
12
13
|
|
|
13
14
|
const ActivityMenu: React.FC = memo(() => {
|
|
14
15
|
const [settingsSidebarVisible, setSettingsSidebarVisible] = useState(false);
|
|
@@ -21,6 +22,18 @@ const ActivityMenu: React.FC = memo(() => {
|
|
|
21
22
|
return (
|
|
22
23
|
<div className={styles.activityMenu}>
|
|
23
24
|
<ActivityQuickActions />
|
|
25
|
+
<Button
|
|
26
|
+
severity="secondary"
|
|
27
|
+
size="small"
|
|
28
|
+
icon="pi pi-cog" // pi-ellipsis-v
|
|
29
|
+
outlined
|
|
30
|
+
tooltip="Paramètres"
|
|
31
|
+
tooltipOptions={{
|
|
32
|
+
position: "left",
|
|
33
|
+
showDelay: settings.TOOLTIP_SHOW_DELAY,
|
|
34
|
+
}}
|
|
35
|
+
onClick={() => setSettingsSidebarVisible(true)}
|
|
36
|
+
/>
|
|
24
37
|
<Button
|
|
25
38
|
severity="secondary"
|
|
26
39
|
size="small"
|
|
@@ -36,14 +49,14 @@ const ActivityMenu: React.FC = memo(() => {
|
|
|
36
49
|
<Button
|
|
37
50
|
severity="secondary"
|
|
38
51
|
size="small"
|
|
39
|
-
icon="pi pi-
|
|
40
|
-
|
|
41
|
-
tooltip="
|
|
52
|
+
icon={"pi pi-times"}
|
|
53
|
+
text
|
|
54
|
+
tooltip="Retour à la liste des activités"
|
|
42
55
|
tooltipOptions={{
|
|
43
56
|
position: "left",
|
|
44
57
|
showDelay: settings.TOOLTIP_SHOW_DELAY,
|
|
45
58
|
}}
|
|
46
|
-
onClick={() =>
|
|
59
|
+
onClick={() => capytaleUI.closeCapytaleIframe()}
|
|
47
60
|
/>
|
|
48
61
|
<Sidebar
|
|
49
62
|
header="Paramètres"
|
|
@@ -2,6 +2,7 @@ import { Button } from "primereact/button";
|
|
|
2
2
|
import { SplitButton } from "primereact/splitbutton";
|
|
3
3
|
import { ConfirmDialog, confirmDialog } from "primereact/confirmdialog";
|
|
4
4
|
import { Toast } from "primereact/toast";
|
|
5
|
+
import { OverlayPanel } from "primereact/overlaypanel";
|
|
5
6
|
|
|
6
7
|
import styles from "./style.module.scss";
|
|
7
8
|
import { useMemo, useRef } from "react";
|
|
@@ -21,6 +22,9 @@ import {
|
|
|
21
22
|
import { copyToClipboard } from "../../utils/clipboard";
|
|
22
23
|
import settings from "../../settings";
|
|
23
24
|
import { wf } from "@capytale/activity.js/activity/field/workflow";
|
|
25
|
+
import capytaleUI from "@capytale/activity.js/backend/capytale/ui";
|
|
26
|
+
import ButtonDoubleIcon from "../../utils/ButtonDoubleIcon";
|
|
27
|
+
import CardSelector from "../../utils/CardSelector";
|
|
24
28
|
|
|
25
29
|
const CapytaleMenu: React.FC = () => {
|
|
26
30
|
const dispatch = useAppDispatch();
|
|
@@ -39,6 +43,7 @@ const CapytaleMenu: React.FC = () => {
|
|
|
39
43
|
() => windowsSize.width && windowsSize.width < ML,
|
|
40
44
|
[windowsSize.width],
|
|
41
45
|
);
|
|
46
|
+
const isInIframe = useMemo(() => capytaleUI.isInCapytaletIframe(), []);
|
|
42
47
|
const toast = useRef<Toast>(null);
|
|
43
48
|
const changeWorkflow = (value: wf) => {
|
|
44
49
|
dispatch(setWorkflow(value));
|
|
@@ -56,10 +61,25 @@ const CapytaleMenu: React.FC = () => {
|
|
|
56
61
|
accept: () => changeWorkflow("finished"),
|
|
57
62
|
});
|
|
58
63
|
};
|
|
64
|
+
const overlayPanelWorkflow = useRef<OverlayPanel>(null);
|
|
59
65
|
return (
|
|
60
66
|
<>
|
|
61
67
|
<Toast ref={toast} position="bottom-right" />
|
|
62
68
|
<div className={styles.capytaleMenu}>
|
|
69
|
+
{isInIframe && (
|
|
70
|
+
<Button
|
|
71
|
+
label="Retour"
|
|
72
|
+
icon="pi pi-chevron-left"
|
|
73
|
+
size="small"
|
|
74
|
+
outlined
|
|
75
|
+
onClick={() => capytaleUI.closeCapytaleIframe()}
|
|
76
|
+
tooltip="Retour à la liste des activités"
|
|
77
|
+
tooltipOptions={{
|
|
78
|
+
position: "right",
|
|
79
|
+
showDelay: settings.TOOLTIP_SHOW_DELAY,
|
|
80
|
+
}}
|
|
81
|
+
/>
|
|
82
|
+
)}
|
|
63
83
|
{showSaveButton && (
|
|
64
84
|
<Button
|
|
65
85
|
label={isQuiteSmall ? undefined : "Enregistrer"}
|
|
@@ -144,60 +164,64 @@ const CapytaleMenu: React.FC = () => {
|
|
|
144
164
|
/>
|
|
145
165
|
)}
|
|
146
166
|
{mode === "review" && (
|
|
147
|
-
<
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
167
|
+
<div>
|
|
168
|
+
<ButtonDoubleIcon
|
|
169
|
+
severity="secondary"
|
|
170
|
+
size="small"
|
|
171
|
+
outlined
|
|
172
|
+
onClick={(e) => {
|
|
173
|
+
overlayPanelWorkflow.current!.toggle(e);
|
|
174
|
+
}}
|
|
175
|
+
label={
|
|
176
|
+
workflow === "current"
|
|
177
|
+
? "En cours"
|
|
178
|
+
: workflow === "finished"
|
|
179
|
+
? "Rendue"
|
|
180
|
+
: "Corrigée"
|
|
181
|
+
}
|
|
182
|
+
leftIcon={
|
|
183
|
+
"pi " +
|
|
184
|
+
(workflow === "current"
|
|
185
|
+
? "pi pi-pencil"
|
|
186
|
+
: workflow === "finished"
|
|
187
|
+
? "pi pi-envelope"
|
|
188
|
+
: "pi pi-check-square")
|
|
189
|
+
}
|
|
190
|
+
rightIcon="pi pi-angle-down"
|
|
191
|
+
/>
|
|
192
|
+
<OverlayPanel ref={overlayPanelWorkflow}>
|
|
193
|
+
<h3 className={styles.overlayPanelWorkflowTitle}>
|
|
194
|
+
État de la copie
|
|
195
|
+
</h3>
|
|
196
|
+
<CardSelector
|
|
197
|
+
selected={workflow}
|
|
198
|
+
onChange={(option: wf) => {
|
|
199
|
+
changeWorkflow(option);
|
|
200
|
+
|
|
201
|
+
overlayPanelWorkflow.current!.hide();
|
|
202
|
+
}}
|
|
203
|
+
options={[
|
|
204
|
+
{
|
|
205
|
+
value: "current",
|
|
206
|
+
title: "En cours",
|
|
207
|
+
description:
|
|
208
|
+
"L'élève peut modifier sa copie à tout moment.",
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
value: "finished",
|
|
212
|
+
title: "Rendue",
|
|
213
|
+
description: "L'élève ne peut plus modifier sa copie.",
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
value: "corrected",
|
|
217
|
+
title: "Corrigée",
|
|
218
|
+
description:
|
|
219
|
+
"L'élève ne peut plus modifier sa copie et a reçu une correction.",
|
|
220
|
+
},
|
|
221
|
+
]}
|
|
222
|
+
/>
|
|
223
|
+
</OverlayPanel>
|
|
224
|
+
</div>
|
|
201
225
|
)}
|
|
202
226
|
<ConfirmDialog />
|
|
203
227
|
</div>
|
|
@@ -21,7 +21,9 @@ import {
|
|
|
21
21
|
} from "../activityData/activityDataSlice";
|
|
22
22
|
import { ActivitySettingsDisplay } from "../activitySettings";
|
|
23
23
|
import { Button } from "primereact/button";
|
|
24
|
+
import { ConfirmPopup, confirmPopup } from "primereact/confirmpopup";
|
|
24
25
|
import settings from "../../settings";
|
|
26
|
+
import { useReset } from "../activityJS/hooks";
|
|
25
27
|
|
|
26
28
|
type SidebarContentProps = {
|
|
27
29
|
showHelp?: () => void;
|
|
@@ -33,11 +35,12 @@ const SidebarContent: FC<SidebarContentProps> = (props) => {
|
|
|
33
35
|
const orientation = useAppSelector(selectOrientation);
|
|
34
36
|
const themeName = useAppSelector(selectThemeNameOrAuto);
|
|
35
37
|
const sidebarActions = useAppSelector(selectSidebarActions);
|
|
38
|
+
const reset = useReset({ onError: console.error });
|
|
36
39
|
const dispatch = useAppDispatch();
|
|
37
40
|
return (
|
|
38
41
|
<>
|
|
39
|
-
{
|
|
40
|
-
|
|
42
|
+
<div className={styles.sidebarCapytaleActions}>
|
|
43
|
+
{props.showHelp && (
|
|
41
44
|
<Button
|
|
42
45
|
severity="secondary"
|
|
43
46
|
size="small"
|
|
@@ -53,8 +56,38 @@ const SidebarContent: FC<SidebarContentProps> = (props) => {
|
|
|
53
56
|
showDelay: settings.TOOLTIP_SHOW_DELAY,
|
|
54
57
|
}}
|
|
55
58
|
/>
|
|
56
|
-
|
|
57
|
-
|
|
59
|
+
)}
|
|
60
|
+
{reset && (
|
|
61
|
+
<>
|
|
62
|
+
<ConfirmPopup />
|
|
63
|
+
<Button
|
|
64
|
+
severity="secondary"
|
|
65
|
+
size="small"
|
|
66
|
+
icon={"pi pi-undo"}
|
|
67
|
+
onClick={(event) => {
|
|
68
|
+
confirmPopup({
|
|
69
|
+
target: event.currentTarget,
|
|
70
|
+
message:
|
|
71
|
+
"Toutes vos modifications seront perdues. Continuer ?",
|
|
72
|
+
icon: "pi pi-exclamation-triangle",
|
|
73
|
+
defaultFocus: "reject",
|
|
74
|
+
acceptClassName: "p-button-danger",
|
|
75
|
+
accept: reset,
|
|
76
|
+
acceptLabel: "Oui",
|
|
77
|
+
rejectLabel: "Non",
|
|
78
|
+
});
|
|
79
|
+
}}
|
|
80
|
+
outlined
|
|
81
|
+
label="Réinitialiser l'activité"
|
|
82
|
+
tooltip="Revenir à la version de l'enseignant"
|
|
83
|
+
tooltipOptions={{
|
|
84
|
+
position: "left",
|
|
85
|
+
showDelay: settings.TOOLTIP_SHOW_DELAY,
|
|
86
|
+
}}
|
|
87
|
+
/>
|
|
88
|
+
</>
|
|
89
|
+
)}
|
|
90
|
+
</div>
|
|
58
91
|
{sidebarActions.length > 0 && (
|
|
59
92
|
<Fieldset
|
|
60
93
|
legend="Actions"
|
|
@@ -7,6 +7,7 @@ import styles from "./style.module.scss";
|
|
|
7
7
|
import { useMemo } from "react";
|
|
8
8
|
import { useAppSelector } from "../../app/hooks";
|
|
9
9
|
import { selectReturnUrl } from "../activityData/activityDataSlice";
|
|
10
|
+
import capytaleUI from "@capytale/activity.js/backend/capytale/ui";
|
|
10
11
|
|
|
11
12
|
const Navbar: React.FC = () => {
|
|
12
13
|
const returnUrl = useAppSelector(selectReturnUrl);
|
|
@@ -15,12 +16,13 @@ const Navbar: React.FC = () => {
|
|
|
15
16
|
() => windowsSize.width && windowsSize.width < MD,
|
|
16
17
|
[windowsSize.width],
|
|
17
18
|
);
|
|
19
|
+
const isInIframe = useMemo(() => capytaleUI.isInCapytaletIframe(), []);
|
|
18
20
|
const logoText = isSmall ? "C" : "CAPYTALE";
|
|
19
21
|
return (
|
|
20
22
|
<div className={styles.navbar}>
|
|
21
23
|
<div className={styles.navbarContainer}>
|
|
22
24
|
<div className={styles.navbarLogo}>
|
|
23
|
-
<a href={returnUrl}>{logoText}</a>
|
|
25
|
+
{!isInIframe && <a href={returnUrl}>{logoText}</a>}
|
|
24
26
|
</div>
|
|
25
27
|
<CapytaleMenu />
|
|
26
28
|
<ActivityInfo />
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Button, ButtonProps } from "primereact/button";
|
|
2
|
+
import React from "react";
|
|
3
|
+
|
|
4
|
+
type ButtonDoubleIconProps = Exclude<
|
|
5
|
+
ButtonProps,
|
|
6
|
+
"icon" | "children" | "iconPos"
|
|
7
|
+
> & {
|
|
8
|
+
leftIcon: string;
|
|
9
|
+
rightIcon: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const ButtonDoubleIcon: React.FC<ButtonDoubleIconProps> = function ({
|
|
13
|
+
label,
|
|
14
|
+
leftIcon,
|
|
15
|
+
rightIcon,
|
|
16
|
+
...props
|
|
17
|
+
}) {
|
|
18
|
+
return (
|
|
19
|
+
<Button {...props}>
|
|
20
|
+
<span
|
|
21
|
+
className={"p-button-icon p-c p-button-icon-left " + leftIcon}
|
|
22
|
+
data-pc-section="icon"
|
|
23
|
+
></span>
|
|
24
|
+
<span className="p-button-label p-c" data-pc-section="label">
|
|
25
|
+
{label}
|
|
26
|
+
</span>
|
|
27
|
+
<span
|
|
28
|
+
className={"p-button-icon p-c p-button-icon-right " + rightIcon}
|
|
29
|
+
data-pc-section="icon"
|
|
30
|
+
></span>
|
|
31
|
+
</Button>
|
|
32
|
+
);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export default ButtonDoubleIcon;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { RadioButton } from "primereact/radiobutton";
|
|
2
|
+
import styles from "./style.module.scss";
|
|
3
|
+
|
|
4
|
+
type CardSelectorProps<ValueType> = {
|
|
5
|
+
options: { title: string; description: string; value: ValueType }[];
|
|
6
|
+
selected?: ValueType | null;
|
|
7
|
+
onChange: (value: ValueType) => void | Promise<void>;
|
|
8
|
+
};
|
|
9
|
+
const CardSelector = function <ValueType>(props: CardSelectorProps<ValueType>) {
|
|
10
|
+
return (
|
|
11
|
+
<>
|
|
12
|
+
<div className={styles.cardSelector}>
|
|
13
|
+
{props.options.map((option) => (
|
|
14
|
+
<div
|
|
15
|
+
className={styles.cardSelectorOption}
|
|
16
|
+
data-selected={props.selected === option.value}
|
|
17
|
+
onClick={() => {
|
|
18
|
+
props.onChange(option.value);
|
|
19
|
+
}}
|
|
20
|
+
>
|
|
21
|
+
<div className={styles.cardSelectorOptionRadio}>
|
|
22
|
+
<RadioButton checked={props.selected === option.value} />
|
|
23
|
+
</div>
|
|
24
|
+
<div className={styles.cardSelectorOptionContent}>
|
|
25
|
+
<div className={styles.cardSelectorOptionTitle}>
|
|
26
|
+
{option.title}
|
|
27
|
+
</div>
|
|
28
|
+
<div className={styles.cardSelectorOptionDescription}>
|
|
29
|
+
{option.description}
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
))}
|
|
34
|
+
</div>
|
|
35
|
+
</>
|
|
36
|
+
);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export default CardSelector;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
.cardSelector {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-direction: column;
|
|
4
|
+
gap: 1.25rem;
|
|
5
|
+
& :global(.p-card-title) {
|
|
6
|
+
font-size: 1.2rem;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
& :global(.p-card-content) {
|
|
10
|
+
padding: 0.5rem 0;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
& p {
|
|
14
|
+
margin: 0;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.cardSelectorOption {
|
|
19
|
+
padding: 1.25rem;
|
|
20
|
+
display: flex;
|
|
21
|
+
gap: 1.25rem;
|
|
22
|
+
background-color: var(--surface-100);
|
|
23
|
+
box-shadow: none;
|
|
24
|
+
border: 1px solid var(--surface-300);
|
|
25
|
+
border-radius: 6px;
|
|
26
|
+
cursor: pointer;
|
|
27
|
+
transition: background-color 0.2s;
|
|
28
|
+
&[data-selected="true"] {
|
|
29
|
+
background-color: var(--surface-0);
|
|
30
|
+
border-color: var(--primary-color);
|
|
31
|
+
}
|
|
32
|
+
&:hover {
|
|
33
|
+
background-color: var(--surface-0);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.cardSelectorOptionRadio {
|
|
38
|
+
display: flex;
|
|
39
|
+
align-items: center;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.cardSelectorOptionTitle {
|
|
43
|
+
font-size: 1.2rem;
|
|
44
|
+
font-weight: 700;
|
|
45
|
+
margin-bottom: 0.5rem;
|
|
46
|
+
}
|