typescript
This commit is contained in:
@@ -0,0 +1,222 @@
|
||||
import { useMemo, useState, useEffect } from 'react';
|
||||
import type { JSX } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
CloseIcon,
|
||||
LoaderIcon,
|
||||
CheckIcon,
|
||||
InfoIcon,
|
||||
WarningIcon,
|
||||
BottombarCollapseIcon,
|
||||
BottombarExpandIcon,
|
||||
ClearAllIcon,
|
||||
} from '../ui/icons';
|
||||
import PanelHeader from '../ui/PanelHeader';
|
||||
|
||||
type UploadStatus = 'pending' | 'uploading' | 'success' | 'duplicate' | 'error' | (string & {});
|
||||
|
||||
interface UploadQueueItem {
|
||||
id: string | number;
|
||||
name: string;
|
||||
status: UploadStatus;
|
||||
error?: string | null;
|
||||
document?: { id?: string | number; title?: string };
|
||||
conflictDocumentId?: string | number;
|
||||
}
|
||||
|
||||
interface UploadQueueOverlayProps {
|
||||
queue?: UploadQueueItem[];
|
||||
onClearQueue?: () => void;
|
||||
}
|
||||
|
||||
const STATUS_META: Record<string, { label: string; tone: string; icon: JSX.Element }> = {
|
||||
pending: {
|
||||
label: 'Queued',
|
||||
tone: 'muted',
|
||||
icon: <LoaderIcon className="icon icon--spin" size={16} />,
|
||||
},
|
||||
uploading: {
|
||||
label: 'Uploading',
|
||||
tone: 'accent',
|
||||
icon: <LoaderIcon className="icon icon--spin" size={16} />,
|
||||
},
|
||||
success: {
|
||||
label: 'Uploaded',
|
||||
tone: 'success',
|
||||
icon: <CheckIcon size={16} />,
|
||||
},
|
||||
duplicate: {
|
||||
label: 'Duplicate',
|
||||
tone: 'info',
|
||||
icon: <InfoIcon size={16} />,
|
||||
},
|
||||
error: {
|
||||
label: 'Failed',
|
||||
tone: 'danger',
|
||||
icon: <WarningIcon size={16} />,
|
||||
},
|
||||
};
|
||||
|
||||
const UploadQueueOverlay = ({ queue = [], onClearQueue }: UploadQueueOverlayProps): JSX.Element | null => {
|
||||
const navigate = useNavigate();
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
const [dismissed, setDismissed] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (queue.length > 0) {
|
||||
setDismissed(false);
|
||||
}
|
||||
}, [queue.length]);
|
||||
|
||||
const summary = useMemo(() => {
|
||||
if (!queue.length) {
|
||||
return 'No uploads';
|
||||
}
|
||||
const uploadingCount = queue.filter((item) => item.status === 'uploading').length;
|
||||
const pendingCount = queue.filter((item) => item.status === 'pending').length;
|
||||
const errorCount = queue.filter((item) => item.status === 'error').length;
|
||||
if (uploadingCount > 0 || pendingCount > 0) {
|
||||
return `${uploadingCount} uploading · ${pendingCount} queued`;
|
||||
}
|
||||
if (errorCount > 0) {
|
||||
return `${errorCount} failed · ${queue.length} total`;
|
||||
}
|
||||
return `${queue.length} completed`;
|
||||
}, [queue]);
|
||||
|
||||
const hasActiveUploads = queue.some((item) => item.status === 'uploading' || item.status === 'pending');
|
||||
|
||||
const handleClearQueue = () => {
|
||||
if (!onClearQueue || hasActiveUploads) {
|
||||
return;
|
||||
}
|
||||
onClearQueue();
|
||||
};
|
||||
|
||||
if (!queue.length || dismissed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`upload-queue-overlay${collapsed ? ' upload-queue-overlay--collapsed' : ''}`}>
|
||||
<PanelHeader
|
||||
className="upload-queue-overlay__header"
|
||||
title={(
|
||||
<span className="upload-queue-overlay__title">
|
||||
<span>Uploads</span>
|
||||
<span className="upload-queue-overlay__summary">{summary}</span>
|
||||
</span>
|
||||
)}
|
||||
actions={(
|
||||
<div className="upload-queue-overlay__controls">
|
||||
<button
|
||||
type="button"
|
||||
className="icon-button ghost"
|
||||
onClick={() => setCollapsed((value) => !value)}
|
||||
aria-label={collapsed ? 'Expand upload queue' : 'Collapse upload queue'}
|
||||
>
|
||||
{collapsed ? <BottombarExpandIcon size={16} /> : <BottombarCollapseIcon size={16} />}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="icon-button ghost"
|
||||
onClick={handleClearQueue}
|
||||
disabled={hasActiveUploads || !queue.length}
|
||||
aria-label="Clear completed uploads"
|
||||
>
|
||||
<ClearAllIcon size={16} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="icon-button"
|
||||
onClick={() => setDismissed(true)}
|
||||
aria-label="Hide upload queue"
|
||||
>
|
||||
<CloseIcon size={16} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
{!collapsed ? (
|
||||
<ul className="upload-queue-overlay__list">
|
||||
{[...queue]
|
||||
.slice()
|
||||
.reverse()
|
||||
.map((item) => {
|
||||
const meta = STATUS_META[item.status] || STATUS_META.pending;
|
||||
const fileLabel = item.name;
|
||||
const duplicateLabel = item.status === 'duplicate' ? item.document?.title || null : null;
|
||||
const documentId = item.document?.id || item.conflictDocumentId || null;
|
||||
const hasLink = Boolean(documentId);
|
||||
const handleNavigate = () => {
|
||||
if (!documentId) {
|
||||
return;
|
||||
}
|
||||
navigate(`/documents/${documentId}`);
|
||||
};
|
||||
return (
|
||||
<li key={item.id} className={`upload-queue-overlay__item upload-queue-overlay__item--${item.status}`}>
|
||||
<span className={`upload-queue-overlay__status upload-queue-overlay__status--${meta.tone}`}>
|
||||
{meta.icon}
|
||||
</span>
|
||||
<div className="upload-queue-overlay__details">
|
||||
{item.status === 'success' && hasLink ? (
|
||||
<button
|
||||
type="button"
|
||||
className="upload-queue-overlay__name-link"
|
||||
onClick={handleNavigate}
|
||||
>
|
||||
{fileLabel}
|
||||
</button>
|
||||
) : (
|
||||
<div className="upload-queue-overlay__name">
|
||||
{fileLabel}
|
||||
</div>
|
||||
)}
|
||||
<div className="upload-queue-overlay__meta-line">
|
||||
{item.status === 'duplicate' && duplicateLabel ? (
|
||||
<span className="upload-queue-overlay__meta-duplicate">
|
||||
Duplicate of{' '}
|
||||
<button
|
||||
type="button"
|
||||
className="upload-queue-overlay__meta-link"
|
||||
onClick={handleNavigate}
|
||||
>
|
||||
{duplicateLabel}
|
||||
</button>
|
||||
</span>
|
||||
) : item.status === 'error' && item.error ? (
|
||||
<span className="upload-queue-overlay__meta-error" title={item.error}>
|
||||
{item.error}
|
||||
</span>
|
||||
) : (
|
||||
<>
|
||||
<span>{meta.label}</span>
|
||||
{documentId ? (
|
||||
<span className="upload-queue-overlay__meta-id">
|
||||
(
|
||||
<button
|
||||
type="button"
|
||||
className="upload-queue-overlay__meta-link"
|
||||
onClick={handleNavigate}
|
||||
disabled={!hasLink}
|
||||
>
|
||||
{documentId}
|
||||
</button>
|
||||
)
|
||||
</span>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UploadQueueOverlay;
|
||||
Reference in New Issue
Block a user