@capytale/meta-player 0.0.1 → 0.0.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 +2 -1
- package/src/App.tsx +13 -3
- package/src/features/activityData/IsDirtySetter.tsx +8 -2
- package/src/features/activityData/activityDataSlice.ts +24 -6
- package/src/features/activityJS/ActivityJSProvider.tsx +0 -1
- package/src/features/activityJS/Saver.tsx +4 -0
- package/src/features/navbar/CapytaleMenu.tsx +22 -15
- package/src/features/pedago/AnswerSheetEditor.tsx +7 -0
- package/src/features/pedago/InstructionsEditor.tsx +8 -6
- package/src/features/pedago/index.tsx +5 -2
- package/src/index.css +13 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@capytale/meta-player",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"dev": "vite",
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
"react-dom": "^18.2.0",
|
|
25
25
|
"react-html-props": "^2.0.9",
|
|
26
26
|
"react-redux": "^9.1.0",
|
|
27
|
+
"react-reverse-portal": "^2.1.2",
|
|
27
28
|
"screenfull": "^6.0.2"
|
|
28
29
|
},
|
|
29
30
|
"devDependencies": {
|
package/src/App.tsx
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
selectOrientation,
|
|
14
14
|
toggleIsPedagoVisible,
|
|
15
15
|
} from "./features/layout/layoutSlice";
|
|
16
|
-
import { FC, PropsWithChildren } from "react";
|
|
16
|
+
import { FC, PropsWithChildren, useMemo } from "react";
|
|
17
17
|
import { Tooltip } from "primereact/tooltip";
|
|
18
18
|
import {
|
|
19
19
|
selectHasGradingOrComments,
|
|
@@ -23,6 +23,8 @@ import {
|
|
|
23
23
|
import settings from "./settings";
|
|
24
24
|
import ReviewNavbar from "./features/navbar/ReviewNavbar";
|
|
25
25
|
|
|
26
|
+
import * as portals from "react-reverse-portal";
|
|
27
|
+
|
|
26
28
|
type AppProps = PropsWithChildren<{}>;
|
|
27
29
|
|
|
28
30
|
const App: FC<AppProps> = (props) => {
|
|
@@ -34,6 +36,9 @@ const App: FC<AppProps> = (props) => {
|
|
|
34
36
|
const hasGradingOrComments = useAppSelector(selectHasGradingOrComments);
|
|
35
37
|
const hasPedago = hasInstructions || hasGradingOrComments;
|
|
36
38
|
const dispatch = useAppDispatch();
|
|
39
|
+
|
|
40
|
+
const portalNode = useMemo(() => portals.createHtmlPortalNode(), []);
|
|
41
|
+
|
|
37
42
|
return (
|
|
38
43
|
<div
|
|
39
44
|
className={classNames(
|
|
@@ -42,6 +47,9 @@ const App: FC<AppProps> = (props) => {
|
|
|
42
47
|
isHorizontal ? "layout-horizontal" : "layout-vertical",
|
|
43
48
|
)}
|
|
44
49
|
>
|
|
50
|
+
<portals.InPortal node={portalNode}>
|
|
51
|
+
<div id="meta-player-content">{props.children}</div>
|
|
52
|
+
</portals.InPortal>
|
|
45
53
|
<div>
|
|
46
54
|
<Navbar />
|
|
47
55
|
{mode === "review" && <ReviewNavbar />}
|
|
@@ -55,7 +63,7 @@ const App: FC<AppProps> = (props) => {
|
|
|
55
63
|
<Pedago key="pedago" />
|
|
56
64
|
</SplitterPanel>
|
|
57
65
|
<SplitterPanel minSize={40} size={70}>
|
|
58
|
-
{
|
|
66
|
+
<portals.OutPortal node={portalNode} />
|
|
59
67
|
</SplitterPanel>
|
|
60
68
|
</Splitter>
|
|
61
69
|
)}
|
|
@@ -106,7 +114,9 @@ const App: FC<AppProps> = (props) => {
|
|
|
106
114
|
mouseTrack={!isHorizontal}
|
|
107
115
|
/>
|
|
108
116
|
</div>
|
|
109
|
-
<div className={styles.hiddenPedagoContent}>
|
|
117
|
+
<div className={styles.hiddenPedagoContent}>
|
|
118
|
+
<portals.OutPortal node={portalNode} />
|
|
119
|
+
</div>
|
|
110
120
|
</div>
|
|
111
121
|
)}
|
|
112
122
|
</div>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { FC, useEffect } from "react";
|
|
2
|
-
import {
|
|
2
|
+
import { setIsPlayerDirty } from "./activityDataSlice";
|
|
3
3
|
import { useAppDispatch } from "../../app/hooks";
|
|
4
4
|
|
|
5
5
|
type IsDirtySetterProps = {
|
|
@@ -9,9 +9,15 @@ type IsDirtySetterProps = {
|
|
|
9
9
|
const IsDirtySetter: FC<IsDirtySetterProps> = (props) => {
|
|
10
10
|
const dispatch = useAppDispatch();
|
|
11
11
|
useEffect(() => {
|
|
12
|
-
dispatch(
|
|
12
|
+
dispatch(setIsPlayerDirty(props.isDirty));
|
|
13
13
|
}, [props]);
|
|
14
14
|
return null;
|
|
15
15
|
};
|
|
16
16
|
|
|
17
|
+
const notifyIsDirty = (isDirty: boolean = true) => {
|
|
18
|
+
const dispatch = useAppDispatch();
|
|
19
|
+
dispatch(setIsPlayerDirty(isDirty));
|
|
20
|
+
};
|
|
21
|
+
|
|
17
22
|
export default IsDirtySetter;
|
|
23
|
+
export { notifyIsDirty };
|
|
@@ -29,7 +29,6 @@ export type Icon = {
|
|
|
29
29
|
interface ActivityJSData {
|
|
30
30
|
title: string;
|
|
31
31
|
mode: ActivityMode;
|
|
32
|
-
isDirty: boolean;
|
|
33
32
|
returnUrl: string;
|
|
34
33
|
helpUrl: string;
|
|
35
34
|
code: string;
|
|
@@ -54,6 +53,8 @@ interface UIState {
|
|
|
54
53
|
canSaveInstructions: boolean;
|
|
55
54
|
canSaveAnswerSheet: boolean;
|
|
56
55
|
saveState: SaveState;
|
|
56
|
+
isPlayerDirty: boolean;
|
|
57
|
+
isMPDirty: boolean;
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
export interface ActivityDataState
|
|
@@ -64,7 +65,6 @@ export interface ActivityDataState
|
|
|
64
65
|
const initialState: ActivityDataState = {
|
|
65
66
|
title: "",
|
|
66
67
|
mode: "view",
|
|
67
|
-
isDirty: false,
|
|
68
68
|
returnUrl: "",
|
|
69
69
|
helpUrl: "",
|
|
70
70
|
code: "",
|
|
@@ -91,6 +91,8 @@ const initialState: ActivityDataState = {
|
|
|
91
91
|
canSaveInstructions: true,
|
|
92
92
|
canSaveAnswerSheet: true,
|
|
93
93
|
saveState: "idle",
|
|
94
|
+
isPlayerDirty: false,
|
|
95
|
+
isMPDirty: false,
|
|
94
96
|
};
|
|
95
97
|
|
|
96
98
|
// If you are not using async thunks you can use the standalone `createSlice`.
|
|
@@ -169,8 +171,13 @@ export const activityDataSlice = createAppSlice({
|
|
|
169
171
|
setWorkflow: create.reducer((state, action: PayloadAction<wf>) => {
|
|
170
172
|
state.workflow = action.payload;
|
|
171
173
|
}),
|
|
172
|
-
|
|
173
|
-
state
|
|
174
|
+
setIsPlayerDirty: create.reducer(
|
|
175
|
+
(state, action: PayloadAction<boolean>) => {
|
|
176
|
+
state.isPlayerDirty = action.payload;
|
|
177
|
+
},
|
|
178
|
+
),
|
|
179
|
+
setIsMPDirty: create.reducer((state, action: PayloadAction<boolean>) => {
|
|
180
|
+
state.isMPDirty = action.payload;
|
|
174
181
|
}),
|
|
175
182
|
}),
|
|
176
183
|
// You can define your selectors here. These selectors receive the slice
|
|
@@ -198,7 +205,9 @@ export const activityDataSlice = createAppSlice({
|
|
|
198
205
|
selectActivityNid: (data) => data.activityNid,
|
|
199
206
|
selectIcon: (data) => data.icon,
|
|
200
207
|
selectWorkflow: (data) => data.workflow,
|
|
201
|
-
|
|
208
|
+
selectIsPlayerDirty: (data) => data.isPlayerDirty,
|
|
209
|
+
selectIsMPDirty: (data) => data.isMPDirty,
|
|
210
|
+
selectIsDirty: (data) => data.isPlayerDirty || data.isMPDirty,
|
|
202
211
|
|
|
203
212
|
selectCanChoosePedagoLayout: (data) =>
|
|
204
213
|
data.pedagoLayout === "default-horizontal" ||
|
|
@@ -206,6 +215,11 @@ export const activityDataSlice = createAppSlice({
|
|
|
206
215
|
|
|
207
216
|
selectCanChooseTheme: (data) =>
|
|
208
217
|
data.supportsDarkTheme && data.supportsLightTheme,
|
|
218
|
+
|
|
219
|
+
selectShowSaveButton: (data) =>
|
|
220
|
+
data.mode === "create" ||
|
|
221
|
+
data.mode === "review" ||
|
|
222
|
+
(data.mode === "assignment" && data.workflow === "current"),
|
|
209
223
|
},
|
|
210
224
|
});
|
|
211
225
|
|
|
@@ -222,7 +236,8 @@ export const {
|
|
|
222
236
|
setGrading,
|
|
223
237
|
setComments,
|
|
224
238
|
setWorkflow,
|
|
225
|
-
|
|
239
|
+
setIsPlayerDirty,
|
|
240
|
+
setIsMPDirty,
|
|
226
241
|
} = activityDataSlice.actions;
|
|
227
242
|
|
|
228
243
|
// Selectors returned by `slice.selectors` take the root state as their first argument.
|
|
@@ -243,8 +258,11 @@ export const {
|
|
|
243
258
|
selectActivityNid,
|
|
244
259
|
selectIcon,
|
|
245
260
|
selectWorkflow,
|
|
261
|
+
selectIsPlayerDirty,
|
|
262
|
+
selectIsMPDirty,
|
|
246
263
|
selectIsDirty,
|
|
247
264
|
|
|
248
265
|
selectCanChoosePedagoLayout,
|
|
249
266
|
selectCanChooseTheme,
|
|
267
|
+
selectShowSaveButton,
|
|
250
268
|
} = activityDataSlice.selectors;
|
|
@@ -7,6 +7,8 @@ import {
|
|
|
7
7
|
selectInstructions,
|
|
8
8
|
selectSaveState,
|
|
9
9
|
selectWorkflow,
|
|
10
|
+
setIsMPDirty,
|
|
11
|
+
setIsPlayerDirty,
|
|
10
12
|
setSaveState,
|
|
11
13
|
} from "../activityData/activityDataSlice";
|
|
12
14
|
import { useActivityJS } from "./ActivityJSProvider";
|
|
@@ -94,6 +96,8 @@ const Saver: FC<{}> = () => {
|
|
|
94
96
|
const saveData = await ab.save();
|
|
95
97
|
console.log("Save return data", saveData);
|
|
96
98
|
dispatch(setSaveState("idle"));
|
|
99
|
+
dispatch(setIsPlayerDirty(false));
|
|
100
|
+
dispatch(setIsMPDirty(false));
|
|
97
101
|
toast.current!.show({
|
|
98
102
|
summary: "Sauvegarde réussie",
|
|
99
103
|
detail: "L'activité a bien été enregistrée.",
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
selectMode,
|
|
13
13
|
selectSaveState,
|
|
14
14
|
selectSharingInfo,
|
|
15
|
+
selectShowSaveButton,
|
|
15
16
|
selectWorkflow,
|
|
16
17
|
setSaveState,
|
|
17
18
|
setWorkflow,
|
|
@@ -28,6 +29,7 @@ const CapytaleMenu: React.FC = () => {
|
|
|
28
29
|
const mode = useAppSelector(selectMode);
|
|
29
30
|
const workflow = useAppSelector(selectWorkflow);
|
|
30
31
|
const isDirty = useAppSelector(selectIsDirty);
|
|
32
|
+
const showSaveButton = useAppSelector(selectShowSaveButton);
|
|
31
33
|
const isLarge = useMemo(
|
|
32
34
|
() => windowsSize.width && windowsSize.width >= XL,
|
|
33
35
|
[windowsSize.width],
|
|
@@ -54,21 +56,26 @@ const CapytaleMenu: React.FC = () => {
|
|
|
54
56
|
};
|
|
55
57
|
return (
|
|
56
58
|
<div className={styles.capytaleMenu}>
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
59
|
+
{showSaveButton && (
|
|
60
|
+
<Button
|
|
61
|
+
label={isQuiteSmall ? undefined : "Enregistrer"}
|
|
62
|
+
disabled={!isDirty}
|
|
63
|
+
severity={"warning"}
|
|
64
|
+
size="small"
|
|
65
|
+
icon="pi pi-save"
|
|
66
|
+
loading={saveState !== "idle"}
|
|
67
|
+
tooltip={isDirty ? "Enregistrer l'activité" : "Rien à enregistrer"}
|
|
68
|
+
tooltipOptions={{
|
|
69
|
+
position: "bottom",
|
|
70
|
+
showDelay: settings.TOOLTIP_SHOW_DELAY,
|
|
71
|
+
showOnDisabled: true,
|
|
72
|
+
}}
|
|
73
|
+
onClick={() => {
|
|
74
|
+
dispatch(setSaveState("should-save"));
|
|
75
|
+
}}
|
|
76
|
+
/>
|
|
77
|
+
)}
|
|
78
|
+
|
|
72
79
|
{mode === "create" && (
|
|
73
80
|
<SplitButton
|
|
74
81
|
label={isLarge ? sharingInfo.code : undefined}
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
selectAnswerSheetContent,
|
|
5
5
|
selectMode,
|
|
6
6
|
setCanSaveAnswerSheet,
|
|
7
|
+
setIsMPDirty,
|
|
7
8
|
setLexicalAnswerSheetState,
|
|
8
9
|
} from "../activityData/activityDataSlice";
|
|
9
10
|
import { forwardRef, useImperativeHandle, useRef } from "react";
|
|
@@ -18,6 +19,7 @@ const AnswerSheetEditor: React.FC = forwardRef((_props, ref) => {
|
|
|
18
19
|
const isEditable =
|
|
19
20
|
mode === "create" || mode === "assignment" || mode === "review";
|
|
20
21
|
const initialEditorState = useAppSelector(selectAnswerSheetContent);
|
|
22
|
+
const initialStateOnChangeDone = useRef<boolean>(false);
|
|
21
23
|
|
|
22
24
|
useImperativeHandle(ref, () => {
|
|
23
25
|
return {
|
|
@@ -37,6 +39,11 @@ const AnswerSheetEditor: React.FC = forwardRef((_props, ref) => {
|
|
|
37
39
|
jsonSizeLimit={settings.STATEMENT_MAX_SIZE}
|
|
38
40
|
onChange={(editorState) => {
|
|
39
41
|
dispatch(setLexicalAnswerSheetState(editorState));
|
|
42
|
+
if (initialStateOnChangeDone.current) {
|
|
43
|
+
dispatch(setIsMPDirty(true));
|
|
44
|
+
} else {
|
|
45
|
+
initialStateOnChangeDone.current = true;
|
|
46
|
+
}
|
|
40
47
|
}}
|
|
41
48
|
onJsonSizeLimitExceeded={() => {
|
|
42
49
|
dispatch(setCanSaveAnswerSheet(false));
|
|
@@ -4,14 +4,10 @@ import {
|
|
|
4
4
|
selectInstructions,
|
|
5
5
|
selectMode,
|
|
6
6
|
setCanSaveInstructions,
|
|
7
|
+
setIsMPDirty,
|
|
7
8
|
setLexicalInstructionsState,
|
|
8
9
|
} from "../activityData/activityDataSlice";
|
|
9
|
-
import {
|
|
10
|
-
forwardRef,
|
|
11
|
-
useImperativeHandle,
|
|
12
|
-
useMemo,
|
|
13
|
-
useRef,
|
|
14
|
-
} from "react";
|
|
10
|
+
import { forwardRef, useImperativeHandle, useMemo, useRef } from "react";
|
|
15
11
|
import { Toast } from "primereact/toast";
|
|
16
12
|
import settings from "../../settings";
|
|
17
13
|
|
|
@@ -34,6 +30,7 @@ const InstructionsEditor: React.FC = forwardRef((_props, ref) => {
|
|
|
34
30
|
? initialInstructions?.value
|
|
35
31
|
: undefined;
|
|
36
32
|
}, [initialInstructions]);
|
|
33
|
+
const initialStateOnChangeDone = useRef<boolean>(false);
|
|
37
34
|
|
|
38
35
|
useImperativeHandle(ref, () => {
|
|
39
36
|
return {
|
|
@@ -54,6 +51,11 @@ const InstructionsEditor: React.FC = forwardRef((_props, ref) => {
|
|
|
54
51
|
jsonSizeLimit={settings.STATEMENT_MAX_SIZE}
|
|
55
52
|
onChange={(editorState) => {
|
|
56
53
|
dispatch(setLexicalInstructionsState(editorState));
|
|
54
|
+
if (initialStateOnChangeDone.current) {
|
|
55
|
+
dispatch(setIsMPDirty(true));
|
|
56
|
+
} else {
|
|
57
|
+
initialStateOnChangeDone.current = true;
|
|
58
|
+
}
|
|
57
59
|
}}
|
|
58
60
|
onJsonSizeLimitExceeded={() => {
|
|
59
61
|
dispatch(setCanSaveInstructions(false));
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
selectMode,
|
|
22
22
|
setComments,
|
|
23
23
|
setGrading,
|
|
24
|
+
setIsMPDirty,
|
|
24
25
|
} from "../activityData/activityDataSlice";
|
|
25
26
|
import { ChangeEventHandler } from "react";
|
|
26
27
|
import settings from "../../settings";
|
|
@@ -45,15 +46,17 @@ const Pedago: React.FC<DivProps> = ({ className, ...props }) => {
|
|
|
45
46
|
const handleCommentsChange: ChangeEventHandler<HTMLTextAreaElement> = (
|
|
46
47
|
event,
|
|
47
48
|
) => {
|
|
48
|
-
console.log("Comments change:", event.target.value);
|
|
49
49
|
dispatch(setComments(event.target.value));
|
|
50
|
+
dispatch(setIsMPDirty(true));
|
|
51
|
+
console.log("51");
|
|
50
52
|
};
|
|
51
53
|
|
|
52
54
|
const handleGradingChange: ChangeEventHandler<HTMLTextAreaElement> = (
|
|
53
55
|
event,
|
|
54
56
|
) => {
|
|
55
|
-
console.log("Grading change:", event.target.value);
|
|
56
57
|
dispatch(setGrading(event.target.value));
|
|
58
|
+
dispatch(setIsMPDirty(true));
|
|
59
|
+
console.log("59");
|
|
57
60
|
};
|
|
58
61
|
|
|
59
62
|
return (
|
package/src/index.css
CHANGED
|
@@ -12,7 +12,9 @@ code {
|
|
|
12
12
|
monospace;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
html,
|
|
15
|
+
html,
|
|
16
|
+
body,
|
|
17
|
+
#root {
|
|
16
18
|
height: 100%;
|
|
17
19
|
}
|
|
18
20
|
|
|
@@ -20,11 +22,18 @@ html, body, #root {
|
|
|
20
22
|
background-color: var(--surface-200);
|
|
21
23
|
}
|
|
22
24
|
|
|
23
|
-
.p-splitter-gutter-handle,
|
|
25
|
+
.p-splitter-gutter-handle,
|
|
26
|
+
.p-splitter-gutter-resizing {
|
|
24
27
|
background-color: var(--surface-400);
|
|
25
28
|
}
|
|
26
29
|
|
|
27
|
-
*:has(
|
|
30
|
+
*:has(> .editor-shell) {
|
|
31
|
+
/* Rich text editor */
|
|
28
32
|
background-color: white;
|
|
29
33
|
color-scheme: light;
|
|
30
|
-
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
#meta-player-content,
|
|
37
|
+
:has(> #meta-player-content) {
|
|
38
|
+
height: 100%;
|
|
39
|
+
}
|