@capytale/meta-player 0.5.1 → 0.5.3
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 +1 -1
- package/src/App.tsx +5 -2
- package/src/features/functionalities/AttachedFilesFunctionality.ts +4 -8
- package/src/features/functionalities/functionalitiesSlice.ts +29 -9
- package/src/features/functionalities/hooks.ts +15 -9
- package/src/features/navbar/ActivityInfo.tsx +14 -3
- package/src/features/navbar/ActivityMenu.tsx +13 -2
- package/src/features/navbar/ActivityQuickActions.tsx +1 -0
- package/src/features/navbar/ActivitySidebarActions.tsx +1 -0
- package/src/features/navbar/AttachedFilesSidebarContent.module.scss +17 -0
- package/src/features/navbar/AttachedFilesSidebarContent.tsx +89 -36
- package/src/features/navbar/CapytaleMenu.tsx +59 -39
- package/src/features/navbar/Countdown.tsx +9 -1
- package/src/features/navbar/GradingNav.tsx +5 -2
- package/src/features/navbar/ReviewNavbar.tsx +1 -1
- package/src/features/navbar/SettingsSidebarContent.tsx +7 -0
- package/src/features/navbar/index.tsx +5 -1
- package/src/features/pedago/PedagoCommands.tsx +4 -3
- package/src/features/pedago/index.tsx +4 -0
- package/src/index.css +4 -0
- package/src/index.tsx +4 -1
- package/src/utils/CardSelector.tsx +2 -0
package/package.json
CHANGED
package/src/App.tsx
CHANGED
|
@@ -34,7 +34,8 @@ const App: FC<AppProps> = (props) => {
|
|
|
34
34
|
const isPedagoVisible = useAppSelector(selectIsPedagoVisible) as boolean;
|
|
35
35
|
const hasInstructions = useAppSelector(selectHasInstructions);
|
|
36
36
|
const hasGradingOrComments = useAppSelector(selectHasGradingOrComments);
|
|
37
|
-
const hasPedago =
|
|
37
|
+
const hasPedago =
|
|
38
|
+
hasInstructions || hasGradingOrComments || mode === "review";
|
|
38
39
|
const dispatch = useAppDispatch();
|
|
39
40
|
const showPedago = hasPedago && isPedagoVisible;
|
|
40
41
|
const isDirty = useAppSelector(selectIsDirty);
|
|
@@ -118,7 +119,9 @@ const App: FC<AppProps> = (props) => {
|
|
|
118
119
|
</SplitterPanel>
|
|
119
120
|
<SplitterPanel minSize={40} size={70} className={styles.contentPanel}>
|
|
120
121
|
<div className="meta-player-content-cover"></div>
|
|
121
|
-
<div id="meta-player-content">
|
|
122
|
+
<div id="meta-player-content" aria-label="Activité">
|
|
123
|
+
{props.children}
|
|
124
|
+
</div>
|
|
122
125
|
</SplitterPanel>
|
|
123
126
|
</Splitter>
|
|
124
127
|
</div>
|
|
@@ -2,22 +2,18 @@ import { FC, useEffect } from "react";
|
|
|
2
2
|
import { AttachedFilesOptions, setAttachedFilesOptions } from "./functionalitiesSlice";
|
|
3
3
|
import { useAppDispatch } from "../../app/hooks";
|
|
4
4
|
|
|
5
|
-
type AttachedFilesFunctionalityProps =
|
|
6
|
-
options: Omit<AttachedFilesOptions, "enabled">;
|
|
7
|
-
};
|
|
5
|
+
type AttachedFilesFunctionalityProps = Omit<AttachedFilesOptions, "enabled">;
|
|
8
6
|
|
|
9
|
-
const AttachedFilesFunctionality: FC<AttachedFilesFunctionalityProps> = ({
|
|
10
|
-
options,
|
|
11
|
-
}) => {
|
|
7
|
+
const AttachedFilesFunctionality: FC<AttachedFilesFunctionalityProps> = (props) => {
|
|
12
8
|
const dispatch = useAppDispatch();
|
|
13
9
|
useEffect(() => {
|
|
14
10
|
dispatch(setAttachedFilesOptions({
|
|
15
|
-
...
|
|
11
|
+
...props,
|
|
16
12
|
enabled: true,
|
|
17
13
|
}));
|
|
18
14
|
return () => {
|
|
19
15
|
dispatch(setAttachedFilesOptions({
|
|
20
|
-
...
|
|
16
|
+
...props,
|
|
21
17
|
enabled: false,
|
|
22
18
|
}));
|
|
23
19
|
};
|
|
@@ -4,26 +4,39 @@ import { createAppSlice } from "../../app/createAppSlice";
|
|
|
4
4
|
export type AttachedFileData = {
|
|
5
5
|
name: string;
|
|
6
6
|
isTemporary: boolean;
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
downloadMode: "default" | "custom" | "none";
|
|
8
|
+
copyLinkMode: "default" | "custom" | "none";
|
|
9
|
+
interactMode: "download" | "preview" | "custom" | "none";
|
|
10
10
|
urlOrId: string;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
export type UploadedFileInfo = {
|
|
14
|
+
source: URL,
|
|
15
|
+
name: string,
|
|
16
|
+
size: number,
|
|
17
|
+
file: File,
|
|
18
|
+
};
|
|
19
|
+
|
|
13
20
|
export type AttachedFilesOptions = {
|
|
14
21
|
enabled: boolean;
|
|
15
22
|
uploadTemporaryFiles?: {
|
|
16
|
-
|
|
17
|
-
|
|
23
|
+
uploadButtonLabel?: string;
|
|
24
|
+
mimeTypes?: string[];
|
|
25
|
+
multiple: boolean;
|
|
26
|
+
handler: (fileInfos: UploadedFileInfo[]) => any;
|
|
18
27
|
};
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
28
|
+
listFilesMiddleware?: (originalFiles: AttachedFileData[]) => AttachedFileData[];
|
|
29
|
+
customHandlers?: {
|
|
30
|
+
downloadFile?: ((originalFile: AttachedFileData) => any);
|
|
31
|
+
copyFileLink?: ((originalFile: AttachedFileData) => any);
|
|
32
|
+
interactWithFile?: ((originalFile: AttachedFileData) => any);
|
|
22
33
|
};
|
|
34
|
+
onViewFiles?: () => any;
|
|
23
35
|
};
|
|
24
36
|
|
|
25
37
|
export interface FunctionalitiesState {
|
|
26
38
|
attachedFilesOptions: AttachedFilesOptions;
|
|
39
|
+
attachedFilesRefresher: number;
|
|
27
40
|
}
|
|
28
41
|
|
|
29
42
|
export const defaultAttachedFilesOptions: AttachedFilesOptions = {
|
|
@@ -31,7 +44,8 @@ export const defaultAttachedFilesOptions: AttachedFilesOptions = {
|
|
|
31
44
|
};
|
|
32
45
|
|
|
33
46
|
export const initialState: FunctionalitiesState = {
|
|
34
|
-
attachedFilesOptions: defaultAttachedFilesOptions
|
|
47
|
+
attachedFilesOptions: defaultAttachedFilesOptions,
|
|
48
|
+
attachedFilesRefresher: 0,
|
|
35
49
|
};
|
|
36
50
|
|
|
37
51
|
// If you are not using async thunks you can use the standalone `createSlice`.
|
|
@@ -44,22 +58,28 @@ export const functionalitiesSlice = createAppSlice({
|
|
|
44
58
|
setAttachedFilesOptions: create.reducer((state, action: PayloadAction<AttachedFilesOptions>) => {
|
|
45
59
|
state.attachedFilesOptions = action.payload;
|
|
46
60
|
}),
|
|
61
|
+
refreshAttachedFiles: create.reducer((state) => {
|
|
62
|
+
state.attachedFilesRefresher += 1;
|
|
63
|
+
}),
|
|
47
64
|
}),
|
|
48
65
|
// You can define your selectors here. These selectors receive the slice
|
|
49
66
|
// state as their first argument.
|
|
50
67
|
selectors: {
|
|
51
68
|
selectAttachedFilesOptions: (state) => state.attachedFilesOptions,
|
|
52
69
|
selectAttachedFilesEnabled: (state) => state.attachedFilesOptions.enabled,
|
|
70
|
+
selectAttachedFilesRefresher: (state) => state.attachedFilesRefresher,
|
|
53
71
|
},
|
|
54
72
|
});
|
|
55
73
|
|
|
56
74
|
// Action creators are generated for each case reducer function.
|
|
57
75
|
export const {
|
|
58
76
|
setAttachedFilesOptions,
|
|
77
|
+
refreshAttachedFiles,
|
|
59
78
|
} = functionalitiesSlice.actions;
|
|
60
79
|
|
|
61
80
|
// Selectors returned by `slice.selectors` take the root state as their first argument.
|
|
62
81
|
export const {
|
|
63
82
|
selectAttachedFilesOptions,
|
|
64
83
|
selectAttachedFilesEnabled,
|
|
84
|
+
selectAttachedFilesRefresher,
|
|
65
85
|
} = functionalitiesSlice.selectors;
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
import { useMemo } from "react";
|
|
2
|
-
import { useAppSelector } from "../../app/hooks"
|
|
2
|
+
import { useAppDispatch, useAppSelector } from "../../app/hooks"
|
|
3
3
|
import { useActivityJS } from "../activityJS/ActivityJSProvider";
|
|
4
|
-
import { AttachedFileData, selectAttachedFilesOptions } from "./functionalitiesSlice"
|
|
4
|
+
import { AttachedFileData, refreshAttachedFiles, selectAttachedFilesOptions, selectAttachedFilesRefresher } from "./functionalitiesSlice"
|
|
5
5
|
|
|
6
6
|
export const useAttachedFiles = () => {
|
|
7
7
|
const attachedFilesOptions = useAppSelector(selectAttachedFilesOptions);
|
|
8
|
+
const attachedFilesRefresher = useAppSelector(selectAttachedFilesRefresher);
|
|
8
9
|
|
|
9
10
|
const activityJS = useActivityJS();
|
|
10
11
|
const files = useMemo(
|
|
11
12
|
() =>
|
|
12
13
|
activityJS.activitySession?.activityBunch.activityNode.attached_files
|
|
13
14
|
.items || [],
|
|
14
|
-
[activityJS],
|
|
15
|
+
[activityJS, attachedFilesRefresher],
|
|
15
16
|
);
|
|
16
17
|
const filesData = useMemo<AttachedFileData[]>(() => {
|
|
17
18
|
return files.map((file) => {
|
|
@@ -20,19 +21,24 @@ export const useAttachedFiles = () => {
|
|
|
20
21
|
name: name,
|
|
21
22
|
urlOrId: file,
|
|
22
23
|
isTemporary: false,
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
downloadMode: "default",
|
|
25
|
+
copyLinkMode: "default",
|
|
26
|
+
interactMode: "preview",
|
|
26
27
|
};
|
|
27
28
|
});
|
|
28
29
|
}, [files]);
|
|
29
30
|
|
|
30
31
|
const treatedFilesData = useMemo(() => {
|
|
31
|
-
if (!attachedFilesOptions.enabled || !attachedFilesOptions.
|
|
32
|
+
if (!attachedFilesOptions.enabled || !attachedFilesOptions.listFilesMiddleware) {
|
|
32
33
|
return filesData;
|
|
33
34
|
}
|
|
34
|
-
return attachedFilesOptions.
|
|
35
|
-
}, [filesData, attachedFilesOptions]);
|
|
35
|
+
return attachedFilesOptions.listFilesMiddleware(filesData);
|
|
36
|
+
}, [filesData, attachedFilesOptions, attachedFilesRefresher]);
|
|
36
37
|
|
|
37
38
|
return treatedFilesData;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const useRefreshAttachedFiles = () => {
|
|
42
|
+
const dispatch = useAppDispatch();
|
|
43
|
+
return () => dispatch(refreshAttachedFiles());
|
|
38
44
|
}
|
|
@@ -15,12 +15,22 @@ const ActivityInfo: React.FC = () => {
|
|
|
15
15
|
return (
|
|
16
16
|
<div className={styles.activityInfo}>
|
|
17
17
|
<div className={styles.activityInfoText}>
|
|
18
|
-
<div
|
|
18
|
+
<div
|
|
19
|
+
className={styles.activityInfoTitle}
|
|
20
|
+
aria-label="Titre de l'activité"
|
|
21
|
+
>
|
|
22
|
+
{activityInfo.title}
|
|
23
|
+
</div>
|
|
19
24
|
{mode === "assignment" && studentName && (
|
|
20
|
-
<div
|
|
25
|
+
<div
|
|
26
|
+
className={styles.activityInfoStudentName}
|
|
27
|
+
aria-label="Nom de l'élève"
|
|
28
|
+
>
|
|
29
|
+
{studentName}
|
|
30
|
+
</div>
|
|
21
31
|
)}
|
|
22
32
|
{mode === "create" && (
|
|
23
|
-
<div className={styles.activityInfoStudentName}>
|
|
33
|
+
<div className={styles.activityInfoStudentName} aria-label="Mode">
|
|
24
34
|
Création d'activité
|
|
25
35
|
</div>
|
|
26
36
|
)}
|
|
@@ -28,6 +38,7 @@ const ActivityInfo: React.FC = () => {
|
|
|
28
38
|
{icon?.path && (
|
|
29
39
|
<img
|
|
30
40
|
className={styles.activityLogo}
|
|
41
|
+
aria-label="Logo de l'activité"
|
|
31
42
|
src={icon.path}
|
|
32
43
|
style={{
|
|
33
44
|
backgroundColor: icon.style
|
|
@@ -15,7 +15,10 @@ import { selectHasAntiCheat } from "../activityData/activityDataSlice";
|
|
|
15
15
|
import AttachedFilesSidebarContent from "./AttachedFilesSidebarContent";
|
|
16
16
|
import { Badge } from "primereact/badge";
|
|
17
17
|
import { useAttachedFiles } from "../functionalities/hooks";
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
selectAttachedFilesEnabled,
|
|
20
|
+
selectAttachedFilesOptions,
|
|
21
|
+
} from "../functionalities/functionalitiesSlice";
|
|
19
22
|
|
|
20
23
|
const ActivityMenu: React.FC = memo(() => {
|
|
21
24
|
const [settingsSidebarVisible, setSettingsSidebarVisible] = useState(false);
|
|
@@ -31,11 +34,13 @@ const ActivityMenu: React.FC = memo(() => {
|
|
|
31
34
|
const hasAntiCheat = useAppSelector(selectHasAntiCheat);
|
|
32
35
|
const attachedFiles = useAttachedFiles();
|
|
33
36
|
const attachedFilesEnabled = useAppSelector(selectAttachedFilesEnabled);
|
|
37
|
+
const attachedFilesOptions = useAppSelector(selectAttachedFilesOptions);
|
|
34
38
|
return (
|
|
35
39
|
<div className={styles.activityMenu}>
|
|
36
40
|
<ActivityQuickActions />
|
|
37
41
|
{attachedFilesEnabled && (
|
|
38
42
|
<Button
|
|
43
|
+
aria-label="Afficher les fichiers joints"
|
|
39
44
|
severity="secondary"
|
|
40
45
|
size="small"
|
|
41
46
|
icon={
|
|
@@ -52,10 +57,14 @@ const ActivityMenu: React.FC = memo(() => {
|
|
|
52
57
|
showDelay: settings.TOOLTIP_SHOW_DELAY,
|
|
53
58
|
}}
|
|
54
59
|
className="p-overlay-badge"
|
|
55
|
-
onClick={() =>
|
|
60
|
+
onClick={() => {
|
|
61
|
+
attachedFilesOptions.onViewFiles?.();
|
|
62
|
+
setAttachedFilesSidebarVisible(true);
|
|
63
|
+
}}
|
|
56
64
|
/>
|
|
57
65
|
)}
|
|
58
66
|
<Button
|
|
67
|
+
aria-label="Afficher les paramètres"
|
|
59
68
|
severity="secondary"
|
|
60
69
|
size="small"
|
|
61
70
|
icon="pi pi-cog" // pi-ellipsis-v
|
|
@@ -69,6 +78,7 @@ const ActivityMenu: React.FC = memo(() => {
|
|
|
69
78
|
/>
|
|
70
79
|
{!hasAntiCheat && (
|
|
71
80
|
<Button
|
|
81
|
+
aria-label="Mettre en plein écran"
|
|
72
82
|
severity="secondary"
|
|
73
83
|
size="small"
|
|
74
84
|
icon={isFullscreen ? "pi pi-expand" : "pi pi-expand"}
|
|
@@ -83,6 +93,7 @@ const ActivityMenu: React.FC = memo(() => {
|
|
|
83
93
|
)}
|
|
84
94
|
{isInIframe && (
|
|
85
95
|
<Button
|
|
96
|
+
aria-label="Retour à la liste des activités (droite)"
|
|
86
97
|
severity="secondary"
|
|
87
98
|
size="small"
|
|
88
99
|
icon={"pi pi-times"}
|
|
@@ -1,5 +1,14 @@
|
|
|
1
|
+
.sidebarContent {
|
|
2
|
+
height: 100%;
|
|
3
|
+
overflow-y: auto;
|
|
4
|
+
display: flex;
|
|
5
|
+
flex-direction: column;
|
|
6
|
+
}
|
|
7
|
+
|
|
1
8
|
.attachedFiles {
|
|
2
9
|
margin: 4px 0;
|
|
10
|
+
flex-grow: 1;
|
|
11
|
+
flex-shrink: 1;
|
|
3
12
|
}
|
|
4
13
|
|
|
5
14
|
.fileRow {
|
|
@@ -24,3 +33,11 @@
|
|
|
24
33
|
flex-shrink: 0;
|
|
25
34
|
flex-grow: 0;
|
|
26
35
|
}
|
|
36
|
+
|
|
37
|
+
.uploadFilesButtonContainer {
|
|
38
|
+
flex-shrink: 0;
|
|
39
|
+
flex-grow: 0;
|
|
40
|
+
& > * {
|
|
41
|
+
width: 100%;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -11,15 +11,58 @@ import { Toast } from "primereact/toast";
|
|
|
11
11
|
import styles from "./AttachedFilesSidebarContent.module.scss";
|
|
12
12
|
import { copyToClipboard } from "../../utils/clipboard";
|
|
13
13
|
import { downloadFile } from "../../utils/download";
|
|
14
|
+
import { useFileUpload } from "use-file-upload";
|
|
14
15
|
|
|
15
16
|
const AttachedFilesSidebarContent: FC = () => {
|
|
16
17
|
const filesData = useAttachedFiles();
|
|
18
|
+
const attachedFilesOptions = useAppSelector(selectAttachedFilesOptions);
|
|
19
|
+
const [_, selectFile] = useFileUpload();
|
|
17
20
|
|
|
18
21
|
return (
|
|
19
|
-
<div className={styles.
|
|
20
|
-
{
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
<div className={styles.sidebarContent}>
|
|
23
|
+
<div className={styles.attachedFiles}>
|
|
24
|
+
{filesData.map((file) => (
|
|
25
|
+
<AttachedFileLinks key={file.name} fileData={file} />
|
|
26
|
+
))}
|
|
27
|
+
</div>
|
|
28
|
+
{attachedFilesOptions.uploadTemporaryFiles && (
|
|
29
|
+
<div className={styles.uploadFilesButtonContainer}>
|
|
30
|
+
<Button
|
|
31
|
+
aria-label="Ajouter fichier temporaire"
|
|
32
|
+
icon="pi pi-upload"
|
|
33
|
+
label={
|
|
34
|
+
attachedFilesOptions.uploadTemporaryFiles?.uploadButtonLabel ||
|
|
35
|
+
"Ajouter fichier temporaire"
|
|
36
|
+
}
|
|
37
|
+
onClick={() => {
|
|
38
|
+
selectFile(
|
|
39
|
+
{
|
|
40
|
+
multiple: attachedFilesOptions.uploadTemporaryFiles?.multiple,
|
|
41
|
+
accept:
|
|
42
|
+
attachedFilesOptions.uploadTemporaryFiles?.mimeTypes?.join(
|
|
43
|
+
",",
|
|
44
|
+
) || undefined,
|
|
45
|
+
},
|
|
46
|
+
(filesOrFiles) => {
|
|
47
|
+
if (Array.isArray(filesOrFiles)) {
|
|
48
|
+
attachedFilesOptions.uploadTemporaryFiles?.handler(
|
|
49
|
+
filesOrFiles,
|
|
50
|
+
);
|
|
51
|
+
} else {
|
|
52
|
+
/* Deconstructing the object here so that if there is a
|
|
53
|
+
bug, the error will be caught here and not in the handler */
|
|
54
|
+
const { source, name, size, file } = filesOrFiles;
|
|
55
|
+
attachedFilesOptions.uploadTemporaryFiles?.handler([
|
|
56
|
+
{ source, name, size, file },
|
|
57
|
+
]);
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
);
|
|
61
|
+
}}
|
|
62
|
+
className={styles.uploadButton}
|
|
63
|
+
/>
|
|
64
|
+
</div>
|
|
65
|
+
)}
|
|
23
66
|
</div>
|
|
24
67
|
);
|
|
25
68
|
};
|
|
@@ -36,13 +79,13 @@ const AttachedFileLinks: FC<{ fileData: AttachedFileData }> = ({
|
|
|
36
79
|
<Button
|
|
37
80
|
label={fileData.name}
|
|
38
81
|
severity={fileData.isTemporary ? "warning" : "secondary"}
|
|
39
|
-
disabled={fileData.
|
|
82
|
+
disabled={fileData.interactMode === "none"}
|
|
40
83
|
onClick={() => {
|
|
41
|
-
if (fileData.
|
|
42
|
-
attachedFilesOptions.
|
|
43
|
-
} else if (fileData.
|
|
84
|
+
if (fileData.interactMode === "custom") {
|
|
85
|
+
attachedFilesOptions.customHandlers?.interactWithFile?.(fileData);
|
|
86
|
+
} else if (fileData.interactMode === "download") {
|
|
44
87
|
window.open(fileData.urlOrId, "_blank")?.focus();
|
|
45
|
-
} else if (fileData.
|
|
88
|
+
} else if (fileData.interactMode === "preview") {
|
|
46
89
|
toast.current?.show({
|
|
47
90
|
severity: "info",
|
|
48
91
|
summary: "Aperçu non disponible",
|
|
@@ -54,46 +97,56 @@ const AttachedFileLinks: FC<{ fileData: AttachedFileData }> = ({
|
|
|
54
97
|
className={styles.fileInteraction}
|
|
55
98
|
text
|
|
56
99
|
/>
|
|
57
|
-
{fileData.
|
|
100
|
+
{fileData.downloadMode !== "none" && (
|
|
58
101
|
<Button
|
|
59
102
|
icon="pi pi-download"
|
|
60
103
|
severity="secondary"
|
|
61
104
|
onClick={() => {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
105
|
+
if (fileData.downloadMode === "custom") {
|
|
106
|
+
attachedFilesOptions.customHandlers?.downloadFile?.(fileData);
|
|
107
|
+
return;
|
|
108
|
+
} else if (fileData.downloadMode === "default") {
|
|
109
|
+
downloadFile(fileData.urlOrId, fileData.name);
|
|
110
|
+
toast.current?.show({
|
|
111
|
+
severity: "success",
|
|
112
|
+
summary: "Téléchargement lancé",
|
|
113
|
+
detail: fileData.name,
|
|
114
|
+
life: 3000,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
69
117
|
}}
|
|
70
118
|
className={styles.fileSmallInteraction}
|
|
71
119
|
text
|
|
72
120
|
/>
|
|
73
121
|
)}
|
|
74
|
-
{fileData.
|
|
122
|
+
{fileData.copyLinkMode !== "none" && (
|
|
75
123
|
<Button
|
|
76
124
|
icon="pi pi-link"
|
|
77
125
|
severity="secondary"
|
|
78
126
|
onClick={() => {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
127
|
+
if (fileData.copyLinkMode === "custom") {
|
|
128
|
+
attachedFilesOptions.customHandlers?.copyFileLink?.(fileData);
|
|
129
|
+
return;
|
|
130
|
+
} else if (fileData.copyLinkMode === "default") {
|
|
131
|
+
// copy link to clipboard
|
|
132
|
+
copyToClipboard(fileData.urlOrId, (success, error) => {
|
|
133
|
+
if (success) {
|
|
134
|
+
toast.current?.show({
|
|
135
|
+
severity: "success",
|
|
136
|
+
summary: "Lien copié",
|
|
137
|
+
detail: fileData.name,
|
|
138
|
+
life: 3000,
|
|
139
|
+
});
|
|
140
|
+
} else {
|
|
141
|
+
toast.current?.show({
|
|
142
|
+
severity: "error",
|
|
143
|
+
summary: "Erreur lors de la copie du lien",
|
|
144
|
+
detail: error,
|
|
145
|
+
life: 5000,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
}
|
|
97
150
|
}}
|
|
98
151
|
style={{
|
|
99
152
|
flexGrow: 0,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Button } from "primereact/button";
|
|
2
|
-
import {
|
|
2
|
+
import { ButtonGroup } from "primereact/buttongroup";
|
|
3
3
|
import { ConfirmDialog, confirmDialog } from "primereact/confirmdialog";
|
|
4
4
|
import { Toast } from "primereact/toast";
|
|
5
5
|
import { OverlayPanel } from "primereact/overlaypanel";
|
|
@@ -78,6 +78,7 @@ const CapytaleMenu: React.FC = () => {
|
|
|
78
78
|
<div className={styles.capytaleMenu}>
|
|
79
79
|
{isInIframe && (
|
|
80
80
|
<Button
|
|
81
|
+
aria-label="Retour à la liste des activités (gauche)"
|
|
81
82
|
label="Retour"
|
|
82
83
|
icon="pi pi-chevron-left"
|
|
83
84
|
size="small"
|
|
@@ -96,6 +97,7 @@ const CapytaleMenu: React.FC = () => {
|
|
|
96
97
|
<Countdown />
|
|
97
98
|
<Button
|
|
98
99
|
label={isQuiteSmall ? undefined : "Enregistrer"}
|
|
100
|
+
aria-label="Enregistrer"
|
|
99
101
|
disabled={!isDirty}
|
|
100
102
|
severity={"warning"}
|
|
101
103
|
size="small"
|
|
@@ -116,51 +118,60 @@ const CapytaleMenu: React.FC = () => {
|
|
|
116
118
|
}
|
|
117
119
|
|
|
118
120
|
{mode === "create" && sharingInfo.code && (
|
|
119
|
-
<
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
121
|
+
<ButtonGroup>
|
|
122
|
+
<Button
|
|
123
|
+
label={isLarge ? sharingInfo.code : undefined}
|
|
124
|
+
aria-label="Copier le code de partage"
|
|
125
|
+
severity="secondary"
|
|
126
|
+
size="small"
|
|
127
|
+
icon="pi pi-share-alt"
|
|
128
|
+
outlined
|
|
129
|
+
tooltip="Copier le code de partage"
|
|
130
|
+
tooltipOptions={{
|
|
128
131
|
position: "bottom",
|
|
129
132
|
showDelay: settings.TOOLTIP_SHOW_DELAY,
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
133
|
+
}}
|
|
134
|
+
onClick={async () => {
|
|
135
|
+
await copyToClipboard(sharingInfo.code!);
|
|
136
|
+
toast.current!.show({
|
|
137
|
+
summary: "Code copié",
|
|
138
|
+
detail:
|
|
139
|
+
"Le code de partage a été copié dans le presse-papier.",
|
|
140
|
+
severity: "info",
|
|
141
|
+
life: 2000,
|
|
142
|
+
});
|
|
143
|
+
}}
|
|
144
|
+
/>
|
|
145
|
+
<Button
|
|
146
|
+
aria-label="Copier l'URL de partage"
|
|
147
|
+
icon="pi pi-link"
|
|
148
|
+
severity="secondary"
|
|
149
|
+
size="small"
|
|
150
|
+
outlined
|
|
151
|
+
tooltip="Copier l'URL de partage"
|
|
152
|
+
tooltipOptions={{
|
|
153
|
+
position: "bottom",
|
|
154
|
+
showDelay: settings.TOOLTIP_SHOW_DELAY,
|
|
155
|
+
}}
|
|
156
|
+
onClick={() => {
|
|
157
|
+
copyToClipboard(sharingInfo.codeLink!).then(() => {
|
|
158
|
+
toast.current!.show({
|
|
159
|
+
summary: "URL copiée",
|
|
160
|
+
detail:
|
|
161
|
+
"L'URL de partage a été copiée dans le presse-papier.",
|
|
162
|
+
severity: "info",
|
|
163
|
+
life: 2000,
|
|
154
164
|
});
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
|
|
165
|
+
});
|
|
166
|
+
}}
|
|
167
|
+
/>
|
|
168
|
+
</ButtonGroup>
|
|
159
169
|
)}
|
|
160
170
|
{showWorkflow && (
|
|
161
171
|
<>
|
|
162
172
|
{mode === "assignment" && workflow === "current" && (
|
|
163
173
|
<Button
|
|
174
|
+
aria-label="Rendre la copie"
|
|
164
175
|
outlined
|
|
165
176
|
label="Rendre"
|
|
166
177
|
icon="pi pi-envelope"
|
|
@@ -171,6 +182,11 @@ const CapytaleMenu: React.FC = () => {
|
|
|
171
182
|
)}
|
|
172
183
|
{mode === "assignment" && workflow !== "current" && (
|
|
173
184
|
<Button
|
|
185
|
+
aria-label={
|
|
186
|
+
workflow === "finished"
|
|
187
|
+
? "Copie déjà rendue"
|
|
188
|
+
: "Copie déjà rendue et corrigée"
|
|
189
|
+
}
|
|
174
190
|
outlined
|
|
175
191
|
label={workflow === "finished" ? "Rendue" : "Corrigée"}
|
|
176
192
|
disabled
|
|
@@ -185,6 +201,7 @@ const CapytaleMenu: React.FC = () => {
|
|
|
185
201
|
{mode === "review" && (
|
|
186
202
|
<div>
|
|
187
203
|
<ButtonDoubleIcon
|
|
204
|
+
aria-label="Changer l'état de la copie"
|
|
188
205
|
severity="secondary"
|
|
189
206
|
size="small"
|
|
190
207
|
outlined
|
|
@@ -208,7 +225,10 @@ const CapytaleMenu: React.FC = () => {
|
|
|
208
225
|
}
|
|
209
226
|
rightIcon="pi pi-angle-down"
|
|
210
227
|
/>
|
|
211
|
-
<OverlayPanel
|
|
228
|
+
<OverlayPanel
|
|
229
|
+
ref={overlayPanelWorkflow}
|
|
230
|
+
aria-label="Panneau de changement d'état de la copie"
|
|
231
|
+
>
|
|
212
232
|
<h3 className={styles.overlayPanelWorkflowTitle}>
|
|
213
233
|
État de la copie
|
|
214
234
|
</h3>
|
|
@@ -175,6 +175,7 @@ const Countdown: FC = () => {
|
|
|
175
175
|
if (diffMs >= MS_PER_DAY) {
|
|
176
176
|
return (
|
|
177
177
|
<div
|
|
178
|
+
aria-label="Deadline"
|
|
178
179
|
style={{
|
|
179
180
|
display: "flex",
|
|
180
181
|
alignItems: "center",
|
|
@@ -200,7 +201,12 @@ const Countdown: FC = () => {
|
|
|
200
201
|
})
|
|
201
202
|
.split(", ")
|
|
202
203
|
.map((s, i) => (
|
|
203
|
-
<div
|
|
204
|
+
<div
|
|
205
|
+
key={i}
|
|
206
|
+
aria-label={i === 0 ? "Date limite" : "Heure limite"}
|
|
207
|
+
>
|
|
208
|
+
{s}
|
|
209
|
+
</div>
|
|
204
210
|
))}
|
|
205
211
|
</div>
|
|
206
212
|
</div>
|
|
@@ -209,6 +215,7 @@ const Countdown: FC = () => {
|
|
|
209
215
|
if (diffMs <= 0) {
|
|
210
216
|
return (
|
|
211
217
|
<div
|
|
218
|
+
aria-label="Temps écoulé"
|
|
212
219
|
style={{
|
|
213
220
|
display: "flex",
|
|
214
221
|
alignItems: "center",
|
|
@@ -227,6 +234,7 @@ const Countdown: FC = () => {
|
|
|
227
234
|
const hours = Math.floor(diffSeconds / 3600) % 24;
|
|
228
235
|
return (
|
|
229
236
|
<div
|
|
237
|
+
aria-label="Temps restant"
|
|
230
238
|
style={{
|
|
231
239
|
display: "flex",
|
|
232
240
|
alignItems: "center",
|
|
@@ -81,18 +81,20 @@ const GradingNav: React.FC = () => {
|
|
|
81
81
|
return (
|
|
82
82
|
<div className="p-inputgroup flex-1">
|
|
83
83
|
<Button
|
|
84
|
+
aria-label="Passer à la copie précédente"
|
|
84
85
|
size="small"
|
|
85
86
|
icon="pi pi-chevron-left"
|
|
86
87
|
severity="secondary"
|
|
87
88
|
onClick={handlePrev}
|
|
88
89
|
tooltip="Copie précédente"
|
|
89
90
|
tooltipOptions={{
|
|
90
|
-
position: "left"
|
|
91
|
+
position: "left",
|
|
91
92
|
}}
|
|
92
93
|
disabled={nid == firstNid}
|
|
93
94
|
outlined
|
|
94
95
|
/>
|
|
95
96
|
<Dropdown
|
|
97
|
+
aria-label="Sélectionner une copie"
|
|
96
98
|
value={nid}
|
|
97
99
|
options={options}
|
|
98
100
|
filter
|
|
@@ -101,13 +103,14 @@ const GradingNav: React.FC = () => {
|
|
|
101
103
|
}}
|
|
102
104
|
/>
|
|
103
105
|
<Button
|
|
106
|
+
aria-label="Passer à la copie suivante"
|
|
104
107
|
size="small"
|
|
105
108
|
icon="pi pi-chevron-right"
|
|
106
109
|
severity="secondary"
|
|
107
110
|
onClick={handleNext}
|
|
108
111
|
tooltip="Copie suivante"
|
|
109
112
|
tooltipOptions={{
|
|
110
|
-
position: "right"
|
|
113
|
+
position: "right",
|
|
111
114
|
}}
|
|
112
115
|
disabled={nid == lastNid}
|
|
113
116
|
outlined
|
|
@@ -6,7 +6,7 @@ import styles from "./style.module.scss";
|
|
|
6
6
|
const ReviewNavbar: FC = () => {
|
|
7
7
|
return (
|
|
8
8
|
<div className={styles.reviewNavbar}>
|
|
9
|
-
<div>Mode correction</div>
|
|
9
|
+
<div aria-label="Indicateur de mode correction">Mode correction</div>
|
|
10
10
|
<div>
|
|
11
11
|
<GradingNav />
|
|
12
12
|
</div>
|
|
@@ -42,6 +42,7 @@ const SettingsSidebarContent: FC<SettingsSidebarContentProps> = (props) => {
|
|
|
42
42
|
<div className={styles.sidebarCapytaleActions}>
|
|
43
43
|
{props.showHelp && (
|
|
44
44
|
<Button
|
|
45
|
+
aria-label="Voir la documentation"
|
|
45
46
|
severity="secondary"
|
|
46
47
|
size="small"
|
|
47
48
|
icon={"pi pi-question-circle"}
|
|
@@ -61,6 +62,7 @@ const SettingsSidebarContent: FC<SettingsSidebarContentProps> = (props) => {
|
|
|
61
62
|
<>
|
|
62
63
|
<ConfirmPopup />
|
|
63
64
|
<Button
|
|
65
|
+
aria-label="Réinitialiser l'activité"
|
|
64
66
|
severity="secondary"
|
|
65
67
|
size="small"
|
|
66
68
|
icon={"pi pi-undo"}
|
|
@@ -104,6 +106,7 @@ const SettingsSidebarContent: FC<SettingsSidebarContentProps> = (props) => {
|
|
|
104
106
|
<div className="sidebarRadioButtons">
|
|
105
107
|
<div className="sidebarRadioGroup">
|
|
106
108
|
<RadioButton
|
|
109
|
+
aria-label="Option de disposition horizontale"
|
|
107
110
|
inputId="rb-horizontal"
|
|
108
111
|
name="horizontal"
|
|
109
112
|
value="horizontal"
|
|
@@ -116,6 +119,7 @@ const SettingsSidebarContent: FC<SettingsSidebarContentProps> = (props) => {
|
|
|
116
119
|
</div>
|
|
117
120
|
<div className="sidebarRadioGroup">
|
|
118
121
|
<RadioButton
|
|
122
|
+
aria-label="Option de disposition verticale"
|
|
119
123
|
inputId="rb-vertical"
|
|
120
124
|
name="vertical"
|
|
121
125
|
value="vertical"
|
|
@@ -134,6 +138,7 @@ const SettingsSidebarContent: FC<SettingsSidebarContentProps> = (props) => {
|
|
|
134
138
|
<div className="sidebarRadioButtons">
|
|
135
139
|
<div className="sidebarRadioGroup">
|
|
136
140
|
<RadioButton
|
|
141
|
+
aria-label="Option de thème clair"
|
|
137
142
|
inputId="rb-light"
|
|
138
143
|
name="light"
|
|
139
144
|
value="light"
|
|
@@ -149,6 +154,7 @@ const SettingsSidebarContent: FC<SettingsSidebarContentProps> = (props) => {
|
|
|
149
154
|
</div>
|
|
150
155
|
<div className="sidebarRadioGroup">
|
|
151
156
|
<RadioButton
|
|
157
|
+
aria-label="Option de thème sombre"
|
|
152
158
|
inputId="rb-dark"
|
|
153
159
|
name="dark"
|
|
154
160
|
value="dark"
|
|
@@ -164,6 +170,7 @@ const SettingsSidebarContent: FC<SettingsSidebarContentProps> = (props) => {
|
|
|
164
170
|
</div>
|
|
165
171
|
<div className="sidebarRadioGroup">
|
|
166
172
|
<RadioButton
|
|
173
|
+
aria-label="Option de thème système"
|
|
167
174
|
inputId="rb-auto"
|
|
168
175
|
name="auto"
|
|
169
176
|
value="auto"
|
|
@@ -22,7 +22,11 @@ const Navbar: React.FC = () => {
|
|
|
22
22
|
<div className={styles.navbar}>
|
|
23
23
|
<div className={styles.navbarContainer}>
|
|
24
24
|
<div className={styles.navbarLogo}>
|
|
25
|
-
{!isInIframe &&
|
|
25
|
+
{!isInIframe && (
|
|
26
|
+
<a href={returnUrl} aria-label="Capytale (accueil)">
|
|
27
|
+
{logoText}
|
|
28
|
+
</a>
|
|
29
|
+
)}
|
|
26
30
|
</div>
|
|
27
31
|
<CapytaleMenu />
|
|
28
32
|
<ActivityInfo />
|
|
@@ -43,7 +43,7 @@ export const CloseOnlyPedagoCommands: FC = () => {
|
|
|
43
43
|
icon="pi pi-times"
|
|
44
44
|
rounded
|
|
45
45
|
text
|
|
46
|
-
aria-label="Masquer
|
|
46
|
+
aria-label="Masquer consignes et évaluation"
|
|
47
47
|
tooltip="Masquer l'évaluation"
|
|
48
48
|
tooltipOptions={{
|
|
49
49
|
position: "left",
|
|
@@ -90,7 +90,7 @@ const HorizontalPedagoCommands = () => {
|
|
|
90
90
|
icon="pi pi-times"
|
|
91
91
|
rounded
|
|
92
92
|
text
|
|
93
|
-
aria-label="Masquer
|
|
93
|
+
aria-label="Masquer consignes et évaluation"
|
|
94
94
|
tooltip="Masquer les consignes"
|
|
95
95
|
tooltipOptions={{
|
|
96
96
|
position: "left",
|
|
@@ -132,6 +132,7 @@ const DocumentSelectorItemActions: FC<{
|
|
|
132
132
|
</div>
|
|
133
133
|
{item.value !== "pdf" && (
|
|
134
134
|
<SelectButton
|
|
135
|
+
aria-label={`Sélectionner le type de ${item.name}`}
|
|
135
136
|
className={styles.smallSelectButton}
|
|
136
137
|
value={
|
|
137
138
|
item.value === "instructions"
|
|
@@ -241,7 +242,7 @@ const VerticalPedagoCommands = () => {
|
|
|
241
242
|
icon="pi pi-times"
|
|
242
243
|
rounded
|
|
243
244
|
text
|
|
244
|
-
aria-label="Masquer
|
|
245
|
+
aria-label="Masquer consignes et évaluation"
|
|
245
246
|
tooltip="Masquer les consignes"
|
|
246
247
|
tooltipOptions={{
|
|
247
248
|
position: "left",
|
|
@@ -73,6 +73,7 @@ const Pedago: React.FC<DivProps> = ({ className, ...props }) => {
|
|
|
73
73
|
>
|
|
74
74
|
{mayReverse([
|
|
75
75
|
<SplitterPanel
|
|
76
|
+
aria-label="Panneau de notation"
|
|
76
77
|
key="gradingPanel"
|
|
77
78
|
minSize={30}
|
|
78
79
|
size={40}
|
|
@@ -81,6 +82,7 @@ const Pedago: React.FC<DivProps> = ({ className, ...props }) => {
|
|
|
81
82
|
<Grading />
|
|
82
83
|
</SplitterPanel>,
|
|
83
84
|
<SplitterPanel
|
|
85
|
+
aria-label="Panneau de consignes"
|
|
84
86
|
key="pedagoPanel"
|
|
85
87
|
minSize={50}
|
|
86
88
|
size={60}
|
|
@@ -138,6 +140,7 @@ const Grading: FC = () => {
|
|
|
138
140
|
<div className={styles.pedagoFeedback}>
|
|
139
141
|
{(comments || mode !== "assignment") && (
|
|
140
142
|
<textarea
|
|
143
|
+
aria-label="Saisie de l'appréciation"
|
|
141
144
|
value={comments || ""}
|
|
142
145
|
placeholder={
|
|
143
146
|
mode !== "review" ? "" : "Rédigez ici l'appréciation."
|
|
@@ -159,6 +162,7 @@ const Grading: FC = () => {
|
|
|
159
162
|
<div className={styles.pedagoGrade}>
|
|
160
163
|
{(grading || mode !== "assignment") && (
|
|
161
164
|
<textarea
|
|
165
|
+
aria-label="Saisie de l'évaluation"
|
|
162
166
|
value={grading || ""}
|
|
163
167
|
placeholder={
|
|
164
168
|
mode !== "review"
|
package/src/index.css
CHANGED
package/src/index.tsx
CHANGED
|
@@ -20,8 +20,10 @@ import { ActivityQuickActionsSetter } from "./features/navbar/ActivityQuickActio
|
|
|
20
20
|
import ActivitySettingsSetter from "./features/activitySettings/ActivitySettingsSetter";
|
|
21
21
|
import IsDirtySetter from "./features/activityData/IsDirtySetter";
|
|
22
22
|
import AttachedFilesFunctionality from "./features/functionalities/AttachedFilesFunctionality";
|
|
23
|
+
import { useRefreshAttachedFiles } from "./features/functionalities/hooks";
|
|
23
24
|
import { Toast } from "./external/prime";
|
|
24
25
|
import type { ToastMessage } from "./external/prime";
|
|
26
|
+
import type { AttachedFileData, UploadedFileInfo } from "./features/functionalities/functionalitiesSlice";
|
|
25
27
|
|
|
26
28
|
export {
|
|
27
29
|
MetaPlayer,
|
|
@@ -43,6 +45,7 @@ export {
|
|
|
43
45
|
ActivitySettingsSetter,
|
|
44
46
|
IsDirtySetter,
|
|
45
47
|
AttachedFilesFunctionality,
|
|
48
|
+
useRefreshAttachedFiles,
|
|
46
49
|
Toast,
|
|
47
50
|
};
|
|
48
|
-
export type { ToastMessage };
|
|
51
|
+
export type { AttachedFileData, UploadedFileInfo, ToastMessage };
|
|
@@ -14,6 +14,8 @@ const CardSelector = function <ValueType>(props: CardSelectorProps<ValueType>) {
|
|
|
14
14
|
<div
|
|
15
15
|
className={styles.cardSelectorOption}
|
|
16
16
|
data-selected={props.selected === option.value}
|
|
17
|
+
role="button"
|
|
18
|
+
aria-label={`Sélectionner l'option ${option.title}`}
|
|
17
19
|
onClick={() => {
|
|
18
20
|
props.onChange(option.value);
|
|
19
21
|
}}
|