@capytale/meta-player 0.5.13 → 0.5.14

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.
@@ -0,0 +1,28 @@
1
+ import js from '@eslint/js'
2
+ import globals from 'globals'
3
+ import reactHooks from 'eslint-plugin-react-hooks'
4
+ import reactRefresh from 'eslint-plugin-react-refresh'
5
+ import tseslint from 'typescript-eslint'
6
+
7
+ export default tseslint.config(
8
+ { ignores: ['dist'] },
9
+ {
10
+ extends: [js.configs.recommended, ...tseslint.configs.recommended],
11
+ files: ['**/*.{ts,tsx}'],
12
+ languageOptions: {
13
+ ecmaVersion: 2020,
14
+ globals: globals.browser,
15
+ },
16
+ plugins: {
17
+ 'react-hooks': reactHooks,
18
+ 'react-refresh': reactRefresh,
19
+ },
20
+ rules: {
21
+ ...reactHooks.configs.recommended.rules,
22
+ 'react-refresh/only-export-components': [
23
+ 'warn',
24
+ { allowConstantExport: true },
25
+ ],
26
+ },
27
+ },
28
+ )
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capytale/meta-player",
3
- "version": "0.5.13",
3
+ "version": "0.5.14",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "dev": "vite",
@@ -16,7 +16,7 @@
16
16
  }
17
17
  },
18
18
  "dependencies": {
19
- "@capytale/activity.js": "^3.1.14",
19
+ "@capytale/activity.js": "^3.1.21",
20
20
  "@capytale/capytale-anti-triche": "^0.2.1",
21
21
  "@capytale/capytale-rich-text-editor": "^0.4.3",
22
22
  "@reduxjs/toolkit": "^2.0.1",
@@ -31,6 +31,7 @@
31
31
  "use-file-upload": "^1.0.11"
32
32
  },
33
33
  "devDependencies": {
34
+ "@eslint/js": "^9.19.0",
34
35
  "@testing-library/dom": "^9.3.4",
35
36
  "@testing-library/jest-dom": "^6.2.0",
36
37
  "@testing-library/react": "^14.1.2",
@@ -38,13 +39,18 @@
38
39
  "@types/react": "^18.3.8",
39
40
  "@types/react-dom": "^18.3.0",
40
41
  "@vitejs/plugin-react": "^4.3.4",
42
+ "eslint": "^9.19.0",
43
+ "eslint-plugin-react-hooks": "^5.0.0",
44
+ "eslint-plugin-react-refresh": "^0.4.18",
45
+ "globals": "^15.14.0",
41
46
  "jsdom": "^23.2.0",
42
47
  "prettier": "^3.2.1",
43
48
  "react": "^18.3.1",
44
49
  "react-dom": "^18.3.1",
45
50
  "sass": "^1.75.0",
46
- "typescript": "^5.3.3",
47
- "vite": "^6.0.2"
51
+ "typescript": "^5.7.2",
52
+ "typescript-eslint": "^8.22.0",
53
+ "vite": "^6.1.0"
48
54
  },
49
55
  "peerDependencies": {
50
56
  "react": "^18.3.1",
@@ -74,7 +74,22 @@ const MetaPlayer: FC<MetaPlayerProps> = (props) => {
74
74
  <ThemeSwitcher />
75
75
  <ExitWarning />
76
76
  <ErrorBoundary fallback={<div>Une erreur est survenue</div>}>
77
- <ActivityJSProvider options={props.activityJSOptions}>
77
+ <ActivityJSProvider
78
+ options={props.activityJSOptions}
79
+ loadingFallback={<div>Chargement de l'activité...</div>}
80
+ errorFallback={({ children }) => (
81
+ <div style={{ marginLeft: "1rem", marginRight: "1rem" }}>
82
+ <p>Erreur lors du chargement de l'activité : {children}</p>
83
+ <p>
84
+ Êtes-vous bien connecté(e) à Capytale ? Avez-vous bien le
85
+ droit d'accéder à cette ressource ?
86
+ </p>
87
+ <p>
88
+ <a href="/">Retour à l'accueil</a>
89
+ </p>
90
+ </div>
91
+ )}
92
+ >
78
93
  <Saver />
79
94
  <MetaPlayerContent antiCheatOptions={antiCheatOptions}>
80
95
  <App>{props.children}</App>
@@ -28,6 +28,9 @@ const ActivityJSContext = createContext<ActivitySessionLoaderReturnValue>({
28
28
  type ActivityJSProviderProps = PropsWithChildren<{
29
29
  options?: LoadOptions;
30
30
  loadingFallback?: ReactNode;
31
+ errorFallback?:
32
+ | keyof JSX.IntrinsicElements
33
+ | React.JSXElementConstructor<{ children: ReactNode }>;
31
34
  }>;
32
35
 
33
36
  export const ActivityJSProvider: FC<ActivityJSProviderProps> = (props) => {
@@ -109,7 +112,14 @@ export const ActivityJSProvider: FC<ActivityJSProviderProps> = (props) => {
109
112
  }
110
113
  const value = useActivitySessionLoader(activityId, props.options, callback);
111
114
  if (!loaded) {
112
- return <>{props.loadingFallback}</>;
115
+ if (value.state === "loading") {
116
+ return <>{props.loadingFallback}</>;
117
+ } else if (value.state === "error" && props.errorFallback) {
118
+ console.error(value.error);
119
+ return (
120
+ <>{<props.errorFallback>{String(value.error)}</props.errorFallback>}</>
121
+ );
122
+ }
113
123
  }
114
124
  return (
115
125
  <ActivityJSContext.Provider value={value}>
@@ -0,0 +1,74 @@
1
+ import { FC, useCallback, useEffect, useState } from "react";
2
+ import { useAppSelector } from "../../../app/hooks";
3
+ import { Dialog } from "primereact/dialog";
4
+ import { selectActivityInfo } from "../../activityData/activityDataSlice";
5
+ import { Button } from "primereact/button";
6
+ import { apiCloneActivity } from "../../../utils/capytale";
7
+ import { useActivityJsEssentials } from "../../activityJS/ActivityJSProvider";
8
+ import { InputText } from "primereact/inputtext";
9
+
10
+ const CloneDialog: FC<{ visible: boolean; onHide: () => any }> = ({
11
+ visible,
12
+ onHide,
13
+ }) => {
14
+ const { nid } = useActivityJsEssentials();
15
+ const activityTitle = useAppSelector(selectActivityInfo).title;
16
+ const [newTitle, setNewTitle] = useState<string>(activityTitle);
17
+ const clone = useCallback(async () => {
18
+ const clone = await apiCloneActivity(nid, newTitle);
19
+ const newNid = clone.nid;
20
+ const url = `/web/c-act/n/${newNid}/play/create`;
21
+ // redirect to the new activity in a new tab
22
+ window.open(url, "_blank");
23
+ }, [nid, newTitle]);
24
+
25
+ useEffect(() => {
26
+ setNewTitle(activityTitle);
27
+ }, [activityTitle, visible]);
28
+
29
+ return (
30
+ <Dialog
31
+ visible={visible}
32
+ header={"Choisissez le titre du clone"}
33
+ onHide={onHide}
34
+ style={{ width: "750px", maxWidth: "100%" }}
35
+ >
36
+ <div
37
+ style={{
38
+ display: "flex",
39
+ flexDirection: "column",
40
+ gap: "0.5rem",
41
+ marginBottom: "1rem",
42
+ }}
43
+ >
44
+ <label htmlFor="title-input">Titre</label>
45
+ <InputText
46
+ id="title-input"
47
+ value={newTitle}
48
+ onChange={(e) => setNewTitle(e.target.value)}
49
+ />
50
+ <div
51
+ style={{
52
+ display: "flex",
53
+ justifyContent: "flex-end",
54
+ gap: "0.5rem",
55
+ }}
56
+ >
57
+ <Button
58
+ aria-label="Annuler le clonage"
59
+ label="Annuler"
60
+ onClick={onHide}
61
+ severity="secondary"
62
+ />
63
+ <Button
64
+ aria-label="Confirmer le clonage"
65
+ label="Cloner"
66
+ onClick={clone}
67
+ />
68
+ </div>
69
+ </div>
70
+ </Dialog>
71
+ );
72
+ };
73
+
74
+ export default CloneDialog;
@@ -5,7 +5,7 @@ import { Toast } from "primereact/toast";
5
5
  import { OverlayPanel } from "primereact/overlaypanel";
6
6
 
7
7
  import styles from "./style.module.scss";
8
- import { useMemo, useRef } from "react";
8
+ import { useMemo, useRef, useState } from "react";
9
9
  import { useWindowSize } from "@uidotdev/usehooks";
10
10
  import { XL } from "../../../utils/breakpoints";
11
11
  import { useAppDispatch, useAppSelector } from "../../../app/hooks";
@@ -24,10 +24,12 @@ import CardSelector from "../../../utils/CardSelector";
24
24
  import { useActivityJS } from "../../activityJS/ActivityJSProvider";
25
25
  import { selectShowWorkflow } from "../../layout/layoutSlice";
26
26
  import CountdownAndSaveButton from "./CountdownAndSaveButton";
27
+ import CloneDialog from "./CloneDialog";
27
28
 
28
29
  const CapytaleMenu: React.FC = () => {
29
30
  const dispatch = useAppDispatch();
30
31
  const activityJS = useActivityJS();
32
+ const [cloneDialogVisible, setCloneDialogVisible] = useState(false);
31
33
  const sharingInfo = useAppSelector(selectSharingInfo);
32
34
  const windowsSize = useWindowSize();
33
35
  const mode = useAppSelector(selectMode);
@@ -76,6 +78,27 @@ const CapytaleMenu: React.FC = () => {
76
78
  />
77
79
  )}
78
80
  <CountdownAndSaveButton />
81
+ {!isInIframe && mode === "view" && (
82
+ <>
83
+ <Button
84
+ aria-label="Cloner l'activité"
85
+ label="Cloner"
86
+ icon="pi pi-clone"
87
+ size="small"
88
+ outlined
89
+ onClick={() => setCloneDialogVisible(true)}
90
+ tooltip="Cloner l'activité"
91
+ tooltipOptions={{
92
+ position: "bottom",
93
+ showDelay: settings.TOOLTIP_SHOW_DELAY,
94
+ }}
95
+ />
96
+ <CloneDialog
97
+ visible={cloneDialogVisible}
98
+ onHide={() => setCloneDialogVisible(false)}
99
+ />
100
+ </>
101
+ )}
79
102
 
80
103
  {mode === "create" && sharingInfo.code && (
81
104
  <ButtonGroup>
@@ -37,7 +37,11 @@ const Pedago: React.FC<DivProps> = ({ className, ...props }) => {
37
37
  (hasGradingOrComments || mode === "review");
38
38
  const isHorizontal = useAppSelector(selectOrientation) === "horizontal";
39
39
  const mayReverse = isHorizontal
40
- ? (tab: Array<any>) => tab.toReversed()
40
+ ? (tab: Array<any>) => {
41
+ const reversed = [...tab];
42
+ reversed.reverse();
43
+ return reversed;
44
+ }
41
45
  : (tab: Array<any>) => tab;
42
46
 
43
47
  if (!hasInstructions) {
@@ -0,0 +1,11 @@
1
+ import httpClient from '@capytale/activity.js/backend/capytale/http';
2
+
3
+ // Definit le endpoint de l'API
4
+ const myActivitiesApiEp = '/web/c-hdls/api/my-activities';
5
+
6
+ export async function apiCloneActivity(nid: number | string, title?: string) {
7
+ return httpClient.postGetJsonAsync<any>(
8
+ myActivitiesApiEp,
9
+ { action: 'clone', nid, title },
10
+ )
11
+ }
package/tsconfig.json CHANGED
@@ -1,16 +1,18 @@
1
1
  {
2
2
  "compilerOptions": {
3
- "target": "ESNext",
3
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
4
+ "target": "ES2020",
4
5
  "useDefineForClassFields": true,
5
- "lib": ["DOM", "DOM.Iterable", "ESNext"],
6
- "allowJs": false,
7
- "skipLibCheck": true,
8
- "esModuleInterop": false,
9
- "allowSyntheticDefaultImports": true,
6
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
10
7
  "module": "ESNext",
8
+ "skipLibCheck": true,
9
+ "allowJs": false,
10
+
11
11
  "moduleResolution": "bundler",
12
- "resolveJsonModule": true,
12
+ "allowSyntheticDefaultImports": true,
13
13
  "isolatedModules": true,
14
+ "esModuleInterop": false,
15
+ "resolveJsonModule": true,
14
16
  "noEmit": true,
15
17
  "jsx": "react-jsx",
16
18