@edifice.io/react 2.6.9-develop-pedago.20261013143657 → 2.6.9-develop-integration.20261014184332

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.
Files changed (41) hide show
  1. package/dist/components/RadioTile/RadioTile.d.ts +35 -0
  2. package/dist/components/RadioTile/RadioTile.js +22 -0
  3. package/dist/components/RadioTile/index.d.ts +2 -0
  4. package/dist/components/Tree/utilities/tree.js +21 -1
  5. package/dist/components/index.d.ts +1 -0
  6. package/dist/hooks/index.d.ts +2 -0
  7. package/dist/hooks/useGoogleDriveSearch/index.d.ts +1 -0
  8. package/dist/hooks/useGoogleDriveSearch/useGoogleDriveSearch.d.ts +14 -0
  9. package/dist/hooks/useGoogleDriveSearch/useGoogleDriveSearch.js +60 -0
  10. package/dist/hooks/useNextcloudSearch/index.d.ts +1 -0
  11. package/dist/hooks/useNextcloudSearch/useNextcloudSearch.d.ts +14 -0
  12. package/dist/hooks/useNextcloudSearch/useNextcloudSearch.js +64 -0
  13. package/dist/index.js +196 -182
  14. package/dist/modules/multimedia/FileCard/GoogleDriveFileCard.d.ts +25 -0
  15. package/dist/modules/multimedia/FileCard/GoogleDriveFileCard.js +102 -0
  16. package/dist/modules/multimedia/FileCard/NextcloudFileCard.d.ts +25 -0
  17. package/dist/modules/multimedia/FileCard/NextcloudFileCard.js +102 -0
  18. package/dist/modules/multimedia/FileCard/index.d.ts +4 -0
  19. package/dist/modules/multimedia/GoogleDrive/GoogleDrive.d.ts +24 -0
  20. package/dist/modules/multimedia/GoogleDrive/GoogleDrive.js +100 -0
  21. package/dist/modules/multimedia/GoogleDrive/index.d.ts +2 -0
  22. package/dist/modules/multimedia/Linker/InternalLinker/InternalLinker.d.ts +7 -1
  23. package/dist/modules/multimedia/Linker/InternalLinker/InternalLinker.js +2 -0
  24. package/dist/modules/multimedia/MediaLibrary/MediaLibrary.d.ts +2 -4
  25. package/dist/modules/multimedia/MediaLibrary/MediaLibrary.js +48 -14
  26. package/dist/modules/multimedia/MediaLibrary/MediaLibraryContext.d.ts +0 -4
  27. package/dist/modules/multimedia/MediaLibrary/innertabs/GoogleDrive.d.ts +1 -0
  28. package/dist/modules/multimedia/MediaLibrary/innertabs/GoogleDrive.js +61 -0
  29. package/dist/modules/multimedia/MediaLibrary/innertabs/Nextcloud.d.ts +1 -0
  30. package/dist/modules/multimedia/MediaLibrary/innertabs/Nextcloud.js +48 -0
  31. package/dist/modules/multimedia/MediaLibrary/innertabs/Upload.js +1 -2
  32. package/dist/modules/multimedia/MediaLibrary/innertabs/Workspace.js +0 -3
  33. package/dist/modules/multimedia/MediaLibrary/innertabs/index.d.ts +2 -0
  34. package/dist/modules/multimedia/MediaLibrary/innertabs/index.js +4 -0
  35. package/dist/modules/multimedia/Nextcloud/Nextcloud.d.ts +24 -0
  36. package/dist/modules/multimedia/Nextcloud/Nextcloud.js +124 -0
  37. package/dist/modules/multimedia/Nextcloud/index.d.ts +2 -0
  38. package/dist/modules/multimedia/Workspace/Workspace.js +8 -5
  39. package/dist/modules/multimedia/index.d.ts +2 -0
  40. package/dist/multimedia.js +26 -18
  41. package/package.json +6 -6
@@ -0,0 +1,102 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { useRef } from "react";
3
+ import { DocumentHelper, odeServices } from "@edifice.io/client";
4
+ import clsx from "clsx";
5
+ import SvgIconLandscape from "../../icons/components/IconLandscape.js";
6
+ import SvgIconMic from "../../icons/components/IconMic.js";
7
+ import SvgIconTextPage from "../../icons/components/IconTextPage.js";
8
+ import SvgIconVideo from "../../icons/components/IconVideo.js";
9
+ import FileIcon from "./FileIcon.js";
10
+ import useThumbnail from "../../../hooks/useThumbnail/useThumbnail.js";
11
+ import Card from "../../../components/Card/Card.js";
12
+ const roleMappings = {
13
+ csv: {
14
+ icon: ".CSV",
15
+ color: "bg-orange-200"
16
+ },
17
+ xls: {
18
+ icon: ".XLS",
19
+ color: "bg-green-200"
20
+ },
21
+ doc: {
22
+ icon: ".DOC",
23
+ color: "bg-blue-200"
24
+ },
25
+ txt: {
26
+ icon: ".TXT",
27
+ color: "bg-blue-200"
28
+ },
29
+ pdf: {
30
+ icon: ".PDF",
31
+ color: "bg-red-200"
32
+ },
33
+ audio: {
34
+ icon: /* @__PURE__ */ jsx(SvgIconMic, { width: 22, height: 22 }),
35
+ color: "bg-red-200"
36
+ },
37
+ ppt: {
38
+ icon: ".PPT",
39
+ color: "bg-red-200"
40
+ },
41
+ img: {
42
+ icon: /* @__PURE__ */ jsx(SvgIconLandscape, { width: 22, height: 22 }),
43
+ color: "bg-green-200"
44
+ },
45
+ video: {
46
+ icon: /* @__PURE__ */ jsx(SvgIconVideo, { width: 22, height: 22 }),
47
+ color: "bg-purple-200"
48
+ },
49
+ zip: {
50
+ icon: ".ZIP",
51
+ color: "bg-gray-300"
52
+ },
53
+ md: {
54
+ icon: ".MD",
55
+ color: "bg-blue-200"
56
+ },
57
+ unknown: {
58
+ icon: /* @__PURE__ */ jsx(SvgIconTextPage, { width: 22, height: 22 }),
59
+ color: "bg-gray-300"
60
+ }
61
+ };
62
+ function getRoleMap(type, customIcon, customColor) {
63
+ return customIcon !== void 0 || customColor !== void 0 ? {
64
+ icon: customIcon || /* @__PURE__ */ jsx(SvgIconTextPage, { width: 22, height: 22 }),
65
+ color: customColor || "bg-gray-300",
66
+ hasShadow: !1
67
+ } : roleMappings[type] ?? roleMappings.unknown;
68
+ }
69
+ const NextcloudFileCard = ({
70
+ doc,
71
+ userId,
72
+ isClickable = !0,
73
+ isSelectable = !1,
74
+ isSelected = !1,
75
+ onClick,
76
+ className,
77
+ onSelect,
78
+ isFocused,
79
+ app,
80
+ customIcon,
81
+ customColor
82
+ }) => {
83
+ const ref = useRef(null), type = DocumentHelper.role(doc.contentType, !1), roleMap = getRoleMap(type ?? "unknown", customIcon, customColor), file = clsx("file position-relative rounded", (roleMap == null ? void 0 : roleMap.color) ?? "bg-yellow-200"), mediaSrc = type === "img" ? odeServices.nextcloud().getFileUrl(userId, doc) : null, hasThumbnail = useThumbnail(mediaSrc, {
84
+ ref
85
+ }), imageStyles = hasThumbnail && {
86
+ backgroundImage: `url(${mediaSrc})`,
87
+ backgroundSize: "cover"
88
+ };
89
+ return /* @__PURE__ */ jsx(Card, { className: clsx("card-file", className), isClickable, isSelectable, isSelected, onClick, app, isFocused, onSelect, children: /* @__PURE__ */ jsxs(Card.Body, { space: "8", children: [
90
+ /* @__PURE__ */ jsx("div", { ref, className: file, style: {
91
+ aspectRatio: "16/10",
92
+ ...imageStyles
93
+ }, children: type !== "img" || !hasThumbnail ? /* @__PURE__ */ jsx(FileIcon, { type, roleMap }) : null }),
94
+ /* @__PURE__ */ jsxs("div", { className: "mt-4", children: [
95
+ /* @__PURE__ */ jsx(Card.Text, { children: doc.name }),
96
+ /* @__PURE__ */ jsx(Card.Text, { className: "text-black-50", children: doc == null ? void 0 : doc.ownerDisplayName })
97
+ ] })
98
+ ] }) });
99
+ };
100
+ export {
101
+ NextcloudFileCard as default
102
+ };
@@ -1,2 +1,6 @@
1
1
  export { default as FileCard } from './FileCard';
2
2
  export * from './FileCard';
3
+ export { default as GoogleDriveFileCard } from './GoogleDriveFileCard';
4
+ export * from './GoogleDriveFileCard';
5
+ export { default as NextcloudFileCard } from './NextcloudFileCard';
6
+ export * from './NextcloudFileCard';
@@ -0,0 +1,24 @@
1
+ import { GoogleDriveDocument, Role } from '@edifice.io/client';
2
+ /**
3
+ * GoogleDrive component properties
4
+ */
5
+ export interface GoogleDriveProps {
6
+ /**
7
+ * Notify parent when media elements are successfully selected.
8
+ */
9
+ onSelect: (result: GoogleDriveDocument[]) => void;
10
+ /**
11
+ * Boolean to know if we can select 1 or many files.
12
+ */
13
+ multiple?: boolean | undefined;
14
+ /**
15
+ * Document roles to filter files by; null (default) shows all roles.
16
+ */
17
+ roles?: Role | Role[] | null;
18
+ /**
19
+ * Optional class for styling purpose
20
+ */
21
+ className?: string;
22
+ }
23
+ declare const GoogleDrive: ({ onSelect, multiple, roles, className, }: GoogleDriveProps) => import("react/jsx-runtime").JSX.Element;
24
+ export default GoogleDrive;
@@ -0,0 +1,100 @@
1
+ import { jsxs, jsx } from "react/jsx-runtime";
2
+ import { useState, useCallback, useEffect, useMemo } from "react";
3
+ import { DocumentHelper } from "@edifice.io/client";
4
+ import clsx from "clsx";
5
+ import { useTranslation } from "react-i18next";
6
+ import { findNodeById } from "../../../components/Tree/utilities/tree.js";
7
+ import SvgIconSortAscendingLetters from "../../icons/components/IconSortAscendingLetters.js";
8
+ import SvgIconSortDescendingLetters from "../../icons/components/IconSortDescendingLetters.js";
9
+ import SvgIconSortTime from "../../icons/components/IconSortTime.js";
10
+ import illuError from "@edifice.io/bootstrap/dist/images/emptyscreen/illu-error.svg";
11
+ import illuTrash from "@edifice.io/bootstrap/dist/images/emptyscreen/illu-trash.svg";
12
+ import useUser from "../../../hooks/useUser/useUser.js";
13
+ import useGoogleDriveSearch from "../../../hooks/useGoogleDriveSearch/useGoogleDriveSearch.js";
14
+ import { Grid } from "../../../components/Grid/Grid.js";
15
+ import Tree from "../../../components/Tree/components/Tree.js";
16
+ import SearchBar from "../../../components/SearchBar/SearchBar.js";
17
+ import Flex from "../../../components/Flex/Flex.js";
18
+ import Dropdown from "../../../components/Dropdown/Dropdown.js";
19
+ import EmptyScreen from "../../../components/EmptyScreen/EmptyScreen.js";
20
+ import Button from "../../../components/Button/Button.js";
21
+ import LoadingScreen from "../../../components/LoadingScreen/LoadingScreen.js";
22
+ import GoogleDriveFileCard from "../FileCard/GoogleDriveFileCard.js";
23
+ const ROOT_ID = "root";
24
+ function compare(a, b) {
25
+ return a === b ? 0 : a === void 0 ? -1 : b === void 0 ? 1 : a.localeCompare(b);
26
+ }
27
+ const GoogleDrive = ({
28
+ onSelect,
29
+ multiple = !0,
30
+ roles,
31
+ className
32
+ }) => {
33
+ const {
34
+ t
35
+ } = useTranslation(), {
36
+ user
37
+ } = useUser(), {
38
+ root,
39
+ loadContent,
40
+ loadError
41
+ } = useGoogleDriveSearch(ROOT_ID, t("google-drive.mydrive"), user == null ? void 0 : user.userId), [currentNodeId, setCurrentNodeId] = useState(ROOT_ID), currentNode = findNodeById(root, currentNodeId) ?? root, [searchTerm, setSearchTerm] = useState(void 0), [sortOrder, setSortOrder] = useState(["modified", "desc"]), [selectedDocuments, setSelectedDocuments] = useState([]), handleTreeItemChange = useCallback((nodeId) => {
42
+ setCurrentNodeId(nodeId), loadContent(nodeId);
43
+ }, [loadContent]);
44
+ useEffect(() => {
45
+ loadContent(ROOT_ID);
46
+ }, [loadContent]);
47
+ const documents = useMemo(() => {
48
+ if (!currentNode.files) return;
49
+ const matchesRole = (doc) => {
50
+ if (!roles) return !0;
51
+ const role = DocumentHelper.role(doc.contentType, !1);
52
+ return Array.isArray(roles) ? roles.includes(role) : roles === role;
53
+ }, normalizedSearchTerm = searchTerm == null ? void 0 : searchTerm.toLowerCase(), list = currentNode.files.filter((f) => (!normalizedSearchTerm || f.name.toLowerCase().includes(normalizedSearchTerm)) && matchesRole(f));
54
+ let sortFunction;
55
+ return sortOrder[0] === "name" ? sortFunction = sortOrder[1] === "asc" ? (a, b) => compare(a.name, b.name) : (a, b) => compare(b.name, a.name) : sortFunction = (a, b) => compare(b.lastModified, a.lastModified), list.sort(sortFunction);
56
+ }, [currentNode, searchTerm, sortOrder, roles]), selectedIds = useMemo(() => new Set(selectedDocuments.map((d) => d.id)), [selectedDocuments]), handleSearchChange = useCallback((e) => {
57
+ setSearchTerm(e.target.value);
58
+ }, [setSearchTerm]);
59
+ function getSortOrderLabel() {
60
+ return sortOrder[0] === "name" ? sortOrder[1] === "asc" ? t("sort.order.alpha.asc") : t("sort.order.alpha.desc") : t("sort.order.modify.desc");
61
+ }
62
+ function handleSelectDoc(doc) {
63
+ let currentDocuments = [...selectedDocuments];
64
+ multiple ? currentDocuments.some((selectedDocument) => selectedDocument.id === doc.id) ? currentDocuments = currentDocuments.filter((selectedDocument) => selectedDocument.id !== doc.id) : currentDocuments = [...currentDocuments, doc] : currentDocuments = [doc], setSelectedDocuments(currentDocuments), onSelect(currentDocuments);
65
+ }
66
+ const googleDrive = clsx("workspace flex-grow-1 gap-0", className);
67
+ return /* @__PURE__ */ jsxs(Grid, { className: googleDrive, children: [
68
+ /* @__PURE__ */ jsx(Grid.Col, { sm: "12", md: "3", xl: "4", className: "workspace-folders p-12 pt-0 gap-12", children: /* @__PURE__ */ jsx("div", { style: {
69
+ position: "sticky",
70
+ top: 0,
71
+ paddingTop: "1.2rem"
72
+ }, children: /* @__PURE__ */ jsx(Tree, { nodes: root, selectedNodeId: currentNodeId, showIcon: !0, onTreeItemClick: handleTreeItemChange, onTreeItemUnfold: handleTreeItemChange }) }) }),
73
+ /* @__PURE__ */ jsx(Grid.Col, { sm: "12", md: "5", xl: "8", children: /* @__PURE__ */ jsxs(Grid, { className: "flex-grow-1 gap-0", children: [
74
+ /* @__PURE__ */ jsxs(Grid.Col, { sm: "4", md: "8", xl: "12", children: [
75
+ /* @__PURE__ */ jsx("div", { className: "workspace-search px-16 py-8 ", children: /* @__PURE__ */ jsx(SearchBar, { isVariant: !0, className: "gap-16", onChange: handleSearchChange }) }),
76
+ /* @__PURE__ */ jsxs(Flex, { className: "px-8 py-4", align: "center", justify: "end", children: [
77
+ /* @__PURE__ */ jsx("small", { className: "text-muted", children: t("workspace.search.order") }),
78
+ /* @__PURE__ */ jsxs(Dropdown, { children: [
79
+ /* @__PURE__ */ jsx(Dropdown.Trigger, { size: "sm", label: getSortOrderLabel(), variant: "ghost" }),
80
+ /* @__PURE__ */ jsxs(Dropdown.Menu, { children: [
81
+ /* @__PURE__ */ jsx(Dropdown.Item, { icon: /* @__PURE__ */ jsx(SvgIconSortTime, {}), onClick: () => setSortOrder(["modified", "desc"]), children: t("sort.order.modify.desc") }),
82
+ /* @__PURE__ */ jsx(Dropdown.Item, { icon: /* @__PURE__ */ jsx(SvgIconSortAscendingLetters, {}), onClick: () => setSortOrder(["name", "asc"]), children: t("sort.order.alpha.asc") }),
83
+ /* @__PURE__ */ jsx(Dropdown.Item, { icon: /* @__PURE__ */ jsx(SvgIconSortDescendingLetters, {}), onClick: () => setSortOrder(["name", "desc"]), children: t("sort.order.alpha.desc") })
84
+ ] })
85
+ ] })
86
+ ] })
87
+ ] }),
88
+ /* @__PURE__ */ jsx(Grid.Col, { sm: "4", md: "8", xl: "12", className: "p-8 gap-8", children: loadError ? /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: "12", className: "h-full w-100", justify: "center", align: "center", children: [
89
+ /* @__PURE__ */ jsx(EmptyScreen, { imageSrc: illuError, title: t("google-drive.error.title"), text: t("google-drive.error.description") }),
90
+ /* @__PURE__ */ jsx(Button, { onClick: () => loadContent(currentNodeId), children: t("retry") })
91
+ ] }) : documents ? documents.length !== 0 ? /* @__PURE__ */ jsx("div", { className: "grid grid-workspace", children: documents.map((doc) => {
92
+ const isSelected = selectedIds.has(doc.id);
93
+ return /* @__PURE__ */ jsx(GoogleDriveFileCard, { doc, userId: user.userId, isSelected, onClick: () => handleSelectDoc(doc) }, doc.id);
94
+ }) }) : /* @__PURE__ */ jsx(EmptyScreen, { imageSrc: illuTrash, text: t("workspace.empty.docSpace"), title: t("explorer.emptyScreen.trash.title") }) : /* @__PURE__ */ jsx(LoadingScreen, {}) })
95
+ ] }) })
96
+ ] });
97
+ };
98
+ export {
99
+ GoogleDrive as default
100
+ };
@@ -0,0 +1,2 @@
1
+ export { default as GoogleDrive } from './GoogleDrive';
2
+ export * from './GoogleDrive';
@@ -1,3 +1,4 @@
1
+ import { ReactNode } from 'react';
1
2
  import { App, ILinkedResource } from '@edifice.io/client';
2
3
  /**
3
4
  * Definition of an internal link.
@@ -32,6 +33,11 @@ export interface InternalLinkerProps {
32
33
  applicationList?: ApplicationOption[];
33
34
  /** Whether to show the application selector */
34
35
  showApplicationSelector?: boolean;
36
+ /**
37
+ * Rendered on the search row, where the application selector sits, for
38
+ * consumers that hide that selector but still need a filter of their own.
39
+ */
40
+ searchFilter?: ReactNode;
35
41
  /** Disable application selecetor */
36
42
  disableApplicationSelector?: boolean;
37
43
  /** Optional callback to filter resources after loading. Applied in addition to search filters. */
@@ -49,5 +55,5 @@ export interface InternalLinkerProps {
49
55
  /** Label for the "load more" button. Defaults to the `bbm.linker.see.more` i18n key. */
50
56
  loadMoreLabel?: string;
51
57
  }
52
- export declare const InternalLinker: ({ appCode, defaultAppCode, defaultResourceId, onChange, onSelect, multiple, resourceList, applicationList, showApplicationSelector, disableApplicationSelector, resourceFilter, onSearch, hasMoreResources, onLoadMore, loadMoreLabel, }: InternalLinkerProps) => import("react/jsx-runtime").JSX.Element;
58
+ export declare const InternalLinker: ({ appCode, defaultAppCode, defaultResourceId, onChange, onSelect, multiple, resourceList, applicationList, showApplicationSelector, searchFilter, disableApplicationSelector, resourceFilter, onSearch, hasMoreResources, onLoadMore, loadMoreLabel, }: InternalLinkerProps) => import("react/jsx-runtime").JSX.Element;
53
59
  export default InternalLinker;
@@ -23,6 +23,7 @@ const InternalLinker = ({
23
23
  resourceList,
24
24
  applicationList,
25
25
  showApplicationSelector = !0,
26
+ searchFilter,
26
27
  disableApplicationSelector = !1,
27
28
  resourceFilter,
28
29
  onSearch,
@@ -131,6 +132,7 @@ const InternalLinker = ({
131
132
  ] }), variant: "ghost", size: "md", disabled: disableApplicationSelector }),
132
133
  /* @__PURE__ */ jsx(Dropdown.Menu, { children: options == null ? void 0 : options.map((option) => /* @__PURE__ */ jsx(Dropdown.Item, { icon: option.icon, onClick: () => handleOptionClick(option), children: option.displayName }, option.application)) })
133
134
  ] }) }),
135
+ !showApplicationSelector && searchFilter && /* @__PURE__ */ jsx("div", { className: "flex-shrink-1 px-8 py-12 border-end align-self-center", children: searchFilter }),
134
136
  /* @__PURE__ */ jsx("div", { className: "flex-grow-1 align-self-center", children: /* @__PURE__ */ jsx("form", { className: "gap-16 d-flex w-100 align-items-center px-16 py-8", onSubmit: handleSubmit, children: /* @__PURE__ */ jsx(SearchBar, { isVariant: !0, placeholder: t("search"), size: "lg", className: "w-100", disabled: !selectedApplication, onChange: handleSearchChange }) }) })
135
137
  ] }),
136
138
  /* @__PURE__ */ jsxs("div", { className: "internal-linker flex-grow-1 w-100 rounded-bottom border gap-0 overflow-auto", children: [
@@ -1,4 +1,4 @@
1
- import { WorkspaceElement, WorkspaceVisibility } from '@edifice.io/client';
1
+ import { NextcloudDocument, WorkspaceElement, WorkspaceVisibility } from '@edifice.io/client';
2
2
  import { TabsItemProps } from '../../../components';
3
3
  import { ExternalLinkTabProps } from './innertabs/ExternalLink';
4
4
  import { InternalLinkTabProps, InternalLinkTabResult } from './innertabs/InternalLink';
@@ -54,7 +54,7 @@ export interface MediaLibraryRef {
54
54
  /**
55
55
  * The resulting type depends on the actual selected Tab when modal is closed.
56
56
  */
57
- export type MediaLibraryResult = WorkspaceElement[] | WorkspaceElement | InternalLinkTabResult | string | /*TODO type des autres résultats ?*/ any;
57
+ export type MediaLibraryResult = WorkspaceElement[] | WorkspaceElement | NextcloudDocument[] | InternalLinkTabResult | string | /*TODO type des autres résultats ?*/ any;
58
58
  /**
59
59
  * MediaLibrary component properties
60
60
  */
@@ -65,8 +65,6 @@ export interface MediaLibraryProps {
65
65
  visibility: WorkspaceVisibility;
66
66
  /** Allow selecting / uploading multiple files at once ? */
67
67
  multiple?: boolean;
68
- /** Restrict attachments to PDF files in Workspace and Upload. */
69
- pdfOnly?: boolean;
70
68
  /**
71
69
  * Called when the user validates the modal (Add button).
72
70
  * @param result depends on which InnerTab is visible
@@ -5,6 +5,7 @@ import { useTranslation } from "react-i18next";
5
5
  import SvgIconApplications from "../../icons/components/IconApplications.js";
6
6
  import SvgIconCode from "../../icons/components/IconCode.js";
7
7
  import SvgIconFolder from "../../icons/components/IconFolder.js";
8
+ import SvgIconGlobe2 from "../../icons/components/IconGlobe2.js";
8
9
  import SvgIconGlobe from "../../icons/components/IconGlobe.js";
9
10
  import SvgIconMic from "../../icons/components/IconMic.js";
10
11
  import SvgIconRecordVideo from "../../icons/components/IconRecordVideo.js";
@@ -12,6 +13,7 @@ import SvgIconSmartphone from "../../icons/components/IconSmartphone.js";
12
13
  import { InnerTabs } from "./innertabs/index.js";
13
14
  import { MediaLibraryContext } from "./MediaLibraryContext.js";
14
15
  import useHasWorkflow from "../../../hooks/useHasWorkflow/useHasWorkflow.js";
16
+ import usePublicConf from "../../../hooks/usePublicConf/usePublicConf.js";
15
17
  import useHttpErrorToast from "../../../hooks/useHttpErrorToast/useHttpErrorToast.js";
16
18
  import Modal from "../../../components/Modal/Modal.js";
17
19
  import { Tabs } from "../../../components/Tabs/components/Tabs.js";
@@ -29,9 +31,13 @@ const orderedTabs = [
29
31
  // Filesystem browser + drag'n'drop of files
30
32
  "workspace",
31
33
  // Media browser
34
+ "nextcloud",
35
+ // Nextcloud media browser
36
+ "google-drive",
37
+ // Google Drive media browser
32
38
  "video-embedder"
33
39
  // Link to a hosted video
34
- ], mediaLibraryTypes = {
40
+ ], GOOGLE_DRIVE_VIEW_WORKFLOW = "io.edifice.google.drive.controller.GoogleDriveController|view", mediaLibraryTypes = {
35
41
  none: null,
36
42
  audio: {
37
43
  title: "bbm.audio.modal.title",
@@ -66,12 +72,11 @@ const orderedTabs = [
66
72
  appCode,
67
73
  visibility,
68
74
  multiple,
69
- pdfOnly,
70
75
  onSuccess,
71
76
  onCancel,
72
77
  onTabChange
73
78
  }, ref) => {
74
- var _a;
79
+ var _a, _b, _c;
75
80
  const linkTabProps = useRef(), refModal = useRef(null);
76
81
  useImperativeHandle(ref, () => ({
77
82
  show,
@@ -82,7 +87,9 @@ const orderedTabs = [
82
87
  }));
83
88
  const {
84
89
  t
85
- } = useTranslation(), workspaceCreateWorkflow = useHasWorkflow("org.entcore.workspace.controllers.WorkspaceController|addDocument"), videoCaptureWorkflow = useHasWorkflow("com.opendigitaleducation.video.controllers.VideoController|capture"), [type, setType] = useState(null);
90
+ } = useTranslation(), workspaceCreateWorkflow = useHasWorkflow("org.entcore.workspace.controllers.WorkspaceController|addDocument"), videoCaptureWorkflow = useHasWorkflow("com.opendigitaleducation.video.controllers.VideoController|capture"), nextcloudViewWorkflow = useHasWorkflow("fr.openent.nextcloud.controller.NextcloudController|view"), googleDriveViewWorkflow = useHasWorkflow(GOOGLE_DRIVE_VIEW_WORKFLOW), {
91
+ data: workspacePublicConf
92
+ } = usePublicConf("workspace"), enableNextcloudTab = ((_a = workspacePublicConf == null ? void 0 : workspacePublicConf["folder-service"]) == null ? void 0 : _a.includes("nextcloud")) && nextcloudViewWorkflow, enableGoogleDriveTab = ((_b = workspacePublicConf == null ? void 0 : workspacePublicConf["folder-service"]) == null ? void 0 : _b.includes("google-drive")) && googleDriveViewWorkflow, [type, setType] = useState(null);
86
93
  useHttpErrorToast({
87
94
  active: !!type,
88
95
  isDismissible: !0,
@@ -97,6 +104,23 @@ const orderedTabs = [
97
104
  availableFor: ["audio", "video", "image", "attachment"],
98
105
  isEnable: null
99
106
  },
107
+ nextcloud: {
108
+ id: "nextcloud",
109
+ icon: /* @__PURE__ */ jsx(SvgIconGlobe2, {}),
110
+ label: t("bbm.nextcloud"),
111
+ content: /* @__PURE__ */ jsx(InnerTabs.Nextcloud, {}),
112
+ availableFor: ["audio", "video", "image", "attachment"],
113
+ isEnable: () => !!enableNextcloudTab
114
+ },
115
+ "google-drive": {
116
+ id: "google-drive",
117
+ // FIXME No Google Drive icon in the pack yet; reusing the generic one.
118
+ icon: /* @__PURE__ */ jsx(SvgIconGlobe2, {}),
119
+ label: t("bbm.google-drive"),
120
+ content: /* @__PURE__ */ jsx(InnerTabs.GoogleDrive, {}),
121
+ availableFor: ["audio", "video", "image", "attachment"],
122
+ isEnable: () => !!enableGoogleDriveTab
123
+ },
100
124
  upload: {
101
125
  id: "upload",
102
126
  icon: /* @__PURE__ */ jsx(SvgIconSmartphone, {}),
@@ -163,7 +187,7 @@ const orderedTabs = [
163
187
  ), [defaultTabId, setDefaultTabId] = useState(), defaultTabIdx = useMemo(() => {
164
188
  const index = tabs.findIndex((t2) => t2.id === defaultTabId);
165
189
  return 0 > index || index >= tabs.length ? 0 : index;
166
- }, [tabs, defaultTabId]), [resultCounter, setResultCounter] = useState(), [result, setResult] = useState(), [deletionsOnCancel, setDeletionsOnCancel] = useState([]), [onSuccessAction, setPreSuccess] = useState();
190
+ }, [tabs, defaultTabId]), [resultCounter, setResultCounter] = useState(), [result, setResult] = useState(), [deletionsOnCancel, setDeletionsOnCancel] = useState([]), [onSuccessAction, setPreSuccess] = useState(), [isSubmitting, setIsSubmitting] = useState(!1);
167
191
  function setVisibleTab(tab) {
168
192
  throw tabs.findIndex((t2) => t2.id === tab) < 0 ? "tab.not.visible" : "not.implemented.yet";
169
193
  }
@@ -178,29 +202,39 @@ const orderedTabs = [
178
202
  linkTabProps.current = props, "resourceId" in props || "appPrefix" in props || setDefaultTabId("external-link"), setType("hyperlink");
179
203
  };
180
204
  useEffect(() => {
181
- var _a2, _b;
205
+ var _a2, _b2;
182
206
  const typeKey = type || "none";
183
- !defaultTabId && typeof ((_a2 = mediaLibraryTypes[typeKey]) == null ? void 0 : _a2.defaultTab) == "string" && setDefaultTabId((_b = mediaLibraryTypes[typeKey]) == null ? void 0 : _b.defaultTab);
207
+ !defaultTabId && typeof ((_a2 = mediaLibraryTypes[typeKey]) == null ? void 0 : _a2.defaultTab) == "string" && setDefaultTabId((_b2 = mediaLibraryTypes[typeKey]) == null ? void 0 : _b2.defaultTab);
184
208
  }, [defaultTabId, type]);
185
- const modalHeader = t(((_a = mediaLibraryTypes[type ?? "none"]) == null ? void 0 : _a.title) ?? "bbm"), resetState = () => {
209
+ const modalHeader = t(((_c = mediaLibraryTypes[type ?? "none"]) == null ? void 0 : _c.title) ?? "bbm"), resetState = () => {
186
210
  linkTabProps.current = void 0, setResult(void 0), setResultCounter(void 0), setDefaultTabId(void 0), setPreSuccess(void 0), setDeletionsOnCancel([]);
187
211
  }, handleTabChange = (tab) => {
188
212
  onTabChange == null || onTabChange(tab, deletionsOnCancel), resetState();
189
213
  }, handleOnSuccess = useCallback(() => {
214
+ if (isSubmitting) return;
190
215
  const triggerSuccess = async (result2) => {
191
216
  result2 instanceof Array && ["protected", "public"].findIndex((v) => v === visibility) >= 0 && (result2 = await odeServices.workspace().transferDocuments(result2, appCode ?? "media-library", visibility)), onSuccess(result2);
192
217
  };
193
- onSuccessAction ? onSuccessAction().then((result2) => {
194
- triggerSuccess(result2);
195
- }) : result && triggerSuccess(result), resetState();
196
- }, [onSuccessAction, result, onSuccess, visibility, appCode]), handleOnCancel = () => {
218
+ (async () => {
219
+ setIsSubmitting(!0);
220
+ try {
221
+ if (onSuccessAction) {
222
+ const asyncResult = await onSuccessAction();
223
+ await triggerSuccess(asyncResult);
224
+ } else result && await triggerSuccess(result);
225
+ resetState();
226
+ } catch {
227
+ } finally {
228
+ setIsSubmitting(!1);
229
+ }
230
+ })();
231
+ }, [isSubmitting, onSuccessAction, result, onSuccess, visibility, appCode]), handleOnCancel = () => {
197
232
  onCancel(deletionsOnCancel), resetState();
198
233
  };
199
234
  return type && /* @__PURE__ */ jsx(MediaLibraryContext.Provider, { value: {
200
235
  appCode,
201
236
  visibility,
202
237
  multiple,
203
- pdfOnly,
204
238
  type,
205
239
  setResultCounter,
206
240
  setResult,
@@ -216,7 +250,7 @@ const orderedTabs = [
216
250
  ] }) }),
217
251
  /* @__PURE__ */ jsxs(Modal.Footer, { children: [
218
252
  /* @__PURE__ */ jsx(Button, { type: "button", color: "tertiary", variant: "ghost", onClick: handleOnCancel, children: t("cancel") }),
219
- /* @__PURE__ */ jsx(Button, { type: "button", color: "primary", variant: "filled", disabled: typeof result > "u", onClick: handleOnSuccess, children: resultCounter && resultCounter > 1 ? t("addMultiple", {
253
+ /* @__PURE__ */ jsx(Button, { type: "button", color: "primary", variant: "filled", disabled: typeof result > "u" || isSubmitting, onClick: handleOnSuccess, children: resultCounter && resultCounter > 1 ? t("addMultiple", {
220
254
  number: resultCounter
221
255
  }) : t("add") })
222
256
  ] })
@@ -7,8 +7,6 @@ export declare const MediaLibraryContext: import('react').Context<{
7
7
  visibility?: WorkspaceVisibility;
8
8
  /** Allow selecting / uploading multiple files at once ? */
9
9
  multiple?: boolean;
10
- /** Restrict attachments to PDF files in Workspace and Upload. */
11
- pdfOnly?: boolean;
12
10
  /** Type of resource to search for. */
13
11
  type: MediaLibraryType | null;
14
12
  /** Set the counter in the success button label. */
@@ -38,8 +36,6 @@ export declare function useMediaLibraryContext(): {
38
36
  visibility?: WorkspaceVisibility;
39
37
  /** Allow selecting / uploading multiple files at once ? */
40
38
  multiple?: boolean;
41
- /** Restrict attachments to PDF files in Workspace and Upload. */
42
- pdfOnly?: boolean;
43
39
  /** Type of resource to search for. */
44
40
  type: MediaLibraryType | null;
45
41
  /** Set the counter in the success button label. */
@@ -0,0 +1 @@
1
+ export declare const GoogleDrive: () => import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,61 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import { odeServices, GoogleDriveCopyError } from "@edifice.io/client";
3
+ import { useTranslation } from "react-i18next";
4
+ import { useMediaLibraryContext } from "../MediaLibraryContext.js";
5
+ import useUser from "../../../../hooks/useUser/useUser.js";
6
+ import useToast from "../../../../hooks/useToast/useToast.js";
7
+ import GoogleDrive$1 from "../../GoogleDrive/GoogleDrive.js";
8
+ const GoogleDrive = () => {
9
+ const {
10
+ appCode,
11
+ visibility,
12
+ type,
13
+ setResultCounter,
14
+ setResult,
15
+ setPreSuccess,
16
+ multiple
17
+ } = useMediaLibraryContext(), {
18
+ user
19
+ } = useUser(), {
20
+ t
21
+ } = useTranslation(), toast = useToast();
22
+ function getDocumentRoleFilter() {
23
+ switch (type) {
24
+ case "image":
25
+ return "img";
26
+ case "audio":
27
+ return "audio";
28
+ case "video":
29
+ return "video";
30
+ default:
31
+ return null;
32
+ }
33
+ }
34
+ async function copyToWorkspace(userId, ids) {
35
+ try {
36
+ return await odeServices.googledrive().copyDocumentToWorkspace(
37
+ userId,
38
+ ids,
39
+ void 0,
40
+ // Copy the documents straight to their final place, the way an upload
41
+ // does, instead of leaving a copy in the user's own documents for the
42
+ // modal to transfer afterwards.
43
+ {
44
+ application: appCode,
45
+ visibility
46
+ }
47
+ );
48
+ } catch (error) {
49
+ if (error instanceof GoogleDriveCopyError && error.copied.length > 0)
50
+ return toast.warning(t("google-drive.transfer.error")), error.copied;
51
+ throw error;
52
+ }
53
+ }
54
+ function handleSelect(result) {
55
+ setResultCounter(result.length), result.length ? (setResult(result), setPreSuccess(() => () => user != null && user.userId ? copyToWorkspace(user.userId, result.map((doc) => doc.id)) : Promise.resolve([]))) : (setResult(), setPreSuccess(void 0));
56
+ }
57
+ return /* @__PURE__ */ jsx(GoogleDrive$1, { roles: getDocumentRoleFilter(), onSelect: handleSelect, multiple, className: "border rounded overflow-y-auto" });
58
+ };
59
+ export {
60
+ GoogleDrive
61
+ };
@@ -0,0 +1 @@
1
+ export declare const Nextcloud: () => import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,48 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import { odeServices } from "@edifice.io/client";
3
+ import { useMediaLibraryContext } from "../MediaLibraryContext.js";
4
+ import useUser from "../../../../hooks/useUser/useUser.js";
5
+ import Nextcloud$1 from "../../Nextcloud/Nextcloud.js";
6
+ const Nextcloud = () => {
7
+ const {
8
+ appCode,
9
+ visibility,
10
+ type,
11
+ setResultCounter,
12
+ setResult,
13
+ setPreSuccess,
14
+ multiple
15
+ } = useMediaLibraryContext(), {
16
+ user
17
+ } = useUser();
18
+ function getDocumentRoleFilter() {
19
+ switch (type) {
20
+ case "image":
21
+ return "img";
22
+ case "audio":
23
+ return "audio";
24
+ case "video":
25
+ return "video";
26
+ default:
27
+ return null;
28
+ }
29
+ }
30
+ function handleSelect(result) {
31
+ setResultCounter(result.length), result.length ? (setResult(result), setPreSuccess(() => () => user != null && user.userId ? odeServices.nextcloud().copyDocumentToWorkspace(
32
+ user.userId,
33
+ result.map((doc) => doc.path),
34
+ void 0,
35
+ // Copy the documents straight to their final place, the way an
36
+ // upload does, instead of leaving a copy in the user's own
37
+ // documents for the modal to transfer afterwards.
38
+ {
39
+ application: appCode,
40
+ visibility
41
+ }
42
+ ) : Promise.resolve([]))) : (setResult(), setPreSuccess(void 0));
43
+ }
44
+ return /* @__PURE__ */ jsx(Nextcloud$1, { roles: getDocumentRoleFilter(), onSelect: handleSelect, multiple, className: "border rounded overflow-y-auto" });
45
+ };
46
+ export {
47
+ Nextcloud
48
+ };
@@ -36,7 +36,6 @@ const acceptedTypes = (type) => {
36
36
  t
37
37
  } = useTranslation(), {
38
38
  type,
39
- pdfOnly,
40
39
  visibility,
41
40
  multiple,
42
41
  setResult,
@@ -61,7 +60,7 @@ const acceptedTypes = (type) => {
61
60
  /* @__PURE__ */ jsx(Radio, { label: t("bbm.upload.sort.alpha"), name: "media-library-upload-sort", value: "alpha", model: sortByAlpha ? "alpha" : "date", checked: sortByAlpha, onChange: () => setSortByAlpha(!0), "data-testid": "media-library-upload-sort-alpha" }),
62
61
  /* @__PURE__ */ jsx(Radio, { label: t("bbm.upload.sort.date"), name: "media-library-upload-sort", value: "date", model: sortByAlpha ? "alpha" : "date", checked: !sortByAlpha, onChange: () => setSortByAlpha(!1), "data-testid": "media-library-upload-sort-date" })
63
62
  ] }),
64
- /* @__PURE__ */ jsx(Dropzone, { multiple, accept: type === "attachment" && pdfOnly ? ["application/pdf"] : acceptedTypes(type ?? "embedder"), children: /* @__PURE__ */ jsx(UploadFiles, { onFilesChange: handleOnFilesChange, visibility }) })
63
+ /* @__PURE__ */ jsx(Dropzone, { multiple, accept: acceptedTypes(type ?? "embedder"), children: /* @__PURE__ */ jsx(UploadFiles, { onFilesChange: handleOnFilesChange, visibility }) })
65
64
  ] });
66
65
  };
67
66
  export {
@@ -4,7 +4,6 @@ import Workspace$1 from "../../Workspace/Workspace.js";
4
4
  const Workspace = () => {
5
5
  const {
6
6
  type,
7
- pdfOnly,
8
7
  setResultCounter,
9
8
  setResult,
10
9
  multiple,
@@ -18,8 +17,6 @@ const Workspace = () => {
18
17
  return "audio";
19
18
  case "video":
20
19
  return "video";
21
- case "attachment":
22
- return pdfOnly ? "pdf" : null;
23
20
  default:
24
21
  return null;
25
22
  }
@@ -4,6 +4,8 @@ export declare const InnerTabs: {
4
4
  Upload: () => import("react/jsx-runtime").JSX.Element;
5
5
  ExternalLink: ({ link, multiNodeSelected, }: import('./ExternalLink').ExternalLinkTabProps) => import("react/jsx-runtime").JSX.Element;
6
6
  Workspace: () => import("react/jsx-runtime").JSX.Element;
7
+ Nextcloud: () => import("react/jsx-runtime").JSX.Element;
8
+ GoogleDrive: () => import("react/jsx-runtime").JSX.Element;
7
9
  InternalLink: ({ target, resourceId, appPrefix, }: import('./InternalLink').InternalLinkTabProps) => import("react/jsx-runtime").JSX.Element;
8
10
  Iframe: () => import("react/jsx-runtime").JSX.Element;
9
11
  VideoEmbedder: () => import("react/jsx-runtime").JSX.Element;