@capytale/meta-player 0.5.12 → 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.
- package/README.md +10 -24
- package/eslint.config.js +28 -0
- package/package.json +11 -4
- package/src/App.tsx +2 -0
- package/src/MetaPlayer.tsx +16 -1
- package/src/features/activityJS/ActivityJSProvider.tsx +11 -1
- package/src/features/functionalities/PreviewDialog.tsx +76 -0
- package/src/features/functionalities/functionalitiesSlice.ts +14 -1
- package/src/features/functionalities/hooks.ts +29 -3
- package/src/features/navbar/capytale-menu/CloneDialog.tsx +74 -0
- package/src/features/navbar/capytale-menu/index.tsx +24 -1
- package/src/features/navbar/sidebars/AttachedFilesSidebarContent.tsx +28 -8
- package/src/features/pedago/index.tsx +5 -1
- package/src/index.tsx +11 -1
- package/src/utils/capytale.ts +11 -0
- package/tsconfig.json +9 -7
package/README.md
CHANGED
|
@@ -1,27 +1,13 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Meta Player Capytale
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
## To put in player HTML
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
- CSS used by the meta-player:
|
|
6
|
+
```html
|
|
7
|
+
<link id="theme-link" rel="stylesheet" href="https://cdn.ac-paris.fr/capytale/meta-player/themes/lara-light-blue/theme.css">
|
|
7
8
|
```
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
-
|
|
12
|
-
|
|
13
|
-
- Optimized performance compared to Create React App
|
|
14
|
-
- Customizable without ejecting
|
|
15
|
-
|
|
16
|
-
## Scripts
|
|
17
|
-
|
|
18
|
-
- `dev`/`start` - start dev server and open browser
|
|
19
|
-
- `build` - build for production
|
|
20
|
-
- `preview` - locally preview production build
|
|
21
|
-
- `test` - launch test runner
|
|
22
|
-
|
|
23
|
-
## Inspiration
|
|
24
|
-
|
|
25
|
-
- [Create React App](https://github.com/facebook/create-react-app/tree/main/packages/cra-template)
|
|
26
|
-
- [Vite](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react)
|
|
27
|
-
- [Vitest](https://github.com/vitest-dev/vitest/tree/main/examples/react-testing-lib)
|
|
9
|
+
- If using the attached files preview capability:
|
|
10
|
+
```html
|
|
11
|
+
<link rel="stylesheet" href="https://cdn.ac-paris.fr/highlight/styles/default.css">
|
|
12
|
+
<script src="https://cdn.ac-paris.fr/highlight/highlight.min.js"></script>
|
|
13
|
+
```
|
package/eslint.config.js
ADDED
|
@@ -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.
|
|
3
|
+
"version": "0.5.14",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"dev": "vite",
|
|
@@ -16,11 +16,12 @@
|
|
|
16
16
|
}
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@capytale/activity.js": "^3.1.
|
|
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",
|
|
23
23
|
"@uidotdev/usehooks": "^2.4.1",
|
|
24
|
+
"mime": "^4.0.6",
|
|
24
25
|
"primeicons": "^7.0.0",
|
|
25
26
|
"primereact": "^10.8.3",
|
|
26
27
|
"react-dropzone": "^14.2.9",
|
|
@@ -30,6 +31,7 @@
|
|
|
30
31
|
"use-file-upload": "^1.0.11"
|
|
31
32
|
},
|
|
32
33
|
"devDependencies": {
|
|
34
|
+
"@eslint/js": "^9.19.0",
|
|
33
35
|
"@testing-library/dom": "^9.3.4",
|
|
34
36
|
"@testing-library/jest-dom": "^6.2.0",
|
|
35
37
|
"@testing-library/react": "^14.1.2",
|
|
@@ -37,13 +39,18 @@
|
|
|
37
39
|
"@types/react": "^18.3.8",
|
|
38
40
|
"@types/react-dom": "^18.3.0",
|
|
39
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",
|
|
40
46
|
"jsdom": "^23.2.0",
|
|
41
47
|
"prettier": "^3.2.1",
|
|
42
48
|
"react": "^18.3.1",
|
|
43
49
|
"react-dom": "^18.3.1",
|
|
44
50
|
"sass": "^1.75.0",
|
|
45
|
-
"typescript": "^5.
|
|
46
|
-
"
|
|
51
|
+
"typescript": "^5.7.2",
|
|
52
|
+
"typescript-eslint": "^8.22.0",
|
|
53
|
+
"vite": "^6.1.0"
|
|
47
54
|
},
|
|
48
55
|
"peerDependencies": {
|
|
49
56
|
"react": "^18.3.1",
|
package/src/App.tsx
CHANGED
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
import settings from "./settings";
|
|
27
27
|
import ReviewNavbar from "./features/navbar/review-navbar";
|
|
28
28
|
import { useSave } from "./features/activityData/hooks";
|
|
29
|
+
import PreviewDialog from "./features/functionalities/PreviewDialog";
|
|
29
30
|
|
|
30
31
|
type AppProps = PropsWithChildren<{}>;
|
|
31
32
|
|
|
@@ -130,6 +131,7 @@ const App: FC<AppProps> = (props) => {
|
|
|
130
131
|
</SplitterPanel>
|
|
131
132
|
</Splitter>
|
|
132
133
|
</div>
|
|
134
|
+
<PreviewDialog />
|
|
133
135
|
</div>
|
|
134
136
|
);
|
|
135
137
|
};
|
package/src/MetaPlayer.tsx
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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,76 @@
|
|
|
1
|
+
import { FC, memo, useEffect, useRef, useState } from "react";
|
|
2
|
+
import { useAppDispatch, useAppSelector } from "../../app/hooks";
|
|
3
|
+
import {
|
|
4
|
+
PreviewFile,
|
|
5
|
+
selectPreviewFile,
|
|
6
|
+
setPreviewFile,
|
|
7
|
+
} from "./functionalitiesSlice";
|
|
8
|
+
import { Dialog } from "primereact/dialog";
|
|
9
|
+
|
|
10
|
+
const HighlightedCode = memo(({ code }: { code: string }) => {
|
|
11
|
+
const codeRef = useRef<HTMLDivElement>(null);
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
if (codeRef.current != null) {
|
|
14
|
+
if ((window as any).hljs) {
|
|
15
|
+
(window as any).hljs.highlightBlock(codeRef.current);
|
|
16
|
+
} else {
|
|
17
|
+
console.warn("Highlight.js not loaded");
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}, [code]);
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<pre>
|
|
24
|
+
<code
|
|
25
|
+
ref={codeRef}
|
|
26
|
+
style={{ padding: 0, background: "white", maxWidth: "100%" }}
|
|
27
|
+
>
|
|
28
|
+
{code}
|
|
29
|
+
</code>
|
|
30
|
+
</pre>
|
|
31
|
+
);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const PreviewDialog: FC = () => {
|
|
35
|
+
const previewFile = useAppSelector(selectPreviewFile);
|
|
36
|
+
const [persistentPreviewFile, setPersistentPreviewFile] =
|
|
37
|
+
useState<PreviewFile | null>(null);
|
|
38
|
+
|
|
39
|
+
const dispatch = useAppDispatch();
|
|
40
|
+
const visible = previewFile != null;
|
|
41
|
+
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
if (previewFile != null) {
|
|
44
|
+
setPersistentPreviewFile(previewFile);
|
|
45
|
+
}
|
|
46
|
+
}, [previewFile]);
|
|
47
|
+
|
|
48
|
+
if (persistentPreviewFile == null) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<Dialog
|
|
54
|
+
visible={visible}
|
|
55
|
+
header={persistentPreviewFile.title}
|
|
56
|
+
onHide={() => {
|
|
57
|
+
dispatch(setPreviewFile(null));
|
|
58
|
+
}}
|
|
59
|
+
style={{ maxWidth: "100%" }}
|
|
60
|
+
>
|
|
61
|
+
{persistentPreviewFile.type === "text" ? (
|
|
62
|
+
<HighlightedCode code={persistentPreviewFile.content} />
|
|
63
|
+
) : (
|
|
64
|
+
<img
|
|
65
|
+
src={persistentPreviewFile.url}
|
|
66
|
+
alt={persistentPreviewFile.title}
|
|
67
|
+
style={{
|
|
68
|
+
maxWidth: "100%",
|
|
69
|
+
}}
|
|
70
|
+
/>
|
|
71
|
+
)}
|
|
72
|
+
</Dialog>
|
|
73
|
+
);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export default PreviewDialog;
|
|
@@ -34,9 +34,15 @@ export type AttachedFilesOptions = {
|
|
|
34
34
|
onViewFiles?: () => any;
|
|
35
35
|
};
|
|
36
36
|
|
|
37
|
-
export
|
|
37
|
+
export type PreviewFile = { title: string } & (
|
|
38
|
+
{ type: "text", content: string } |
|
|
39
|
+
{ type: "image", url: string }
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
export type FunctionalitiesState = {
|
|
38
43
|
attachedFilesOptions: AttachedFilesOptions;
|
|
39
44
|
attachedFilesRefresher: number;
|
|
45
|
+
previewFile: PreviewFile | null;
|
|
40
46
|
}
|
|
41
47
|
|
|
42
48
|
export const defaultAttachedFilesOptions: AttachedFilesOptions = {
|
|
@@ -46,6 +52,7 @@ export const defaultAttachedFilesOptions: AttachedFilesOptions = {
|
|
|
46
52
|
export const initialState: FunctionalitiesState = {
|
|
47
53
|
attachedFilesOptions: defaultAttachedFilesOptions,
|
|
48
54
|
attachedFilesRefresher: 0,
|
|
55
|
+
previewFile: null,
|
|
49
56
|
};
|
|
50
57
|
|
|
51
58
|
// If you are not using async thunks you can use the standalone `createSlice`.
|
|
@@ -61,6 +68,9 @@ export const functionalitiesSlice = createAppSlice({
|
|
|
61
68
|
refreshAttachedFiles: create.reducer((state) => {
|
|
62
69
|
state.attachedFilesRefresher += 1;
|
|
63
70
|
}),
|
|
71
|
+
setPreviewFile: create.reducer((state, action: PayloadAction<PreviewFile | null>) => {
|
|
72
|
+
state.previewFile = action.payload;
|
|
73
|
+
}),
|
|
64
74
|
}),
|
|
65
75
|
// You can define your selectors here. These selectors receive the slice
|
|
66
76
|
// state as their first argument.
|
|
@@ -68,6 +78,7 @@ export const functionalitiesSlice = createAppSlice({
|
|
|
68
78
|
selectAttachedFilesOptions: (state) => state.attachedFilesOptions,
|
|
69
79
|
selectAttachedFilesEnabled: (state) => state.attachedFilesOptions.enabled,
|
|
70
80
|
selectAttachedFilesRefresher: (state) => state.attachedFilesRefresher,
|
|
81
|
+
selectPreviewFile: (state) => state.previewFile,
|
|
71
82
|
},
|
|
72
83
|
});
|
|
73
84
|
|
|
@@ -75,6 +86,7 @@ export const functionalitiesSlice = createAppSlice({
|
|
|
75
86
|
export const {
|
|
76
87
|
setAttachedFilesOptions,
|
|
77
88
|
refreshAttachedFiles,
|
|
89
|
+
setPreviewFile,
|
|
78
90
|
} = functionalitiesSlice.actions;
|
|
79
91
|
|
|
80
92
|
// Selectors returned by `slice.selectors` take the root state as their first argument.
|
|
@@ -82,4 +94,5 @@ export const {
|
|
|
82
94
|
selectAttachedFilesOptions,
|
|
83
95
|
selectAttachedFilesEnabled,
|
|
84
96
|
selectAttachedFilesRefresher,
|
|
97
|
+
selectPreviewFile,
|
|
85
98
|
} = functionalitiesSlice.selectors;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useMemo } from "react";
|
|
2
2
|
import { useAppDispatch, useAppSelector } from "../../app/hooks"
|
|
3
3
|
import { useActivityJS } from "../activityJS/ActivityJSProvider";
|
|
4
|
-
import { AttachedFileData, refreshAttachedFiles, selectAttachedFilesOptions, selectAttachedFilesRefresher } from "./functionalitiesSlice"
|
|
4
|
+
import { AttachedFileData, refreshAttachedFiles, selectAttachedFilesOptions, selectAttachedFilesRefresher, setPreviewFile } from "./functionalitiesSlice"
|
|
5
5
|
|
|
6
6
|
export const useAttachedFiles = () => {
|
|
7
7
|
const attachedFilesOptions = useAppSelector(selectAttachedFilesOptions);
|
|
@@ -36,9 +36,35 @@ export const useAttachedFiles = () => {
|
|
|
36
36
|
}, [filesData, attachedFilesOptions, attachedFilesRefresher]);
|
|
37
37
|
|
|
38
38
|
return treatedFilesData;
|
|
39
|
-
}
|
|
39
|
+
};
|
|
40
40
|
|
|
41
41
|
export const useRefreshAttachedFiles = () => {
|
|
42
42
|
const dispatch = useAppDispatch();
|
|
43
43
|
return () => dispatch(refreshAttachedFiles());
|
|
44
|
-
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const usePreviewTextFile = () => {
|
|
47
|
+
const dispatch = useAppDispatch();
|
|
48
|
+
return (title: string, content: string) => {
|
|
49
|
+
dispatch(
|
|
50
|
+
setPreviewFile({
|
|
51
|
+
title,
|
|
52
|
+
type: "text",
|
|
53
|
+
content,
|
|
54
|
+
}),
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export const usePreviewImageFile = () => {
|
|
60
|
+
const dispatch = useAppDispatch();
|
|
61
|
+
return (title: string, url: string) => {
|
|
62
|
+
dispatch(
|
|
63
|
+
setPreviewFile({
|
|
64
|
+
title,
|
|
65
|
+
type: "image",
|
|
66
|
+
url,
|
|
67
|
+
}),
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
@@ -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>
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { FC, useRef } from "react";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
useAttachedFiles,
|
|
4
|
+
usePreviewImageFile,
|
|
5
|
+
usePreviewTextFile,
|
|
6
|
+
} from "../../functionalities/hooks";
|
|
3
7
|
import { useAppSelector } from "../../../app/hooks";
|
|
4
8
|
import {
|
|
5
9
|
AttachedFileData,
|
|
@@ -12,6 +16,7 @@ import styles from "./AttachedFilesSidebarContent.module.scss";
|
|
|
12
16
|
import { copyToClipboard } from "../../../utils/clipboard";
|
|
13
17
|
import { downloadFile } from "../../../utils/download";
|
|
14
18
|
import { useFileUpload } from "use-file-upload";
|
|
19
|
+
import mime from "mime";
|
|
15
20
|
|
|
16
21
|
const AttachedFilesSidebarContent: FC = () => {
|
|
17
22
|
const filesData = useAttachedFiles();
|
|
@@ -72,6 +77,8 @@ const AttachedFileLinks: FC<{ fileData: AttachedFileData }> = ({
|
|
|
72
77
|
}) => {
|
|
73
78
|
const attachedFilesOptions = useAppSelector(selectAttachedFilesOptions);
|
|
74
79
|
const toast = useRef<Toast>(null);
|
|
80
|
+
const previewTextFile = usePreviewTextFile();
|
|
81
|
+
const previewImageFile = usePreviewImageFile();
|
|
75
82
|
|
|
76
83
|
return (
|
|
77
84
|
<div className={styles.fileRow}>
|
|
@@ -80,18 +87,31 @@ const AttachedFileLinks: FC<{ fileData: AttachedFileData }> = ({
|
|
|
80
87
|
label={fileData.name}
|
|
81
88
|
severity={fileData.isTemporary ? "warning" : "secondary"}
|
|
82
89
|
disabled={fileData.interactMode === "none"}
|
|
83
|
-
onClick={() => {
|
|
90
|
+
onClick={async () => {
|
|
84
91
|
if (fileData.interactMode === "custom") {
|
|
85
92
|
attachedFilesOptions.customHandlers?.interactWithFile?.(fileData);
|
|
86
93
|
} else if (fileData.interactMode === "download") {
|
|
87
94
|
window.open(fileData.urlOrId, "_blank")?.focus();
|
|
88
95
|
} else if (fileData.interactMode === "preview") {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
96
|
+
const mimeType = mime.getType(fileData.name);
|
|
97
|
+
if (
|
|
98
|
+
mimeType?.startsWith("text") ||
|
|
99
|
+
mimeType === "application/json"
|
|
100
|
+
) {
|
|
101
|
+
// fetch file content
|
|
102
|
+
const response = await fetch(fileData.urlOrId);
|
|
103
|
+
const content = await response.text();
|
|
104
|
+
previewTextFile(fileData.name, content);
|
|
105
|
+
} else if (mimeType?.startsWith("image")) {
|
|
106
|
+
previewImageFile(fileData.name, fileData.urlOrId);
|
|
107
|
+
} else {
|
|
108
|
+
toast.current?.show({
|
|
109
|
+
severity: "info",
|
|
110
|
+
summary: `Impossible de prévisualiser {fileData.name}`,
|
|
111
|
+
detail: `Aperçu non disponible pour ce type de fichier (${mimeType})`,
|
|
112
|
+
life: 3000,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
95
115
|
}
|
|
96
116
|
}}
|
|
97
117
|
className={styles.fileInteraction}
|
|
@@ -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>) =>
|
|
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) {
|
package/src/index.tsx
CHANGED
|
@@ -13,7 +13,11 @@ import {
|
|
|
13
13
|
useActivityJS,
|
|
14
14
|
useActivityJsEssentials,
|
|
15
15
|
} from "./features/activityJS/ActivityJSProvider";
|
|
16
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
useNotifyIsDirty,
|
|
18
|
+
useCanSave,
|
|
19
|
+
useSave,
|
|
20
|
+
} from "./features/activityData/hooks";
|
|
17
21
|
import { useOrientation } from "./features/layout/hooks";
|
|
18
22
|
import { ActivitySidebarActionsSetter } from "./features/navbar/sidebars/ActivitySidebarActions";
|
|
19
23
|
import { ActivityQuickActionsSetter } from "./features/navbar/activity-menu/ActivityQuickActions";
|
|
@@ -22,6 +26,10 @@ import IsDirtySetter from "./features/activityData/IsDirtySetter";
|
|
|
22
26
|
import AttachedFilesFunctionality from "./features/functionalities/AttachedFilesFunctionality";
|
|
23
27
|
import { useRefreshAttachedFiles } from "./features/functionalities/hooks";
|
|
24
28
|
import { useActivitySettings } from "./features/activitySettings/hooks";
|
|
29
|
+
import {
|
|
30
|
+
usePreviewTextFile,
|
|
31
|
+
usePreviewImageFile,
|
|
32
|
+
} from "./features/functionalities/hooks";
|
|
25
33
|
import { Toast } from "./external/prime";
|
|
26
34
|
import type { ToastMessage } from "./external/prime";
|
|
27
35
|
import type {
|
|
@@ -46,6 +54,8 @@ export {
|
|
|
46
54
|
useOrientation,
|
|
47
55
|
useThemeType,
|
|
48
56
|
useActivitySettings,
|
|
57
|
+
usePreviewTextFile,
|
|
58
|
+
usePreviewImageFile,
|
|
49
59
|
ActivitySidebarActionsSetter,
|
|
50
60
|
ActivityQuickActionsSetter,
|
|
51
61
|
ActivitySettingsSetter,
|
|
@@ -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
|
-
"
|
|
3
|
+
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
|
4
|
+
"target": "ES2020",
|
|
4
5
|
"useDefineForClassFields": true,
|
|
5
|
-
"lib": ["
|
|
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
|
-
"
|
|
12
|
+
"allowSyntheticDefaultImports": true,
|
|
13
13
|
"isolatedModules": true,
|
|
14
|
+
"esModuleInterop": false,
|
|
15
|
+
"resolveJsonModule": true,
|
|
14
16
|
"noEmit": true,
|
|
15
17
|
"jsx": "react-jsx",
|
|
16
18
|
|