diff --git a/invenio_app_rdm/theme/assets/semantic-ui/js/invenio_app_rdm/landing_page/RecordManagement.js b/invenio_app_rdm/theme/assets/semantic-ui/js/invenio_app_rdm/landing_page/RecordManagement.js index ca8fae0dd2..aeb2d38b66 100644 --- a/invenio_app_rdm/theme/assets/semantic-ui/js/invenio_app_rdm/landing_page/RecordManagement.js +++ b/invenio_app_rdm/theme/assets/semantic-ui/js/invenio_app_rdm/landing_page/RecordManagement.js @@ -11,7 +11,7 @@ import React, { Component } from "react"; import { Button, Grid, Icon, Message } from "semantic-ui-react"; import { EditButton } from "./EditButton"; -import { ShareButton } from "./ShareButton"; +import { ShareButton } from "./ShareOptions/ShareButton"; import { NewVersionButton } from "@js/invenio_rdm_records/"; import PropTypes from "prop-types"; import Overridable from "react-overridable"; diff --git a/invenio_app_rdm/theme/assets/semantic-ui/js/invenio_app_rdm/landing_page/ShareModal.js b/invenio_app_rdm/theme/assets/semantic-ui/js/invenio_app_rdm/landing_page/ShareModal.js deleted file mode 100644 index c46aa3f3a3..0000000000 --- a/invenio_app_rdm/theme/assets/semantic-ui/js/invenio_app_rdm/landing_page/ShareModal.js +++ /dev/null @@ -1,262 +0,0 @@ -// This file is part of InvenioRDM -// Copyright (C) 2021 Northwestern University. -// -// Invenio RDM Records is free software; you can redistribute it and/or modify it -// under the terms of the MIT License; see LICENSE file for more details. - -import React, { useEffect, useState } from "react"; -import { Dropdown, Icon, Input, Button, Modal, Popup } from "semantic-ui-react"; -import axios from "axios"; -import { Trans } from "react-i18next"; -import { i18next } from "@translations/invenio_app_rdm/i18next"; -import { http } from "react-invenio-forms"; -import PropTypes from "prop-types"; - -export const ShareModal = ({ recid, open, handleClose }) => { - const [accessLinkObj, setAccessLinkObj] = useState(); - const [linkCreated, setLinkCreated] = useState(false); - const [shareMode, setShareMode] = useState("view"); - const [copied, setCopied] = useState(false); - - const dropdownOptions = [ - { key: "view", text: i18next.t("Can view"), value: "view" }, - { key: "preview", text: i18next.t("Can preview"), value: "preview" }, - { key: "edit", text: i18next.t("Can edit"), value: "edit" }, - ]; - - const message = { - view: ( - - - Anyone with this link can view all versions of this record & - files. - - - ), - preview: ( - - - Anyone with this link{" "} - can view all published and unpublished versions of this - record & files. - - - ), - edit: ( - - - Anyone with an account and this link can edit all versions of - this record & files. - - - ), - }; - - const copyButtonRef = React.createRef(); - - const getAccessLink = (linkObj) => { - const extraParam = shareMode === "preview" ? "preview=1&" : ""; - return linkObj ? `${window.location}?${extraParam}token=${linkObj.token}` : ""; - }; - - const createAccessLink = async () => { - await http - .post( - `/api/records/${recid}/access/links`, - { permission: shareMode }, - { - headers: { - "Accept": "application/json", - "Content-Type": "application/json", - }, - withCredentials: true, - } - ) - .then((resp) => { - setAccessLinkObj(resp.data); - setLinkCreated(true); - }); - }; - - const copyAccessLink = () => { - const copyText = document.querySelector("#input"); - copyText.select(); - let copySuccess = document.execCommand("copy"); - setCopied(copySuccess); - }; - - const handleChangeMode = (e, { value }) => setShareMode(value); - - const handleDelete = async () => { - await http - .delete(`/api/records/${recid}/access/links/${accessLinkObj.id}`, { - headers: { - Accept: "application/json", - }, - withCredentials: true, - }) - .then(() => { - setAccessLinkObj(); - setLinkCreated(false); - }); - }; - - useEffect(() => { - linkCreated && copyAccessLink(); - }, [linkCreated]); - - useEffect(() => { - // Update access link according to share mode - let source = axios.CancelToken.source(); - - if (accessLinkObj) { - (async () => { - await http.patch( - `/api/records/${recid}/access/links/${accessLinkObj.id}`, - { permission: shareMode }, - { - headers: { - "Content-Type": "application/json", - }, - withCredentials: true, - cancelToken: source.token, - } - ); - })(); - } - return () => { - source.cancel(); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [shareMode]); - - useEffect(() => { - if (accessLinkObj) { - setShareMode(accessLinkObj.permission); - } - }, [accessLinkObj]); - - useEffect(() => { - // Fetch existing access link - let source = axios.CancelToken.source(); - - (async () => { - const result = await axios(`/api/records/${recid}/access/links`, { - headers: { - Accept: "application/json", - }, - withCredentials: true, - cancelToken: source.token, - }); - const { hits, total } = result.data.hits; - if (total > 0) { - // Only accessing first access link for MVP. - setAccessLinkObj(hits[0]); - } - })(); - - return () => { - source.cancel(); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - useEffect(() => { - copyButtonRef.current?.focus(); // Accessiblity: focus the copy-button when modal opens - }, [copyButtonRef]); - - useEffect(() => { - let popupTimeout = setTimeout(() => { - setCopied(false); - }, 1500); - - return () => { - clearTimeout(popupTimeout); - }; - }, [copied]); - - return ( - - - - {i18next.t("Get a link")} - - - -
- - - - - {accessLinkObj ? i18next.t("Copy link") : i18next.t("Get a link")} - - } - /> - -
-

{copied && i18next.t("Copied")}

-
-
- -

- - {!accessLinkObj - ? i18next.t( - "No link has been created. Click on 'Get a Link' to make a new link." - ) - : message[shareMode]} -

-
-
- - - {!!accessLinkObj && ( - - )} - - -
- ); -}; - -ShareModal.propTypes = { - recid: PropTypes.string.isRequired, - open: PropTypes.bool.isRequired, - handleClose: PropTypes.func.isRequired, -}; diff --git a/invenio_app_rdm/theme/assets/semantic-ui/js/invenio_app_rdm/landing_page/ShareOptions/GrantsTab.js b/invenio_app_rdm/theme/assets/semantic-ui/js/invenio_app_rdm/landing_page/ShareOptions/GrantsTab.js new file mode 100644 index 0000000000..5bdad2d9fd --- /dev/null +++ b/invenio_app_rdm/theme/assets/semantic-ui/js/invenio_app_rdm/landing_page/ShareOptions/GrantsTab.js @@ -0,0 +1,101 @@ +// This file is part of InvenioRDM +// Copyright (C) 2021 Northwestern University. +// Copyright (C) 2023 TU Wien. +// +// Invenio RDM Records is free software; you can redistribute it and/or modify it +// under the terms of the MIT License; see LICENSE file for more details. + +import React, { useEffect, useState } from "react"; +import { Dropdown, Icon, List, Button, Modal, Tab } from "semantic-ui-react"; +import { i18next } from "@translations/invenio_app_rdm/i18next"; +import { http } from "react-invenio-forms"; +import PropTypes from "prop-types"; + +export const GrantsTab = ({ recid, dropdownOptions }) => { + const [accessGrants, setAccessGrants] = useState({}); + + useEffect(async () => { + try { + const result = await http.get(`/api/records/${recid}/access/grants?expand=1`); + const { hits } = result.data.hits; + setAccessGrants(hits); + } catch (err) { + console.log(err); + } + }, []); + + const grantIconDict = { + edit: "edit", + view: "eye", + preview: "file alternate", + }; + + const handleGrantModeChange = (id, permission) => { + http + .patch(`/api/records/${recid}/access/grants/${id}?expand=1`, { + permission: permission, + }) + .then((result) => { + let newAccessGrants = { ...accessGrants }; + delete newAccessGrants[id]; + newAccessGrants[result.data.id ?? id] = result.data; + setAccessGrants(newAccessGrants); + }); + }; + + const handleDeleteGrant = (id) => { + http.delete(`/api/records/${recid}/access/grants/${id}`).then((_result) => { + const newAccessGrants = [...accessGrants]; + delete newAccessGrants[id]; + setAccessGrants(newAccessGrants); + }); + }; + + return ( + <> +

Access shared with users and groups

+ + + {Object.keys(accessGrants).map((grantId) => ( + + + + {accessGrants[grantId].expanded.subject.profile?.full_name ?? + `Unknown user (#${accessGrants[grantId].expanded.subject.id})`}{" "} + {accessGrants[grantId].origin.startsWith("request:") + ? "(created via request)" + : "(created manually)"} + { + handleGrantModeChange(grantId, value); + }} + /> + + + + ))} + + + ); +}; + +GrantsTab.propTypes = { + recid: PropTypes.string.isRequired, + dropdownOptions: PropTypes.array.isRequired, +}; diff --git a/invenio_app_rdm/theme/assets/semantic-ui/js/invenio_app_rdm/landing_page/ShareOptions/LinksTab.js b/invenio_app_rdm/theme/assets/semantic-ui/js/invenio_app_rdm/landing_page/ShareOptions/LinksTab.js new file mode 100644 index 0000000000..26547ca66a --- /dev/null +++ b/invenio_app_rdm/theme/assets/semantic-ui/js/invenio_app_rdm/landing_page/ShareOptions/LinksTab.js @@ -0,0 +1,233 @@ +// This file is part of InvenioRDM +// Copyright (C) 2021 Northwestern University. +// +// Invenio RDM Records is free software; you can redistribute it and/or modify it +// under the terms of the MIT License; see LICENSE file for more details. + +import React, { useEffect, useState } from "react"; +import { Dropdown, Icon, Input, Button, Modal, Popup, Header } from "semantic-ui-react"; +import axios from "axios"; +import { Trans } from "react-i18next"; +import { i18next } from "@translations/invenio_app_rdm/i18next"; +import { http } from "react-invenio-forms"; +import PropTypes from "prop-types"; +import { resolveObjectURL } from "buffer"; + +export const LinksTab = ({ recid, dropdownOptions }) => { + const [accessLinks, setAccessLinks] = useState({}); + const [linkCreated, setLinkCreated] = useState(false); + const [shareMode, setShareMode] = useState("view"); + const [copied, setCopied] = useState(false); + + const message = { + view: ( + + + Anyone with this link can view all versions of this record & + files. + + + ), + preview: ( + + + Anyone with this link{" "} + can view all published and unpublished versions of this + record & files. + + + ), + edit: ( + + + Anyone with an account and this link can edit all versions of + this record & files. + + + ), + }; + + const copyButtonRef = React.createRef(); + + const getAccessLink = (linkId) => { + const linkObj = accessLinks[linkId]; + const extraParam = linkObj?.permission === "preview" ? "preview=1&" : ""; + return linkObj ? `${window.location}?${extraParam}token=${linkObj.token}` : ""; + }; + + const createAccessLink = async () => { + await http + .post( + `/api/records/${recid}/access/links`, + { permission: shareMode }, + { + headers: { + "Accept": "application/json", + "Content-Type": "application/json", + }, + withCredentials: true, + } + ) + .then((resp) => { + setAccessLinks({[resp.data.id]: resp.data}); + setLinkCreated(true); + }); + }; + + const copyAccessLink = (linkId) => { + const copyText = document.querySelector("#input"); + copyText.select(); + let copySuccess = document.execCommand("copy"); + setCopied(copySuccess); + }; + + const handleChangeMode = (linkId, value) => { + setShareMode(value); + } + + const handleDelete = (linkId) => { + http.delete(`/api/records/${recid}/access/links/${linkId}`) + .then(() => { + let newLinks = {...accessLinks}; + delete newLinks[linkId]; + setAccessLinks(newLinks); + setLinkCreated(false); + }); + }; + + useEffect(() => { + linkCreated && copyAccessLink(); + }, [linkCreated]); + + /* + useEffect(() => { + // Update access link according to share mode + let source = axios.CancelToken.source(); + + if (accessLinkObj) { + (async () => { + await http.patch( + `/api/records/${recid}/access/links/${accessLinkObj.id}`, + { permission: shareMode }, + { cancelToken: source.token } + ); + })(); + } + return () => { + source.cancel(); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [shareMode]); + */ + + /* + useEffect(() => { + if (accessLinkObj) { + setShareMode(accessLinkObj.permission); + } + }, [accessLinkObj]); + */ + + useEffect(() => { + // Fetch existing access link + let source = axios.CancelToken.source(); + + (async () => { + const result = await axios(`/api/records/${recid}/access/links`, { + cancelToken: source.token, + }); + + let newAccessLinks = {}; + result.data.hits.hits.forEach(hit => { + newAccessLinks[hit.id] = hit; + }); + setAccessLinks(newAccessLinks); + })(); + + return () => { + source.cancel(); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { + copyButtonRef.current?.focus(); // Accessiblity: focus the copy-button when modal opens + }, [copyButtonRef]); + + useEffect(() => { + let popupTimeout = setTimeout(() => { + setCopied(false); + }, 1500); + + return () => { + clearTimeout(popupTimeout); + }; + }, [copied]); + + const linkIds = Object.keys(accessLinks) + const firstLink = linkIds.length > 0 ? accessLinks[linkIds[0]] : null; + + return ( + <> + + +
+ + {handleChangeMode(firstLink?.id, value)}} + /> + copyAccessLink(firstLink?.id)} + aria-label={ + i18next.t("Copy link") + } + > + + {i18next.t("Copy link")} + + } + /> + + {linkIds.length > 0 && ( + + )} +
+ +

+ + {linkIds.length <= 0 + ? i18next.t( + "No link has been created. Click on 'Get a Link' to make a new link." + ) + : message[shareMode]} +

+ + ); +}; + +LinksTab.propTypes = { + recid: PropTypes.string.isRequired, + dropdownOptions: PropTypes.array.isRequired, +}; diff --git a/invenio_app_rdm/theme/assets/semantic-ui/js/invenio_app_rdm/landing_page/ShareButton.js b/invenio_app_rdm/theme/assets/semantic-ui/js/invenio_app_rdm/landing_page/ShareOptions/ShareButton.js similarity index 98% rename from invenio_app_rdm/theme/assets/semantic-ui/js/invenio_app_rdm/landing_page/ShareButton.js rename to invenio_app_rdm/theme/assets/semantic-ui/js/invenio_app_rdm/landing_page/ShareOptions/ShareButton.js index 10e6baf389..a3de5795f1 100644 --- a/invenio_app_rdm/theme/assets/semantic-ui/js/invenio_app_rdm/landing_page/ShareButton.js +++ b/invenio_app_rdm/theme/assets/semantic-ui/js/invenio_app_rdm/landing_page/ShareOptions/ShareButton.js @@ -2,6 +2,7 @@ // Copyright (C) 2021 CERN. // Copyright (C) 2021 Northwestern University. // Copyright (C) 2021 Graz University of Technology. +// Copyright (C) 2023 TU Wien. // // Invenio RDM Records is free software; you can redistribute it and/or modify it // under the terms of the MIT License; see LICENSE file for more details. diff --git a/invenio_app_rdm/theme/assets/semantic-ui/js/invenio_app_rdm/landing_page/ShareOptions/ShareModal.js b/invenio_app_rdm/theme/assets/semantic-ui/js/invenio_app_rdm/landing_page/ShareOptions/ShareModal.js new file mode 100644 index 0000000000..255b181512 --- /dev/null +++ b/invenio_app_rdm/theme/assets/semantic-ui/js/invenio_app_rdm/landing_page/ShareOptions/ShareModal.js @@ -0,0 +1,85 @@ +// This file is part of InvenioRDM +// Copyright (C) 2021 Northwestern University. +// Copyright (C) 2023 TU Wien. +// +// Invenio RDM Records is free software; you can redistribute it and/or modify it +// under the terms of the MIT License; see LICENSE file for more details. + +import React from "react"; +import { Icon, Button, Modal, Tab } from "semantic-ui-react"; +import { i18next } from "@translations/invenio_app_rdm/i18next"; +import PropTypes from "prop-types"; +import { LinksTab } from "./LinksTab"; +import { GrantsTab } from "./GrantsTab"; + +export const ShareModal = ({ recid, open, handleClose }) => { + const dropdownOptions = [ + { key: "view", text: i18next.t("Can view"), value: "view" }, + { key: "preview", text: i18next.t("Can preview"), value: "preview" }, + { key: "edit", text: i18next.t("Can edit"), value: "edit" }, + ]; + + const panes = [ + { + menuItem: { icon: "users", content: "People and groups" }, + pane: ( + + + + ), + }, + { + menuItem: { icon: "linkify", content: "Links" }, + pane: ( + + + + ), + }, + { + menuItem: { icon: "cogs", content: "Access requests" }, + pane: ( + +

Coming soon

+
+ ), + }, + ]; + + return ( + + + + {i18next.t("Share access")} + + + + + + + + + + + ); +}; + +ShareModal.propTypes = { + recid: PropTypes.string.isRequired, + open: PropTypes.bool.isRequired, + handleClose: PropTypes.func.isRequired, +};