diff --git a/src/app/Files/_components/Toolbar/ShareModal/ShareModal.tsx b/src/app/Files/_components/Toolbar/ShareModal/ShareModal.tsx new file mode 100644 index 00000000..74c8c7b0 --- /dev/null +++ b/src/app/Files/_components/Toolbar/ShareModal/ShareModal.tsx @@ -0,0 +1,214 @@ +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { Alert, AlertTitle, TextField, Stack } from '@mui/material'; +import { Button } from 'reactstrap'; +import { GenericModal, SubmitWrapper } from '@tapis/tapisui-common'; +import { ToolbarModalProps } from '../Toolbar'; +import { Files as Hooks } from '@tapis/tapisui-hooks'; +import { useFilesSelect } from '../../FilesContext'; + +const DEFAULT_ALLOWED_USES = 1; +const DEFAULT_VALID_SECONDS = 3600; // 1 hour + +const ShareModal: React.FC = ({ + toggle, + systemId = '', + path = '/', +}) => { + const { selectedFiles, unselect } = useFilesSelect(); + const { create, isLoading, isError, error, isSuccess, data, reset } = + Hooks.PostIts.useCreate(); + + const [allowedUses, setAllowedUses] = useState(DEFAULT_ALLOWED_USES); + const [validSeconds, setValidSeconds] = useState( + DEFAULT_VALID_SECONDS + ); + const [redeemUrl, setRedeemUrl] = useState(''); + const [copyStatus, setCopyStatus] = useState<'idle' | 'success' | 'error'>( + 'idle' + ); + + const selected = selectedFiles[0]; + const isDir = selected?.type === 'dir'; + const isMultipleSelection = selectedFiles.length > 1; + + const description = useMemo(() => { + if (!selected) return ''; + const kind = isDir ? 'folder' : 'file'; + return `You are creating a shareable link for the ${kind} "${selected.name}".`; + }, [selected, isDir]); + + useEffect(() => { + if (data?.result?.redeemUrl) { + // Add download parameter to force download instead of display + const url = new URL(data.result.redeemUrl); + url.searchParams.set('download', 'true'); + setRedeemUrl(url.toString()); + } + }, [data]); + + const onGenerate = useCallback(() => { + if (!selected) return; + setRedeemUrl(''); + create( + { + systemId, + path: selected.path!, + createPostItRequest: { + allowedUses, + validSeconds, + }, + }, + { + onSuccess: (resp) => { + if (resp.result?.redeemUrl) { + // Add download parameter to force download instead of display + const url = new URL(resp.result.redeemUrl); + url.searchParams.set('download', 'true'); + setRedeemUrl(url.toString()); + } else { + setRedeemUrl(''); + } + }, + } + ); + }, [create, systemId, selected, allowedUses, validSeconds]); + + const onClose = useCallback(() => { + reset(); + setRedeemUrl(''); + setCopyStatus('idle'); + unselect(selectedFiles); + toggle(); + }, [reset, toggle, unselect, selectedFiles]); + + const handleCopyToClipboard = useCallback(async () => { + if (!redeemUrl) { + setCopyStatus('error'); + return; + } + + try { + await navigator.clipboard.writeText(redeemUrl); + setCopyStatus('success'); + } catch (error) { + console.error('Error copying to clipboard:', error); + setCopyStatus('error'); + } + + // Reset status after 2 seconds + setTimeout(() => { + setCopyStatus('idle'); + }, 2000); + }, [redeemUrl]); + + return ( + + {isError && error && ( + + Error creating link + {error.message} + + )} + {isMultipleSelection && ( + + Multiple files selected + You have selected {selectedFiles.length} items. To share multiple + files, select them one at a time or create a folder containing all + files. + + )} +

{description}

+ + + setAllowedUses(Math.max(1, Number(e.target.value))) + } + inputProps={{ min: 1 }} + /> + + setValidSeconds(Math.max(1, Number(e.target.value))) + } + inputProps={{ min: 1 }} + /> + +
+ + + +
+
+
+ +
+ +
+
+
+ {isSuccess && redeemUrl && ( + + Link created. You can share this URL with users without + authentication. + + )} + + } + /> + ); +}; + +export default ShareModal; diff --git a/src/app/Files/_components/Toolbar/ShareModal/index.ts b/src/app/Files/_components/Toolbar/ShareModal/index.ts new file mode 100644 index 00000000..cb8e5248 --- /dev/null +++ b/src/app/Files/_components/Toolbar/ShareModal/index.ts @@ -0,0 +1 @@ +export { default } from './ShareModal'; diff --git a/src/app/Files/_components/Toolbar/Toolbar.tsx b/src/app/Files/_components/Toolbar/Toolbar.tsx index dc453a72..ed231823 100644 --- a/src/app/Files/_components/Toolbar/Toolbar.tsx +++ b/src/app/Files/_components/Toolbar/Toolbar.tsx @@ -11,6 +11,7 @@ import PermissionsModal from './PermissionsModal'; import DeleteModal from './DeleteModal'; import CreatePostitModal from './CreatePostitModal'; import TransferModal from './TransferModal'; +import ShareModal from './ShareModal'; import { useLocation } from 'react-router-dom'; import { useFilesSelect } from '../FilesContext'; import { @@ -57,6 +58,7 @@ export const ToolbarButton: React.FC = ({ type Op = | 'view' + | 'share' | 'download' | 'upload' | 'copy' @@ -172,6 +174,15 @@ const Toolbar: React.FC = ({
{pathname !== '/files' && (
+ {show('share', buttons) && ( + setModal('share')} + aria-label="Share link" + /> + )} {show('view', buttons) && ( = ({ path={currentPath} /> )} + {modal === 'share' && ( + + )}
)}