ui: migrate Downloads, Upload, and BulkTagModal off Semantic

Ports admin/Downloads.js, Upload.js, and BulkTagModal.js to the token-driven
ui library: Table/Modal/Progress/Message/Button/IconButton replace their
Semantic and Theme.tsx equivalents. Failed download rows take the dashed
danger border via Table.Row's `failed` prop; retry/restart actions use
role='retry' and delete/stop actions use role='danger' instead of hardcoded
colors. RecurringDownloadsTable keeps its export name and props for
Archive.js. Updates BulkTagModal.test.js to render through the real
Mantine-backed components instead of mocking the retired ./Theme module, and
drops the three files from semantic-baseline.json now that they no longer
import Semantic UI.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Roland 2026-07-30 13:10:51 -06:00
parent 87a6a34d68
commit 308784962d
No known key found for this signature in database
GPG key ID: 1BA620CE6C9A4680
5 changed files with 184 additions and 267 deletions

View file

@ -1,9 +1,7 @@
import React from 'react';
import {Button, Divider, Header, Loader, Modal, Progress} from "./Theme";
import Message from "semantic-ui-react/dist/commonjs/collections/Message";
import {Button, Divider, Header, Loading, Message, Modal, Progress} from "./ui";
import {applyBulkTags, getBulkTagPreview, getBulkTagProgress} from "../api";
import {TagsContext} from "../Tags";
import {ThemeContext} from "../contexts/contexts";
const BULK_TAG_WARNING_THRESHOLD = 50;
const POLL_INTERVAL = 500; // Poll progress every 500ms
@ -18,7 +16,6 @@ export function BulkTagModal({open, onClose, paths, onComplete}) {
const [error, setError] = React.useState(null);
const {tagNames, TagsGroup} = React.useContext(TagsContext);
const {t} = React.useContext(ThemeContext);
// Fetch preview when modal opens
React.useEffect(() => {
@ -132,16 +129,13 @@ export function BulkTagModal({open, onClose, paths, onComplete}) {
<Modal.Header>Bulk Tag Files</Modal.Header>
<Modal.Content>
{state === 'loading' && (
<div style={{textAlign: 'center', padding: '2em'}}>
<Loader active inline="centered">Loading preview...</Loader>
</div>
<Loading>Loading preview...</Loading>
)}
{state === 'preview' && (
<>
{error && (
<Message negative>
<Message.Header>Error</Message.Header>
<Message kind='error' title='Error'>
<p>{error}</p>
</Message>
)}
@ -151,8 +145,7 @@ export function BulkTagModal({open, onClose, paths, onComplete}) {
</Header>
{showWarning && (
<Message warning>
<Message.Header>Warning</Message.Header>
<Message kind='warning' title='Warning'>
<p>Tagging over {BULK_TAG_WARNING_THRESHOLD} files may make it difficult to find what you are looking for. Are you sure?</p>
</Message>
)}
@ -163,7 +156,7 @@ export function BulkTagModal({open, onClose, paths, onComplete}) {
{localTags.length > 0 ? (
<TagsGroup tagNames={localTags} onClick={removeTag}/>
) : (
<p {...t}>Add one or more tags below</p>
<p>Add one or more tags below</p>
)}
<Divider/>
@ -171,7 +164,7 @@ export function BulkTagModal({open, onClose, paths, onComplete}) {
{unusedTags.length > 0 ? (
<TagsGroup tagNames={unusedTags} onClick={addTag}/>
) : (
<p {...t}>You have no tags</p>
<p>You have no tags</p>
)}
</>
)}
@ -181,18 +174,14 @@ export function BulkTagModal({open, onClose, paths, onComplete}) {
<Header as="h4">Applying tags...</Header>
<Progress
percent={progressPercent}
progress
indicating
color="blue"
>
<span {...t}>{progress.completed} / {progress.total} files</span>
</Progress>
label={`${progress.completed} / ${progress.total} files`}
/>
{progress.queued_jobs > 0 && (
<p {...t}>{progress.queued_jobs} job{progress.queued_jobs !== 1 ? 's' : ''} queued</p>
<p>{progress.queued_jobs} job{progress.queued_jobs !== 1 ? 's' : ''} queued</p>
)}
{error && (
<Message negative>
<Message.Header>Error</Message.Header>
<Message kind='error' title='Error'>
<p>{error}</p>
</Message>
)}
@ -201,13 +190,11 @@ export function BulkTagModal({open, onClose, paths, onComplete}) {
{state === 'complete' && (
<div style={{padding: '1em'}}>
<Message positive>
<Message.Header>Complete</Message.Header>
<Message kind='success' title='Complete'>
<p>Successfully tagged {progress.total} file{progress.total !== 1 ? 's' : ''}.</p>
</Message>
{error && (
<Message warning>
<Message.Header>Warning</Message.Header>
<Message kind='warning' title='Warning'>
<p>Some errors occurred: {error}</p>
</Message>
)}
@ -217,7 +204,7 @@ export function BulkTagModal({open, onClose, paths, onComplete}) {
<Modal.Actions>
{state === 'preview' && (
<>
<Button onClick={handleClose}>Cancel</Button>
<Button role='cancel' onClick={handleClose}>Cancel</Button>
<Button
color="violet"
onClick={handleApply}
@ -228,12 +215,12 @@ export function BulkTagModal({open, onClose, paths, onComplete}) {
</>
)}
{state === 'applying' && (
<Button onClick={handleClose} disabled>
<Button role='cancel' onClick={handleClose} disabled>
Please wait...
</Button>
)}
{state === 'complete' && (
<Button color="green" onClick={handleClose}>
<Button role='save' onClick={handleClose}>
Done
</Button>
)}

View file

@ -1,7 +1,8 @@
import React from 'react';
import {act, render, screen, waitFor} from '@testing-library/react';
import {act, screen, waitFor} from '@testing-library/react';
import {BulkTagModal} from './BulkTagModal';
import {TagsContext} from '../Tags';
import {renderWithProviders} from '../test-utils';
// Mock the API functions
jest.mock('../api', () => ({
@ -10,42 +11,6 @@ jest.mock('../api', () => ({
getBulkTagProgress: jest.fn(),
}));
// Mock Theme components with compound patterns
jest.mock('./Theme', () => {
const MockModal = ({open, onClose, children, closeIcon}) => {
if (!open) return null;
return (
<div data-testid="modal">
{closeIcon && <button data-testid="close-icon" onClick={onClose}>x</button>}
{children}
</div>
);
};
MockModal.Header = ({children}) => <div data-testid="modal-header">{children}</div>;
MockModal.Content = ({children}) => <div data-testid="modal-content">{children}</div>;
MockModal.Actions = ({children}) => <div data-testid="modal-actions">{children}</div>;
MockModal.Description = ({children}) => <div data-testid="modal-description">{children}</div>;
return {
...jest.requireActual('./Theme'),
Modal: MockModal,
Button: ({children, onClick, disabled, color, ...props}) => (
<button onClick={onClick} disabled={disabled} data-testid={`button-${children}`} {...props}>{children}</button>
),
Header: ({children, as}) => <div data-testid="header">{children}</div>,
Divider: () => <hr data-testid="divider"/>,
Loader: ({children}) => <div data-testid="loader">{children}</div>,
Message: ({children, warning, negative, positive}) => (
<div data-testid={`message-${warning ? 'warning' : negative ? 'negative' : positive ? 'positive' : 'info'}`}>
{children}
</div>
),
Progress: ({percent, children}) => (
<div data-testid="progress" data-percent={percent}>{children}</div>
),
};
});
// Import mocked API functions
import {getBulkTagPreview, applyBulkTags, getBulkTagProgress} from '../api';
@ -60,23 +25,18 @@ const MockTagsGroup = ({tagNames, onClick}) => (
</div>
);
// Create a wrapper with TagsContext
const TagsContextWrapper = ({children}) => {
// Render with the real ui library components (Mantine-backed) plus TagsContext.
const renderWithTags = (ui, options) => {
const tagsValue = {
tagNames: ['tag1', 'tag2', 'tag3'],
TagsGroup: MockTagsGroup,
};
return (
<TagsContext.Provider value={tagsValue}>
{children}
</TagsContext.Provider>
return renderWithProviders(
<TagsContext.Provider value={tagsValue}>{ui}</TagsContext.Provider>,
options,
);
};
// Custom render function with TagsContext
const renderWithTags = (ui, options) =>
render(ui, {wrapper: TagsContextWrapper, ...options});
describe('BulkTagModal', () => {
const defaultProps = {
open: true,
@ -102,17 +62,17 @@ describe('BulkTagModal', () => {
describe('Modal Rendering', () => {
it('renders modal when open is true', async () => {
renderWithTags(<BulkTagModal {...defaultProps} />);
expect(screen.getByTestId('modal')).toBeInTheDocument();
expect(screen.getByRole('dialog')).toBeInTheDocument();
});
it('does not render modal when open is false', () => {
renderWithTags(<BulkTagModal {...defaultProps} open={false}/>);
expect(screen.queryByTestId('modal')).not.toBeInTheDocument();
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
});
it('shows header', async () => {
renderWithTags(<BulkTagModal {...defaultProps} />);
expect(screen.getByTestId('modal-header')).toHaveTextContent('Bulk Tag Files');
expect(screen.getByText('Bulk Tag Files')).toBeInTheDocument();
});
});
@ -121,7 +81,7 @@ describe('BulkTagModal', () => {
getBulkTagPreview.mockImplementation(() => new Promise(() => {
})); // Never resolves
renderWithTags(<BulkTagModal {...defaultProps} />);
expect(screen.getByTestId('loader')).toBeInTheDocument();
expect(screen.getByText('Loading preview...')).toBeInTheDocument();
});
});
@ -142,7 +102,7 @@ describe('BulkTagModal', () => {
await waitFor(() => {
expect(screen.getByText(/10 files will be affected/i)).toBeInTheDocument();
});
expect(screen.queryByTestId('message-warning')).not.toBeInTheDocument();
expect(screen.queryByText(/Tagging over 50 files/i)).not.toBeInTheDocument();
});
it('Apply button is disabled when no changes', async () => {
@ -150,7 +110,7 @@ describe('BulkTagModal', () => {
await waitFor(() => {
expect(screen.getByText(/2 files will be affected/i)).toBeInTheDocument();
});
const applyButton = screen.getByTestId('button-Apply Tags');
const applyButton = screen.getByRole('button', {name: 'Apply Tags'});
expect(applyButton).toBeDisabled();
});
});
@ -183,7 +143,7 @@ describe('BulkTagModal', () => {
await waitFor(() => {
expect(screen.getByText(/2 files will be affected/i)).toBeInTheDocument();
});
const cancelButton = screen.getByTestId('button-Cancel');
const cancelButton = screen.getByRole('button', {name: 'Cancel'});
act(() => {
cancelButton.click();
});
@ -197,7 +157,7 @@ describe('BulkTagModal', () => {
getBulkTagPreview.mockResolvedValue(undefined);
renderWithTags(<BulkTagModal {...defaultProps} />);
// Modal should still be visible
expect(screen.getByTestId('modal')).toBeInTheDocument();
expect(screen.getByRole('dialog')).toBeInTheDocument();
});
});
});

View file

@ -1,16 +1,11 @@
import React from "react";
import {useDropzone} from "react-dropzone";
import {Form, Header, Icon, Progress, Segment} from "./Theme";
import {DirectorySearch, mimetypeIconName, Toggle} from "./Common";
import {Box, Group, Header, Icon, Panel, Progress, Stack} from "./ui";
import {DirectorySearch, mimetypeIconName, RequiredAsterisk, Toggle} from "./Common";
import {useUploadFile} from "../hooks/customHooks";
import _ from "lodash";
import {ThemeContext} from "../contexts/contexts";
import Grid from "semantic-ui-react/dist/commonjs/collections/Grid";
import {Form as SForm} from "semantic-ui-react";
export function Upload({disabled}) {
const {t} = React.useContext(ThemeContext);
const {
setFiles,
progresses,
@ -57,33 +52,29 @@ export function Upload({disabled}) {
color = 'orange';
statusString = 'Already Exists:';
}
// Only indicating when upload is pending.
let indicating = status === 'pending';
return <Grid.Row key={name}>
<Grid.Column mobile={3} tablet={2} computer={1}>
<Icon name={mimetypeIconName(type, name.toLowerCase())} size='big'/>
</Grid.Column>
<Grid.Column mobile={13} tablet={14} computer={15}>
<Progress progress indicating={indicating} percent={percent} color={color}>
<p {...t}>{statusString} {name}</p>
</Progress>
</Grid.Column>
</Grid.Row>
return <Group key={name} align='center' wrap='nowrap' gap='sm'>
<Icon name={mimetypeIconName(type, name.toLowerCase())} size='large'/>
<Box style={{flex: 1}}>
<Progress percent={percent} color={color} label={`${statusString} ${name}`}/>
</Box>
</Group>
})
}
return <>
<Form>
<SForm.Field required>
<label>Destination</label>
<Stack gap='sm'>
<div>
<label style={{display: 'block', marginBottom: 4}}>
<b>Destination</b> <RequiredAsterisk/>
</label>
<DirectorySearch
onSelect={i => setDestination(i)}
disabled={disabled || inProgress}
style={{marginBottom: '0.5em'}}
/>
{tagsSelector}
</SForm.Field>
</div>
<Toggle
checked={overwrite}
@ -91,50 +82,30 @@ export function Upload({disabled}) {
onChange={() => setOverwrite(!overwrite)}
disabled={disabled || inProgress}
/>
</Form>
</Stack>
{destination ?
<Segment>
<Grid columns={1}>
<Grid.Row>
<Grid.Column style={{padding: '1em'}}>
<Form>
<div {...getRootProps()}>
<input {...getInputProps()}/>
<Grid textAlign='center'>
<Grid.Row>
<Grid.Column>
<Header icon>
<Icon name='file text'/>
Click here, or drop files here to upload
</Header>
</Grid.Column>
</Grid.Row>
</Grid>
</div>
</Form>
</Grid.Column>
</Grid.Row>
</Grid>
</Segment>
<Panel style={{marginTop: '1em'}}>
<div {...getRootProps()} style={{padding: '1em', textAlign: 'center', cursor: 'pointer'}}>
<input {...getInputProps()}/>
<Header as='h3' icon='file text'>
Click here, or drop files here to upload
</Header>
</div>
</Panel>
: <Header as='h3'>You must search for a directory to place your files</Header>
}
<br/>
<Grid>
<Grid.Row columns={1}>
<Grid.Column>
{progresses && Object.keys(progresses).length > 1 && <Progress progress
percent={overallProgress}
color='blue'
label='Overall Progress'
/>}
</Grid.Column>
</Grid.Row>
</Grid>
<Grid columns={2}>
{progresses && Object.keys(progresses).length > 1 && <Progress
percent={overallProgress}
color='blue'
label='Overall Progress'
/>}
<Stack gap='sm' mt='sm'>
{progressBars}
</Grid>
</Stack>
</>
}

View file

@ -27,20 +27,19 @@ import {
WROLModeMessage
} from "../Common";
import {
Button as SButton,
Button,
ButtonGroup,
Checkbox,
Header,
IconButton,
Label,
Loader,
PlaceholderLine,
TableBody,
TableCell,
TableFooter,
TableHeader,
TableHeaderCell,
TableRow
} from "semantic-ui-react";
import {Button, Header, Modal, Placeholder, Progress, Segment, Table} from "../Theme";
Modal,
Panel,
Placeholder,
Progress,
Table,
} from "../ui";
import {useDownloads} from "../../hooks/customHooks";
import {
EditArchiveDownloadForm,
@ -70,35 +69,32 @@ function DownloadProgressModal({progress, url}) {
<Modal.Content>
<Progress
percent={progress.percent}
progress
indicating
color={navColor}
>
{humanFileSize(progress.bytes_downloaded)} / {humanFileSize(progress.total_bytes)}
</Progress>
<Table definition>
<TableBody>
<TableRow>
<TableCell width={4}>Speed</TableCell>
<TableCell>{speedText}</TableCell>
</TableRow>
<TableRow>
<TableCell>ETA</TableCell>
<TableCell>{progress.eta || 'Calculating...'}</TableCell>
</TableRow>
<TableRow>
<TableCell>Downloaded</TableCell>
<TableCell>{humanFileSize(progress.bytes_downloaded)} / {humanFileSize(progress.total_bytes)}</TableCell>
</TableRow>
{url && <TableRow>
<TableCell>URL</TableCell>
<TableCell style={{wordBreak: 'break-all'}}>{url}</TableCell>
</TableRow>}
</TableBody>
label={`${humanFileSize(progress.bytes_downloaded)} / ${humanFileSize(progress.total_bytes)}`}
/>
<Table>
<Table.Body>
<Table.Row>
<Table.Cell style={{fontWeight: 600}}>Speed</Table.Cell>
<Table.Cell>{speedText}</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell style={{fontWeight: 600}}>ETA</Table.Cell>
<Table.Cell>{progress.eta || 'Calculating...'}</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell style={{fontWeight: 600}}>Downloaded</Table.Cell>
<Table.Cell>{humanFileSize(progress.bytes_downloaded)} / {humanFileSize(progress.total_bytes)}</Table.Cell>
</Table.Row>
{url && <Table.Row>
<Table.Cell style={{fontWeight: 600}}>URL</Table.Cell>
<Table.Cell style={{wordBreak: 'break-all'}}>{url}</Table.Cell>
</Table.Row>}
</Table.Body>
</Table>
</Modal.Content>
<Modal.Actions>
<Button onClick={() => setOpen(false)}>Close</Button>
<Button role='cancel' onClick={() => setOpen(false)}>Close</Button>
</Modal.Actions>
</Modal>
</>;
@ -154,7 +150,7 @@ function RetryDownloadsButton({callback, selectedIds, clearSelection}) {
const label = hasSelection ? `Retry (${selectedIds.length})` : 'Retry';
return <APIButton
color='green'
role='retry'
onClick={localRetryOnce}
obeyWROLMode={true}
>{label}</APIButton>
@ -184,7 +180,7 @@ function DeleteOnceDownloadsButton({callback, selectedIds, clearSelection}) {
: 'Are you sure you want to delete all downloads? Some may be retried!';
return <APIButton
color='red'
role='danger'
onClick={localDeleteOnce}
confirmContent={confirmContent}
confirmButton='Delete'
@ -231,24 +227,26 @@ function RecurringDownloadRow({download, fetchDownloads, onDelete}) {
const errorModal = <Modal
closeIcon
onClose={handleErrorClose}
onOpen={handleErrorOpen}
open={errorModalOpen}
trigger={<Button icon='exclamation circle' color='orange'/>}
>
<Modal.Header>Download Error</Modal.Header>
<Modal.Content>
<pre style={{overflowX: 'scroll'}}>{error}</pre>
</Modal.Content>
<Modal.Actions>
<SButton onClick={handleEditClose}>Close</SButton>
<Button role='cancel' onClick={handleErrorClose}>Close</Button>
</Modal.Actions>
</Modal>;
const errorTrigger = <IconButton icon='exclamation triangle' label='Download error' color='orange'
onClick={handleErrorOpen}/>;
const hideEdit = downloader === Downloaders.MapCatalog || downloader === Downloaders.MapExtract;
const editButton = hideEdit ? null : <Button icon='edit' onClick={handleEditOpen}/>;
const editButton = hideEdit ? null :
<IconButton icon='edit' label='Edit download' onClick={handleEditOpen}/>;
const restartButton = <APIButton
color='green'
role='retry'
icon='redo'
confirmContent='Are you sure you want to restart this download?'
confirmButton='Restart'
@ -320,24 +318,25 @@ function RecurringDownloadRow({download, fetchDownloads, onDelete}) {
</Modal.Content>
</Modal>;
return <TableRow>
<TableCell className='column-ellipsis'>
return <Table.Row failed={!!error && !download.progress}>
<Table.Cell className='column-ellipsis'>
{link(url)}
</TableCell>
<TableCell>{formatFrequency(frequency)}</TableCell>
<TableCell>
</Table.Cell>
<Table.Cell>{formatFrequency(frequency)}</Table.Cell>
<Table.Cell>
{last_successful_download && isoDatetimeToElapsedPopup(last_successful_download)}
{status === 'pending' && !download.progress && <Loader active inline size='tiny'/>}
{status === 'pending' && !download.progress && <Loader size='xs'/>}
{download.progress && <DownloadProgressModal progress={download.progress} url={download.url}/>}
</TableCell>
<TableCell>{next}</TableCell>
<TableCell textAlign='right'>
{error && !download.progress && errorModal}
</Table.Cell>
<Table.Cell>{next}</Table.Cell>
<Table.Cell style={{textAlign: 'right'}}>
{error && !download.progress && errorTrigger}
{editButton}
{restartButton}
</TableCell>
</Table.Cell>
{errorModal}
{editModal}
</TableRow>
</Table.Row>
}
function OnceDownloadRow({download, fetchDownloads, isSelected, onSelect}) {
@ -384,35 +383,36 @@ function OnceDownloadRow({download, fetchDownloads, isSelected, onSelect}) {
(text) => <a href={location || url} target='_blank' rel='noopener noreferrer'>{text}</a>;
let completedAtCell = last_successful_download ? isoDatetimeToElapsedPopup(last_successful_download) : null;
let buttonCell = <TableCell/>;
let buttonCell = <Table.Cell/>;
if (status === 'pending' || status === 'new') {
buttonCell = (
<TableCell>
<Table.Cell>
<ButtonGroup>
<APIButton
color='red'
icon='stop circle'
role='danger'
icon='stop'
onClick={handleStop}
confirmContent='Are you sure you want to stop this download? It will not be retried.'
confirmButton='Stop'
obeyWROLMode={true}
/>
{status === 'new' && (downloader === Downloaders.Video || downloader === Downloaders.Archive) && (
<Button
<IconButton
icon='edit'
label='Edit download'
color='blue'
onClick={handleEditOpen}
/>
)}
</ButtonGroup>
</TableCell>
</Table.Cell>
);
} else if (status === 'failed' || status === 'deferred') {
buttonCell = (
<TableCell>
<Table.Cell>
<ButtonGroup>
<APIButton
color='red'
role='danger'
icon='trash'
onClick={handleDelete}
confirmContent='Are you sure you want to delete this download?'
@ -420,14 +420,15 @@ function OnceDownloadRow({download, fetchDownloads, isSelected, onSelect}) {
obeyWROLMode={true}
/>
{(downloader === Downloaders.Video || downloader === Downloaders.Archive) && status !== 'pending' && (
<Button
<IconButton
icon='edit'
label='Edit download'
color='blue'
onClick={handleEditOpen}
/>
)}
<APIButton
color='green'
role='retry'
icon='redo'
confirmContent='Are you sure you want to restart this download?'
confirmButton='Start'
@ -435,31 +436,37 @@ function OnceDownloadRow({download, fetchDownloads, isSelected, onSelect}) {
obeyWROLMode={true}
/>
</ButtonGroup>
</TableCell>
</Table.Cell>
);
} else if (status === 'complete' && location) {
buttonCell = (
<TableCell>
<Table.Cell>
{link('View')}
</TableCell>
</Table.Cell>
);
}
if (error && !download.progress) {
completedAtCell = (
<Modal
closeIcon
trigger={<Button icon='exclamation circle' color='red'/>}
>
<Modal.Header>Download Error</Modal.Header>
<Modal.Content>
<pre style={{overflowX: 'scroll'}}>{error}</pre>
</Modal.Content>
</Modal>
)
}
// Create edit modal for video or archive downloads
let editModal = null;
let errorModal = null;
const [errorModalOpen, setErrorModalOpen] = React.useState(false);
if (error && !download.progress) {
completedAtCell = (
<IconButton icon='exclamation triangle' label='Download error' color='red'
onClick={() => setErrorModalOpen(true)}/>
)
errorModal = <Modal
closeIcon
open={errorModalOpen}
onClose={() => setErrorModalOpen(false)}
>
<Modal.Header>Download Error</Modal.Header>
<Modal.Content>
<pre style={{overflowX: 'scroll'}}>{error}</pre>
</Modal.Content>
</Modal>
}
if (downloader === Downloaders.Video) {
editModal = (
@ -482,7 +489,7 @@ function OnceDownloadRow({download, fetchDownloads, isSelected, onSelect}) {
onDelete={handleDelete}
/>
{parentDownloadUrl && (
<p style={{marginTop: '1em', color: '#666'}}>
<p style={{marginTop: '1em', color: 'var(--muted)'}}>
From: <a href={parentDownloadUrl} target='_blank' rel='noopener noreferrer'>
{parentDownloadUrl}
</a>
@ -511,7 +518,7 @@ function OnceDownloadRow({download, fetchDownloads, isSelected, onSelect}) {
onDelete={handleDelete}
/>
{parentDownloadUrl && (
<p style={{marginTop: '1em', color: '#666'}}>
<p style={{marginTop: '1em', color: 'var(--muted)'}}>
From: <a href={parentDownloadUrl} target='_blank' rel='noopener noreferrer'>
{parentDownloadUrl}
</a>
@ -522,24 +529,25 @@ function OnceDownloadRow({download, fetchDownloads, isSelected, onSelect}) {
);
}
return <TableRow>
<TableCell collapsing>
return <Table.Row failed={status === 'failed' || status === 'deferred'}>
<Table.Cell>
<Checkbox
checked={isSelected}
onChange={() => onSelect(id)}
/>
</TableCell>
<TableCell className='column-ellipsis'>
</Table.Cell>
<Table.Cell className='column-ellipsis'>
{link(url)}
</TableCell>
<TableCell>
</Table.Cell>
<Table.Cell>
{completedAtCell}
{status === 'pending' && !download.progress ? <Loader active inline size='tiny'/> : null}
{status === 'pending' && !download.progress ? <Loader size='xs'/> : null}
{download.progress && <DownloadProgressModal progress={download.progress} url={download.url}/>}
</TableCell>
</Table.Cell>
{buttonCell}
{errorModal}
{editModal}
</TableRow>
</Table.Row>
}
export function OnceDownloadsTable({downloads, fetchDownloads}) {
@ -597,9 +605,9 @@ export function OnceDownloadsTable({downloads, fetchDownloads}) {
/>
);
const footer = <TableFooter>
<TableRow>
<TableHeaderCell colSpan={4}>
const footer = <Table.Footer>
<Table.Row>
<Table.HeaderCell colSpan={4}>
<ClearDownloadsButton
callback={fetchDownloads}
selectedIds={selectedIds}
@ -615,9 +623,9 @@ export function OnceDownloadsTable({downloads, fetchDownloads}) {
selectedIds={selectedIds}
clearSelection={clearSelection}
/>
</TableHeaderCell>
</TableRow>
</TableFooter>;
</Table.HeaderCell>
</Table.Row>
</Table.Footer>;
if (downloads && downloads.length >= 1) {
return <SortableTable
@ -629,29 +637,26 @@ export function OnceDownloadsTable({downloads, fetchDownloads}) {
tableProps={{className: 'table-ellipsis'}}
/>
} else if (downloads) {
return <Segment>No downloads are scheduled.</Segment>
return <Panel>No downloads are scheduled.</Panel>
} else if (downloads === undefined) {
return <ErrorMessage>Unable to fetch downloads</ErrorMessage>
}
return <Placeholder>
<PlaceholderLine/>
<PlaceholderLine/>
</Placeholder>
return <Placeholder lines={2}/>
}
export function RecurringDownloadsTable({downloads, fetchDownloads, onDelete}) {
if (downloads && downloads.length >= 1) {
return <Table compact className='table-ellipsis'>
<TableHeader>
<TableRow>
<TableHeaderCell width={8}>URL</TableHeaderCell>
<TableHeaderCell width={2}>Download Frequency</TableHeaderCell>
<TableHeaderCell width={2}>Completed At</TableHeaderCell>
<TableHeaderCell width={1}>Next</TableHeaderCell>
<TableHeaderCell width={3} textAlign='right'>Control</TableHeaderCell>
</TableRow>
</TableHeader>
<TableBody>
return <Table className='table-ellipsis'>
<Table.Header>
<Table.Row>
<Table.HeaderCell style={{width: '50%'}}>URL</Table.HeaderCell>
<Table.HeaderCell style={{width: '12.5%'}}>Download Frequency</Table.HeaderCell>
<Table.HeaderCell style={{width: '12.5%'}}>Completed At</Table.HeaderCell>
<Table.HeaderCell style={{width: '6.25%'}}>Next</Table.HeaderCell>
<Table.HeaderCell style={{width: '18.75%', textAlign: 'right'}}>Control</Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
{downloads.map(i => {
return <RecurringDownloadRow
key={i.id}
@ -660,17 +665,14 @@ export function RecurringDownloadsTable({downloads, fetchDownloads, onDelete}) {
onDelete={onDelete}
/>
})}
</TableBody>
</Table.Body>
</Table>
} else if (downloads) {
return <Segment>No downloads are scheduled.</Segment>
return <Panel>No downloads are scheduled.</Panel>
} else if (downloads === undefined) {
return <ErrorMessage>Unable to fetch downloads</ErrorMessage>
}
return <Placeholder>
<PlaceholderLine/>
<PlaceholderLine/>
</Placeholder>
return <Placeholder lines={2}/>
}
export function DownloadsPage() {
@ -679,7 +681,7 @@ export function DownloadsPage() {
const {onceDownloads, recurringDownloads, pendingOnceDownloads, fetchDownloads} = useDownloads();
const pendingOnceDownloadsSpan = pendingOnceDownloads > 0 ?
<Label color='violet' size='large'>{pendingOnceDownloads}</Label>
<Label color='violet'>{pendingOnceDownloads}</Label>
: null;
return <>

View file

@ -1,6 +1,5 @@
[
"src/components/Archive.js",
"src/components/BulkTagModal.js",
"src/components/Channels.js",
"src/components/Docs.js",
"src/components/FileBrowser.js",
@ -9,9 +8,7 @@
"src/components/Nav.js",
"src/components/Playlists.js",
"src/components/Theme.tsx",
"src/components/Upload.js",
"src/components/VideoPlayer.js",
"src/components/Zim.js",
"src/components/admin/ControllerPage.js",
"src/components/admin/Downloads.js"
"src/components/admin/ControllerPage.js"
]