Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
214 changes: 214 additions & 0 deletions src/app/Files/_components/Toolbar/ShareModal/ShareModal.tsx
Original file line number Diff line number Diff line change
@@ -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<ToolbarModalProps> = ({
toggle,
systemId = '',
path = '/',
}) => {
const { selectedFiles, unselect } = useFilesSelect();
const { create, isLoading, isError, error, isSuccess, data, reset } =
Hooks.PostIts.useCreate();

const [allowedUses, setAllowedUses] = useState<number>(DEFAULT_ALLOWED_USES);
const [validSeconds, setValidSeconds] = useState<number>(
DEFAULT_VALID_SECONDS
);
const [redeemUrl, setRedeemUrl] = useState<string>('');
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 (
<GenericModal
size="lg"
toggle={onClose}
title={'Share via PostIt'}
body={
<div>
{isError && error && (
<Alert severity="error" sx={{ mb: 2 }}>
<AlertTitle>Error creating link</AlertTitle>
{error.message}
</Alert>
)}
{isMultipleSelection && (
<Alert severity="warning" sx={{ mb: 2 }}>
<AlertTitle>Multiple files selected</AlertTitle>
You have selected {selectedFiles.length} items. To share multiple
files, select them one at a time or create a folder containing all
files.
</Alert>
)}
<p>{description}</p>
<Stack direction="row" spacing={2} sx={{ mb: 2 }}>
<TextField
label="Allowed Uses"
type="number"
size="small"
value={allowedUses}
onChange={(e) =>
setAllowedUses(Math.max(1, Number(e.target.value)))
}
inputProps={{ min: 1 }}
/>
<TextField
label="Valid Seconds"
type="number"
size="small"
value={validSeconds}
onChange={(e) =>
setValidSeconds(Math.max(1, Number(e.target.value)))
}
inputProps={{ min: 1 }}
/>
</Stack>
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
<SubmitWrapper
isLoading={isLoading}
error={null}
success={redeemUrl ? 'Link created' : undefined}
>
<Button
color="primary"
onClick={onGenerate}
disabled={!selected || isMultipleSelection}
>
Generate Link
</Button>
</SubmitWrapper>
<div style={{ flex: 1 }}>
<div className="input-group">
<div className="input-group-prepend">
<Button
color={
copyStatus === 'success'
? 'success'
: copyStatus === 'error'
? 'danger'
: 'secondary'
}
onClick={handleCopyToClipboard}
disabled={!redeemUrl}
type="button"
style={{
minWidth: '80px',
transition: 'all 0.2s ease',
}}
>
{copyStatus === 'success'
? '✓ Copied!'
: copyStatus === 'error'
? '✗ Failed'
: 'Copy'}
</Button>
</div>
<input
type="text"
value={redeemUrl}
className="form-control"
placeholder="Shareable link will appear here"
readOnly
style={{
backgroundColor: '#f8f9fa',
cursor: 'text',
}}
/>
</div>
</div>
</div>
{isSuccess && redeemUrl && (
<Alert severity="success" sx={{ mt: 2 }}>
Link created. You can share this URL with users without
authentication.
</Alert>
)}
</div>
}
/>
);
};

export default ShareModal;
1 change: 1 addition & 0 deletions src/app/Files/_components/Toolbar/ShareModal/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './ShareModal';
18 changes: 18 additions & 0 deletions src/app/Files/_components/Toolbar/Toolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -57,6 +58,7 @@ export const ToolbarButton: React.FC<ToolbarButtonProps> = ({

type Op =
| 'view'
| 'share'
| 'download'
| 'upload'
| 'copy'
Expand Down Expand Up @@ -172,6 +174,15 @@ const Toolbar: React.FC<ToolbarProps> = ({
<div id="file-operation-toolbar">
{pathname !== '/files' && (
<div className={styles['toolbar-wrapper']}>
{show('share', buttons) && (
<ToolbarButton
text="Share"
icon="link"
disabled={selectedFiles.length !== 1}
onClick={() => setModal('share')}
aria-label="Share link"
/>
)}
{show('view', buttons) && (
<ToolbarButton
text="View"
Expand Down Expand Up @@ -338,6 +349,13 @@ const Toolbar: React.FC<ToolbarProps> = ({
path={currentPath}
/>
)}
{modal === 'share' && (
<ShareModal
toggle={toggle}
systemId={systemId}
path={currentPath}
/>
)}
</div>
)}
</div>
Expand Down
Loading