@capytale/meta-player 0.4.2 → 0.5.0

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capytale/meta-player",
3
- "version": "0.4.2",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "dev": "vite",
@@ -34,16 +34,11 @@ import {
34
34
  import { initialState as layoutInitialState } from "./features/layout/layoutSlice";
35
35
  import ExitWarning from "./features/activityData/ExitWarning";
36
36
  import type { AntiCheatOptions, UIOptions } from "./types";
37
- import {
38
- AttachedFilesOptions,
39
- defaultAttachedFilesOptions,
40
- } from "./features/functionalities/functionalitiesSlice";
41
37
 
42
38
  type MetaPlayerProps = PropsWithChildren<{
43
39
  activityJSOptions?: LoadOptions;
44
40
  antiCheatOptions?: Partial<AntiCheatOptions>;
45
41
  uiOptions?: Partial<UIOptions>;
46
- attachedFilesOptions?: AttachedFilesOptions;
47
42
  }>;
48
43
 
49
44
  const MetaPlayer: FC<MetaPlayerProps> = (props) => {
@@ -61,10 +56,6 @@ const MetaPlayer: FC<MetaPlayerProps> = (props) => {
61
56
  noSaveForStudents: false,
62
57
  ...props.uiOptions,
63
58
  };
64
- const attachedFilesOptions: AttachedFilesOptions = {
65
- ...defaultAttachedFilesOptions,
66
- ...props.attachedFilesOptions,
67
- };
68
59
  const store = useMemo(
69
60
  () =>
70
61
  makeStore({
@@ -74,9 +65,6 @@ const MetaPlayer: FC<MetaPlayerProps> = (props) => {
74
65
  showWorkflow: !uiOptions.noWorkflow,
75
66
  showSaveForStudents: !uiOptions.noSaveForStudents,
76
67
  },
77
- functionalities: {
78
- attachedFilesOptions,
79
- },
80
68
  }),
81
69
  [],
82
70
  );
@@ -149,6 +149,7 @@ export const activityDataSlice = createAppSlice({
149
149
  setPlayerSettings: create.reducer(
150
150
  (state, action: PayloadAction<MetaPlayerOptions>) => {
151
151
  state.hasInstructions = action.payload.hasInstructions;
152
+ state.canReset = action.payload.canReset;
152
153
  state.pedagoLayout = action.payload.pedagoLayout;
153
154
  state.supportsLightTheme = action.payload.supportsLightTheme;
154
155
  state.supportsDarkTheme = action.payload.supportsDarkTheme;
@@ -239,6 +240,7 @@ export const activityDataSlice = createAppSlice({
239
240
  selectComments: (data) => data.comments,
240
241
  selectGrading: (data) => data.grading,
241
242
  selectHasInstructions: (data) => data.hasInstructions,
243
+ selectCanReset: (data) => data.canReset,
242
244
  selectPedagoLayout: (data) => data.pedagoLayout,
243
245
  selectHasGradingOrComments: (data) => !!(data.grading || data.comments),
244
246
  selectSaveState: (data) => data.saveState,
@@ -310,6 +312,7 @@ export const {
310
312
  selectComments,
311
313
  selectGrading,
312
314
  selectHasInstructions,
315
+ selectCanReset,
313
316
  selectPedagoLayout,
314
317
  selectHasGradingOrComments,
315
318
  selectSaveState,
@@ -1,5 +1,6 @@
1
1
  export type MetaPlayerOptions = {
2
2
  hasInstructions: boolean;
3
+ canReset: boolean;
3
4
  pedagoLayout:
4
5
  | "horizontal"
5
6
  | "vertical"
@@ -11,6 +12,7 @@ export type MetaPlayerOptions = {
11
12
 
12
13
  export const defaultMetaPlayerOptions: MetaPlayerOptions = {
13
14
  hasInstructions: true,
15
+ canReset: true,
14
16
  pedagoLayout: "default-horizontal",
15
17
  supportsLightTheme: true,
16
18
  supportsDarkTheme: false,
@@ -1,5 +1,6 @@
1
1
  import { useAppDispatch, useAppSelector } from "../../app/hooks";
2
2
  import {
3
+ selectCanReset,
3
4
  selectMode,
4
5
  selectWorkflow,
5
6
  setSaveState,
@@ -13,6 +14,7 @@ type UseResetProps = {
13
14
 
14
15
  export const useReset = (props: UseResetProps) => {
15
16
  const dispatch = useAppDispatch();
17
+ const canReset = useAppSelector(selectCanReset);
16
18
  const beforeReset = useAppSelector(selectBeforeReset);
17
19
  const afterReset = useAppSelector(selectAfterReset);
18
20
  const activityJs = useActivityJS();
@@ -21,6 +23,9 @@ export const useReset = (props: UseResetProps) => {
21
23
  if (mode !== "assignment" || workflow !== "current") {
22
24
  return null;
23
25
  }
26
+ if (!canReset) {
27
+ return null;
28
+ }
24
29
  return async (reload: boolean = true) => {
25
30
  if (!activityJs.activitySession) {
26
31
  throw new Error("No activity session to reset");
@@ -0,0 +1,28 @@
1
+ import { FC, useEffect } from "react";
2
+ import { AttachedFilesOptions, setAttachedFilesOptions } from "./functionalitiesSlice";
3
+ import { useAppDispatch } from "../../app/hooks";
4
+
5
+ type AttachedFilesFunctionalityProps = {
6
+ options: Omit<AttachedFilesOptions, "enabled">;
7
+ };
8
+
9
+ const AttachedFilesFunctionality: FC<AttachedFilesFunctionalityProps> = ({
10
+ options,
11
+ }) => {
12
+ const dispatch = useAppDispatch();
13
+ useEffect(() => {
14
+ dispatch(setAttachedFilesOptions({
15
+ ...options,
16
+ enabled: true,
17
+ }));
18
+ return () => {
19
+ dispatch(setAttachedFilesOptions({
20
+ ...options,
21
+ enabled: false,
22
+ }));
23
+ };
24
+ });
25
+ return null;
26
+ }
27
+
28
+ export default AttachedFilesFunctionality;
@@ -4,7 +4,9 @@ import { createAppSlice } from "../../app/createAppSlice";
4
4
  export type AttachedFileData = {
5
5
  name: string;
6
6
  isTemporary: boolean;
7
- interactionMode: "download" | "preview" | "preview-or-download" | "custom";
7
+ canDownload: boolean;
8
+ canCopyLink: boolean;
9
+ interactionMode: "download" | "preview" | "custom" | "none";
8
10
  urlOrId: string;
9
11
  }
10
12
 
@@ -20,7 +20,9 @@ export const useAttachedFiles = () => {
20
20
  name: name,
21
21
  urlOrId: file,
22
22
  isTemporary: false,
23
- interactionMode: "download",
23
+ canDownload: true,
24
+ canCopyLink: true,
25
+ interactionMode: "preview",
24
26
  };
25
27
  });
26
28
  }, [files]);
@@ -0,0 +1,26 @@
1
+ .attachedFiles {
2
+ margin: 4px 0;
3
+ }
4
+
5
+ .fileRow {
6
+ display: flex;
7
+ align-items: center;
8
+ }
9
+
10
+ .fileInteraction {
11
+ flex-shrink: 1;
12
+ flex-grow: 1;
13
+ padding-left: 0.2rem;
14
+ padding-right: 0.2rem;
15
+ & :global(.p-button-label) {
16
+ text-align: left;
17
+ text-overflow: ellipsis;
18
+ overflow-x: hidden;
19
+ font-size: 0.8rem;
20
+ }
21
+ }
22
+
23
+ .fileSmallInteraction {
24
+ flex-shrink: 0;
25
+ flex-grow: 0;
26
+ }
@@ -1,44 +1,109 @@
1
- import { FC } from "react";
1
+ import { FC, useRef } from "react";
2
2
  import { useAttachedFiles } from "../functionalities/hooks";
3
3
  import { useAppSelector } from "../../app/hooks";
4
- import { selectAttachedFilesOptions } from "../functionalities/functionalitiesSlice";
4
+ import {
5
+ AttachedFileData,
6
+ selectAttachedFilesOptions,
7
+ } from "../functionalities/functionalitiesSlice";
8
+ import { Button } from "primereact/button";
9
+ import { Toast } from "primereact/toast";
10
+
11
+ import styles from "./AttachedFilesSidebarContent.module.scss";
12
+ import { copyToClipboard } from "../../utils/clipboard";
13
+ import { downloadFile } from "../../utils/download";
5
14
 
6
15
  const AttachedFilesSidebarContent: FC = () => {
7
16
  const filesData = useAttachedFiles();
8
- const attachedFilesOptions = useAppSelector(selectAttachedFilesOptions);
9
17
 
10
18
  return (
11
- <div>
19
+ <div className={styles.attachedFiles}>
12
20
  {filesData.map((file) => (
13
- <p key={file.urlOrId}>
14
- <a
15
- href={file.urlOrId}
16
- target="_blank"
17
- rel="noreferrer"
18
- download={file.name}
19
- style={{
20
- color: file.isTemporary ? "rgb(200, 0, 0)" : "rgb(0, 50, 200)",
21
- textDecoration:
22
- file.interactionMode === "download" ? "underline" : "none",
23
- }}
24
- onClick={(e) => {
25
- if (file.interactionMode !== "download") {
26
- e.preventDefault();
27
- if (
28
- file.interactionMode === "custom" &&
29
- attachedFilesOptions.middleware?.handleFileClick
30
- ) {
31
- attachedFilesOptions.middleware.handleFileClick(file);
32
- }
33
- }
34
- }}
35
- >
36
- {file.name}
37
- </a>
38
- </p>
21
+ <AttachedFileLinks key={file.name} fileData={file} />
39
22
  ))}
40
23
  </div>
41
24
  );
42
25
  };
43
26
 
27
+ const AttachedFileLinks: FC<{ fileData: AttachedFileData }> = ({
28
+ fileData,
29
+ }) => {
30
+ const attachedFilesOptions = useAppSelector(selectAttachedFilesOptions);
31
+ const toast = useRef<Toast>(null);
32
+
33
+ return (
34
+ <div className={styles.fileRow}>
35
+ <Toast ref={toast} position="bottom-right" />
36
+ <Button
37
+ label={fileData.name}
38
+ severity={fileData.isTemporary ? "warning" : "secondary"}
39
+ disabled={fileData.interactionMode === "none"}
40
+ onClick={() => {
41
+ if (fileData.interactionMode === "custom") {
42
+ attachedFilesOptions.middleware?.handleFileClick?.(fileData);
43
+ } else if (fileData.interactionMode === "download") {
44
+ window.open(fileData.urlOrId, "_blank")?.focus();
45
+ } else if (fileData.interactionMode === "preview") {
46
+ toast.current?.show({
47
+ severity: "info",
48
+ summary: "Aperçu non disponible",
49
+ detail: fileData.name,
50
+ life: 3000,
51
+ });
52
+ }
53
+ }}
54
+ className={styles.fileInteraction}
55
+ text
56
+ />
57
+ {fileData.canDownload && (
58
+ <Button
59
+ icon="pi pi-download"
60
+ severity="secondary"
61
+ onClick={() => {
62
+ downloadFile(fileData.urlOrId, fileData.name);
63
+ toast.current?.show({
64
+ severity: "success",
65
+ summary: "Téléchargement lancé",
66
+ detail: fileData.name,
67
+ life: 3000,
68
+ });
69
+ }}
70
+ className={styles.fileSmallInteraction}
71
+ text
72
+ />
73
+ )}
74
+ {fileData.canCopyLink && (
75
+ <Button
76
+ icon="pi pi-link"
77
+ severity="secondary"
78
+ onClick={() => {
79
+ // copy link to clipboard
80
+ copyToClipboard(fileData.urlOrId, (success, error) => {
81
+ if (success) {
82
+ toast.current?.show({
83
+ severity: "success",
84
+ summary: "Lien copié",
85
+ detail: fileData.name,
86
+ life: 3000,
87
+ });
88
+ } else {
89
+ toast.current?.show({
90
+ severity: "error",
91
+ summary: "Erreur lors de la copie du lien",
92
+ detail: error,
93
+ life: 5000,
94
+ });
95
+ }
96
+ });
97
+ }}
98
+ style={{
99
+ flexGrow: 0,
100
+ flexShrink: 0,
101
+ }}
102
+ text
103
+ />
104
+ )}
105
+ </div>
106
+ );
107
+ };
108
+
44
109
  export default AttachedFilesSidebarContent;
@@ -34,6 +34,28 @@ export const PedagoCommands = () => {
34
34
  );
35
35
  };
36
36
 
37
+ export const CloseOnlyPedagoCommands: FC = () => {
38
+ const dispatch = useAppDispatch();
39
+ return (
40
+ <div className={styles.closeOnlyPedagoCommands}>
41
+ <Button
42
+ severity="secondary"
43
+ icon="pi pi-times"
44
+ rounded
45
+ text
46
+ aria-label="Masquer l'évaluation"
47
+ tooltip="Masquer l'évaluation"
48
+ tooltipOptions={{
49
+ position: "left",
50
+ showDelay: settings.TOOLTIP_SHOW_DELAY,
51
+ }}
52
+ onClick={() => dispatch(toggleIsPedagoVisible())}
53
+ style={{ flexShrink: 0 }}
54
+ />
55
+ </div>
56
+ );
57
+ };
58
+
37
59
  type DocumentSelectorHzItem = {
38
60
  name: string;
39
61
  value: PedagoTab;
@@ -24,7 +24,7 @@ import {
24
24
  import { ChangeEventHandler, FC } from "react";
25
25
  import { DivProps } from "react-html-props";
26
26
  import SharedNotesEditor from "./SharedNotesEditor";
27
- import { PedagoCommands } from "./PedagoCommands";
27
+ import { CloseOnlyPedagoCommands, PedagoCommands } from "./PedagoCommands";
28
28
  import { PdfEditor } from "./PdfEditor";
29
29
 
30
30
  const Pedago: React.FC<DivProps> = ({ className, ...props }) => {
@@ -42,8 +42,11 @@ const Pedago: React.FC<DivProps> = ({ className, ...props }) => {
42
42
 
43
43
  if (!hasInstructions) {
44
44
  return (
45
- <div className={styles.gradingPanel}>
46
- <Grading />
45
+ <div className={styles.onlyGradingPedago}>
46
+ <CloseOnlyPedagoCommands />
47
+ <div className={styles.gradingPanel}>
48
+ <Grading />
49
+ </div>
47
50
  </div>
48
51
  );
49
52
  }
@@ -1,3 +1,14 @@
1
+ .onlyGradingPedago {
2
+ display: flex;
3
+ width: 100%;
4
+ :global(.layout-horizontal) & {
5
+ flex-direction: row-reverse;
6
+ }
7
+ :global(.layout-vertical) & {
8
+ flex-direction: column;
9
+ }
10
+ }
11
+
1
12
  .pedago {
2
13
  display: flex;
3
14
  height: 100%;
@@ -10,6 +21,18 @@
10
21
  }
11
22
  }
12
23
 
24
+ .closeOnlyPedagoCommands {
25
+ flex-direction: column;
26
+ flex-shrink: 0;
27
+ display: flex;
28
+ background-color: var(--surface-200);
29
+ :global(.layout-vertical) & {
30
+ gap: 8px;
31
+ flex-direction: row-reverse;
32
+ align-items: center;
33
+ }
34
+ }
35
+
13
36
  .pedagoCommands {
14
37
  flex-direction: column;
15
38
  flex-shrink: 0;
package/src/index.tsx CHANGED
@@ -18,6 +18,7 @@ import { ActivitySidebarActionsSetter } from "./features/navbar/ActivitySidebarA
18
18
  import { ActivityQuickActionsSetter } from "./features/navbar/ActivityQuickActions";
19
19
  import ActivitySettingsSetter from "./features/activitySettings/ActivitySettingsSetter";
20
20
  import IsDirtySetter from "./features/activityData/IsDirtySetter";
21
+ import AttachedFilesFunctionality from "./features/functionalities/AttachedFilesFunctionality";
21
22
  import { Toast } from "./external/prime";
22
23
  import type { ToastMessage } from "./external/prime";
23
24
 
@@ -39,6 +40,7 @@ export {
39
40
  ActivityQuickActionsSetter,
40
41
  ActivitySettingsSetter,
41
42
  IsDirtySetter,
43
+ AttachedFilesFunctionality,
42
44
  Toast,
43
45
  };
44
46
  export type { ToastMessage };
@@ -0,0 +1,8 @@
1
+ export function downloadFile(url: string, name?: string) {
2
+ const a = document.createElement("a");
3
+ a.href = url;
4
+ a.download = name || url.split("/").pop() || "download";
5
+ document.body.appendChild(a);
6
+ a.click();
7
+ document.body.removeChild(a);
8
+ }