mirror of
https://github.com/lrnselfreliance/wrolpi.git
synced 2026-10-26 19:01:39 -04:00
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:
parent
87a6a34d68
commit
308784962d
5 changed files with 184 additions and 267 deletions
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 <>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue