import { jsx, jsxs } from 'react/jsx-runtime'; import 'react'; import { Flex, IconButton, TextButton, Typography } from '@strapi/design-system'; import { ChevronDown, ArrowsCounterClockwise, CrossCircle, MinusCircle, CheckCircle, Cross, Check, Upload } from '@strapi/icons'; import { useIntl } from 'react-intl'; import { styled } from 'styled-components'; import { useRetryCancelledFilesMutation, abortUpload } from '../services/api.mjs'; import { useTypedDispatch, useTypedSelector } from '../store/hooks.mjs'; import { closeUploadProgress, selectAggregateProgress, cancelUpload, toggleMinimize } from '../store/uploadProgress.mjs'; import { getTranslationKey } from '../utils/translations.mjs'; import { Drawer } from './Drawer.mjs'; /* ------------------------------------------------------------------------------------------------- * DialogHeader * -----------------------------------------------------------------------------------------------*/ const HeaderStatusMessage = ({ title, subtitle })=>{ return /*#__PURE__*/ jsxs(Flex, { direction: "column", alignItems: "flex-start", paddingLeft: 2, children: [ /*#__PURE__*/ jsx(Drawer.Title, { children: /*#__PURE__*/ jsx(Typography, { variant: "omega", children: title }) }), /*#__PURE__*/ jsx(Drawer.Description, { children: /*#__PURE__*/ jsx(Typography, { variant: "pi", textColor: "neutral600", children: subtitle }) }) ] }); }; const HeaderStatusIcon = styled(Flex)` padding: ${({ theme })=>theme.spaces[3]}; border-radius: ${({ theme })=>`${theme.borderRadius} 0 0 ${theme.borderRadius}`}; > svg { height: 24px; width: 24px; } `; const HeaderStatusWrapper = styled(Flex)` align-items: center; `; const HeaderStatus = ({ status, progress, totalFiles, successfulCount, errorCount })=>{ const { formatMessage } = useIntl(); if (status === 'error') { return /*#__PURE__*/ jsxs(HeaderStatusWrapper, { children: [ /*#__PURE__*/ jsx(HeaderStatusIcon, { background: "danger200", children: /*#__PURE__*/ jsx(Cross, { fill: "danger700" }) }), /*#__PURE__*/ jsx(HeaderStatusMessage, { title: formatMessage({ id: getTranslationKey('upload.progress.failed'), defaultMessage: 'Upload failed' }), subtitle: formatMessage({ id: getTranslationKey('upload.progress.failed.subtitle'), defaultMessage: 'Please try to upload files again' }) }) ] }); } if (status === 'success') { const subtitle = errorCount > 0 ? formatMessage({ id: getTranslationKey('upload.progress.success.subtitle.withErrors'), defaultMessage: '{successCount} uploaded, {errorCount} failed' }, { successCount: successfulCount, errorCount }) : formatMessage({ id: getTranslationKey('upload.progress.success.subtitle'), defaultMessage: '{count} files uploaded successfully' }, { count: successfulCount }); return /*#__PURE__*/ jsxs(HeaderStatusWrapper, { children: [ /*#__PURE__*/ jsx(HeaderStatusIcon, { background: "success200", children: /*#__PURE__*/ jsx(Check, { fill: "success700" }) }), /*#__PURE__*/ jsx(HeaderStatusMessage, { title: formatMessage({ id: getTranslationKey('upload.progress.success'), defaultMessage: 'Upload successful!' }), subtitle: subtitle }) ] }); } if (status === 'canceled') { return /*#__PURE__*/ jsxs(HeaderStatusWrapper, { children: [ /*#__PURE__*/ jsx(HeaderStatusIcon, { background: "neutral200", children: /*#__PURE__*/ jsx(MinusCircle, { fill: "neutral700" }) }), /*#__PURE__*/ jsx(HeaderStatusMessage, { title: formatMessage({ id: getTranslationKey('upload.progress.canceled'), defaultMessage: 'Upload canceled' }), subtitle: formatMessage({ id: getTranslationKey('upload.progress.canceled.subtitle'), defaultMessage: 'Some files were not uploaded' }) }) ] }); } if (status === 'uploading') { const progressPercentage = progress ? Math.round(progress) : 0; return /*#__PURE__*/ jsxs(HeaderStatusWrapper, { children: [ /*#__PURE__*/ jsx(HeaderStatusIcon, { background: "primary200", children: /*#__PURE__*/ jsx(Upload, { fill: "primary700" }) }), /*#__PURE__*/ jsx(HeaderStatusMessage, { title: formatMessage({ id: getTranslationKey('upload.progress.uploading.withCount'), defaultMessage: 'Uploading {total} items ({percentage}%)' }, { total: totalFiles, percentage: progressPercentage }) }) ] }); } return null; }; const HeaderIconButton = styled(IconButton)` &:hover { background: transparent; } `; const ChevronWrapper = styled.span` display: flex; transition: transform 0.5s ease-in-out; transform: ${({ $isMinimized })=>$isMinimized ? 'rotate(180deg)' : 'rotate(0deg)'}; `; const HEADER_COLOR_MAP = { uploading: { background: 'primary100' }, canceled: { background: 'neutral100' }, success: { background: 'success100' }, error: { background: 'danger100' } }; const DialogHeader = ({ handleClose })=>{ const { formatMessage } = useIntl(); const { isMinimized, files, uploadId, totalFiles } = useTypedSelector((state)=>state.uploadProgress); const progress = useTypedSelector(selectAggregateProgress); const dispatch = useTypedDispatch(); const [retryCancelledFiles] = useRetryCancelledFilesMutation(); // The batch is complete once every file has reached a terminal state. Byte-weighted // progress can't be used here because errored/cancelled rows never reach 100%. const isComplete = files.length > 0 && files.every((f)=>f.status === 'complete' || f.status === 'error' || f.status === 'cancelled'); const isAllUploaded = isComplete; const isAllErrored = isComplete && files.every((f)=>f.status === 'error'); const hasCancelledFiles = files.some((f)=>f.status === 'cancelled'); const successfulCount = files.filter((f)=>f.status === 'complete').length; const errorCount = files.filter((f)=>f.status === 'error').length; // Success includes partial success (some files succeeded, even if some failed) const isSuccess = isComplete && successfulCount > 0 && !hasCancelledFiles; const status = (()=>{ if (isAllErrored) return 'error'; if (isSuccess) return 'success'; if (hasCancelledFiles) return 'canceled'; return 'uploading'; })(); const handleCancel = ()=>{ abortUpload(uploadId); dispatch(cancelUpload()); }; const handleRetry = async ()=>{ try { await retryCancelledFiles().unwrap(); } catch { // Error is already dispatched to store from the API queryFn } }; const handleToggleMinimize = ()=>{ dispatch(toggleMinimize()); }; return /*#__PURE__*/ jsxs(Flex, { background: HEADER_COLOR_MAP[status].background, justifyContent: "space-between", margin: 1, hasRadius: true, children: [ /*#__PURE__*/ jsx(HeaderStatus, { status: status, progress: progress, totalFiles: totalFiles, successfulCount: successfulCount, errorCount: errorCount }), /*#__PURE__*/ jsxs(Flex, { gap: 1, children: [ !isAllUploaded && /*#__PURE__*/ jsx(TextButton, { onClick: handleCancel, fontWeight: "bold", children: formatMessage({ id: getTranslationKey('upload.progress.cancel'), defaultMessage: 'Cancel' }) }), hasCancelledFiles && /*#__PURE__*/ jsx(TextButton, { onClick: handleRetry, fontWeight: "bold", children: formatMessage({ id: getTranslationKey('upload.progress.retry'), defaultMessage: 'Retry' }) }), /*#__PURE__*/ jsx(HeaderIconButton, { onClick: handleToggleMinimize, label: formatMessage({ id: getTranslationKey(isMinimized ? 'upload.progress.maximize' : 'upload.progress.minimize'), defaultMessage: isMinimized ? 'Maximize' : 'Minimize' }), variant: "ghost", children: /*#__PURE__*/ jsx(ChevronWrapper, { $isMinimized: isMinimized, children: /*#__PURE__*/ jsx(ChevronDown, {}) }) }), isComplete && /*#__PURE__*/ jsx(Drawer.CloseButton, { onClose: handleClose, label: formatMessage({ id: getTranslationKey('upload.progress.close'), defaultMessage: 'Close' }) }) ] }) ] }); }; /* ------------------------------------------------------------------------------------------------- * UploadProgressDialog * -----------------------------------------------------------------------------------------------*/ const ProgressTrack = styled.div` width: 100%; height: ${({ theme })=>theme.spaces[1]}; background-color: ${({ theme })=>theme.colors.neutral200}; border-radius: 4px; overflow: hidden; `; const ProgressIndicator = styled.div` height: 100%; width: ${({ $percent })=>$percent}%; background-color: ${({ theme })=>theme.colors.primary700}; border-radius: 4px; transition: width 0.15s linear; `; const DeterminateBar = ({ percent })=>{ const clamped = Math.min(100, Math.max(0, Math.round(percent))); return /*#__PURE__*/ jsx(ProgressTrack, { role: "progressbar", "aria-valuemin": 0, "aria-valuemax": 100, "aria-valuenow": clamped, children: /*#__PURE__*/ jsx(ProgressIndicator, { $percent: clamped }) }); }; const FileRow = ({ icon, fileName, children })=>{ return /*#__PURE__*/ jsxs(Flex, { direction: "column", alignItems: "stretch", justifyContent: "center", gap: 1, width: "100%", children: [ /*#__PURE__*/ jsxs(Flex, { gap: 2, children: [ icon, /*#__PURE__*/ jsx(Typography, { variant: "omega", fontWeight: "semiBold", ellipsis: true, children: fileName }) ] }), children ] }); }; const FileRowRenderer = ({ file })=>{ const { formatMessage } = useIntl(); const isError = file.status === 'error'; const isCurrentFile = file.status === 'uploading'; const isCompleted = file.status === 'complete'; const isCancelled = file.status === 'cancelled'; if (isCurrentFile) { const percent = file.size > 0 ? file.uploadedBytes / file.size * 100 : 0; return /*#__PURE__*/ jsxs(FileRow, { icon: /*#__PURE__*/ jsx(ArrowsCounterClockwise, { fill: "secondary600" }), fileName: file.name, children: [ /*#__PURE__*/ jsx(Typography, { variant: "pi", textColor: "neutral600", children: formatMessage({ id: getTranslationKey('upload.progress.file.uploading'), defaultMessage: 'Uploading...' }) }), /*#__PURE__*/ jsx(DeterminateBar, { percent: percent }) ] }); } if (isError) { return /*#__PURE__*/ jsx(FileRow, { icon: /*#__PURE__*/ jsx(CrossCircle, { fill: "danger500" }), fileName: file.name, children: /*#__PURE__*/ jsx(Typography, { variant: "pi", textColor: "neutral600", children: file.error }) }); } if (isCancelled) { return /*#__PURE__*/ jsx(FileRow, { icon: /*#__PURE__*/ jsx(MinusCircle, { fill: "neutral600" }), fileName: file.name, children: /*#__PURE__*/ jsx(Typography, { variant: "pi", textColor: "neutral600", children: formatMessage({ id: getTranslationKey('upload.progress.file.canceled'), defaultMessage: 'Canceled' }) }) }); } if (isCompleted) { return /*#__PURE__*/ jsx(FileRow, { icon: /*#__PURE__*/ jsx(CheckCircle, { fill: "success500" }), fileName: file.name, children: /*#__PURE__*/ jsx(Typography, { variant: "pi", textColor: "neutral600", children: formatMessage({ id: getTranslationKey('upload.progress.file.uploaded'), defaultMessage: 'Uploaded' }) }) }); } return null; }; const CompletedFilesList = styled(Flex)` flex-direction: column; gap: ${({ theme })=>theme.spaces[2]}; width: 100%; `; const UploadProgressDialog = ()=>{ const dispatch = useTypedDispatch(); const { isVisible, isMinimized, files } = useTypedSelector((state)=>state.uploadProgress); const currentFile = files.find((f)=>f.status === 'uploading'); const completedFiles = files.filter((f)=>f.status === 'complete' || f.status === 'error' || f.status === 'cancelled').sort((a, b)=>{ // Sort priority: error > cancelled > complete const priority = { error: 0, cancelled: 1, complete: 2, uploading: 3, pending: 4 }; return priority[a.status] - priority[b.status]; }); const handleClose = ()=>{ dispatch(closeUploadProgress()); }; return /*#__PURE__*/ jsx(Drawer.Root, { isVisible: isVisible, onClose: handleClose, children: /*#__PURE__*/ jsxs(Drawer.Body, { animationDirection: "up", width: "41.6rem", maxHeight: "34.2rem", children: [ /*#__PURE__*/ jsx(DialogHeader, { handleClose: handleClose }), /*#__PURE__*/ jsx(Drawer.ScrollableContent, { isContentExpanded: !isMinimized, children: /*#__PURE__*/ jsxs(Flex, { direction: "column", alignItems: "stretch", gap: 4, paddingTop: 4, paddingBottom: 4, paddingLeft: 4, paddingRight: 4, children: [ currentFile && /*#__PURE__*/ jsx(FileRowRenderer, { file: currentFile }), completedFiles.length > 0 && /*#__PURE__*/ jsx(CompletedFilesList, { children: completedFiles.map((file)=>/*#__PURE__*/ jsx(FileRowRenderer, { file: file }, file.index)) }) ] }) }) ] }) }); }; export { UploadProgressDialog }; //# sourceMappingURL=UploadProgressDialog.mjs.map