From 308784962d91d910d72edccbb16df2bd0d31e681 Mon Sep 17 00:00:00 2001 From: Roland Date: Thu, 30 Jul 2026 13:10:51 -0600 Subject: [PATCH] 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) --- app/src/components/BulkTagModal.js | 43 ++--- app/src/components/BulkTagModal.test.js | 70 ++----- app/src/components/Upload.js | 91 +++------ app/src/components/admin/Downloads.js | 242 ++++++++++++------------ app/src/semantic-baseline.json | 5 +- 5 files changed, 184 insertions(+), 267 deletions(-) diff --git a/app/src/components/BulkTagModal.js b/app/src/components/BulkTagModal.js index 8e12f51a..b3228569 100644 --- a/app/src/components/BulkTagModal.js +++ b/app/src/components/BulkTagModal.js @@ -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}) { Bulk Tag Files {state === 'loading' && ( -
- Loading preview... -
+ Loading preview... )} {state === 'preview' && ( <> {error && ( - - Error +

{error}

)} @@ -151,8 +145,7 @@ export function BulkTagModal({open, onClose, paths, onComplete}) { {showWarning && ( - - Warning +

Tagging over {BULK_TAG_WARNING_THRESHOLD} files may make it difficult to find what you are looking for. Are you sure?

)} @@ -163,7 +156,7 @@ export function BulkTagModal({open, onClose, paths, onComplete}) { {localTags.length > 0 ? ( ) : ( -

Add one or more tags below

+

Add one or more tags below

)} @@ -171,7 +164,7 @@ export function BulkTagModal({open, onClose, paths, onComplete}) { {unusedTags.length > 0 ? ( ) : ( -

You have no tags

+

You have no tags

)} )} @@ -181,18 +174,14 @@ export function BulkTagModal({open, onClose, paths, onComplete}) {
Applying tags...
- {progress.completed} / {progress.total} files - + label={`${progress.completed} / ${progress.total} files`} + /> {progress.queued_jobs > 0 && ( -

{progress.queued_jobs} job{progress.queued_jobs !== 1 ? 's' : ''} queued

+

{progress.queued_jobs} job{progress.queued_jobs !== 1 ? 's' : ''} queued

)} {error && ( - - Error +

{error}

)} @@ -201,13 +190,11 @@ export function BulkTagModal({open, onClose, paths, onComplete}) { {state === 'complete' && (
- - Complete +

Successfully tagged {progress.total} file{progress.total !== 1 ? 's' : ''}.

{error && ( - - Warning +

Some errors occurred: {error}

)} @@ -217,7 +204,7 @@ export function BulkTagModal({open, onClose, paths, onComplete}) { {state === 'preview' && ( <> - + )} {state === 'complete' && ( - )} diff --git a/app/src/components/BulkTagModal.test.js b/app/src/components/BulkTagModal.test.js index 55309499..ee2ee5b8 100644 --- a/app/src/components/BulkTagModal.test.js +++ b/app/src/components/BulkTagModal.test.js @@ -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 ( -
- {closeIcon && } - {children} -
- ); - }; - MockModal.Header = ({children}) =>
{children}
; - MockModal.Content = ({children}) =>
{children}
; - MockModal.Actions = ({children}) =>
{children}
; - MockModal.Description = ({children}) =>
{children}
; - - return { - ...jest.requireActual('./Theme'), - Modal: MockModal, - Button: ({children, onClick, disabled, color, ...props}) => ( - - ), - Header: ({children, as}) =>
{children}
, - Divider: () =>
, - Loader: ({children}) =>
{children}
, - Message: ({children, warning, negative, positive}) => ( -
- {children} -
- ), - Progress: ({percent, children}) => ( -
{children}
- ), - }; -}); - // Import mocked API functions import {getBulkTagPreview, applyBulkTags, getBulkTagProgress} from '../api'; @@ -60,23 +25,18 @@ const MockTagsGroup = ({tagNames, onClick}) => (
); -// 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 ( - - {children} - + return renderWithProviders( + {ui}, + 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(); - expect(screen.getByTestId('modal')).toBeInTheDocument(); + expect(screen.getByRole('dialog')).toBeInTheDocument(); }); it('does not render modal when open is false', () => { renderWithTags(); - expect(screen.queryByTestId('modal')).not.toBeInTheDocument(); + expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); }); it('shows header', async () => { renderWithTags(); - 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(); - 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(); // Modal should still be visible - expect(screen.getByTestId('modal')).toBeInTheDocument(); + expect(screen.getByRole('dialog')).toBeInTheDocument(); }); }); }); diff --git a/app/src/components/Upload.js b/app/src/components/Upload.js index 2c12970c..ce59bc3c 100644 --- a/app/src/components/Upload.js +++ b/app/src/components/Upload.js @@ -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 - - - - - -

{statusString} {name}

-
-
-
+ return + + + + + }) } return <> -
- - + +
+ setDestination(i)} disabled={disabled || inProgress} style={{marginBottom: '0.5em'}} /> {tagsSelector} - +
setOverwrite(!overwrite)} disabled={disabled || inProgress} /> - +
{destination ? - - - - -
-
- - - - -
- - Click here, or drop files here to upload -
-
-
-
-
-
-
-
-
-
+ +
+ +
+ Click here, or drop files here to upload +
+
+
:
You must search for a directory to place your files
}
- - - - {progresses && Object.keys(progresses).length > 1 && } - - - - + {progresses && Object.keys(progresses).length > 1 && } + + {progressBars} - + } diff --git a/app/src/components/admin/Downloads.js b/app/src/components/admin/Downloads.js index 70ee7c5c..93526f1a 100644 --- a/app/src/components/admin/Downloads.js +++ b/app/src/components/admin/Downloads.js @@ -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}) { - {humanFileSize(progress.bytes_downloaded)} / {humanFileSize(progress.total_bytes)} - - - - - Speed - {speedText} - - - ETA - {progress.eta || 'Calculating...'} - - - Downloaded - {humanFileSize(progress.bytes_downloaded)} / {humanFileSize(progress.total_bytes)} - - {url && - URL - {url} - } - + label={`${humanFileSize(progress.bytes_downloaded)} / ${humanFileSize(progress.total_bytes)}`} + /> +
+ + + Speed + {speedText} + + + ETA + {progress.eta || 'Calculating...'} + + + Downloaded + {humanFileSize(progress.bytes_downloaded)} / {humanFileSize(progress.total_bytes)} + + {url && + URL + {url} + } +
- + ; @@ -154,7 +150,7 @@ function RetryDownloadsButton({callback, selectedIds, clearSelection}) { const label = hasSelection ? `Retry (${selectedIds.length})` : 'Retry'; return {label} @@ -184,7 +180,7 @@ function DeleteOnceDownloadsButton({callback, selectedIds, clearSelection}) { : 'Are you sure you want to delete all downloads? Some may be retried!'; return } > Download Error
{error}
- Close + ; + const errorTrigger = ; + const hideEdit = downloader === Downloaders.MapCatalog || downloader === Downloaders.MapExtract; - const editButton = hideEdit ? null :