mirror of
https://github.com/lrnselfreliance/wrolpi.git
synced 2026-10-26 19:01:39 -04:00
Adding Upload button to the FileBrowser. Adding ErrorBoundaries to prevent entire app from crashing.
This commit is contained in:
parent
166d6a47c3
commit
5c8f164c1a
7 changed files with 508 additions and 29 deletions
|
|
@ -21,6 +21,7 @@ import {SemanticToastContainer} from "react-semantic-toasts-2";
|
|||
import {FilePreviewProvider} from "./components/FilePreview";
|
||||
import {TagsProvider} from "./Tags";
|
||||
import {ZimRoute} from "./components/Zim";
|
||||
import ErrorBoundary from "./components/ErrorBoundary";
|
||||
|
||||
function PageNotFound() {
|
||||
const {t} = useContext(ThemeContext);
|
||||
|
|
@ -60,7 +61,9 @@ function Root() {
|
|||
<header>
|
||||
<NavBar/>
|
||||
</header>
|
||||
<Outlet/>
|
||||
<ErrorBoundary>
|
||||
<Outlet/>
|
||||
</ErrorBoundary>
|
||||
<Footer/>
|
||||
</FilePreviewProvider>
|
||||
</TagsProvider>
|
||||
|
|
@ -73,19 +76,19 @@ const router = createBrowserRouter(createRoutesFromElements(<Route
|
|||
element={<Root/>}
|
||||
errorElement={<PageNotFound/>}
|
||||
>
|
||||
<Route index element={<DashboardPage/>}/>
|
||||
<Route path='search/*' element={<DashboardPage/>}/>
|
||||
<Route index element={<ErrorBoundary><DashboardPage/></ErrorBoundary>}/>
|
||||
<Route path='search/*' element={<ErrorBoundary><DashboardPage/></ErrorBoundary>}/>
|
||||
<Route path='donate' element={<DonatePage/>}/>
|
||||
<Route path='videos/video/:videoId' exact element={<VideoWrapper/>}/>
|
||||
<Route path='videos/channel/:channelId/video/:videoId' exact element={<VideoWrapper/>}/>
|
||||
<Route path="videos/*" element={<VideosRoute/>}/>
|
||||
<Route path="admin/*" element={<AdminRoute/>}/>
|
||||
<Route path="more/*" element={<MoreRoute/>}/>
|
||||
<Route path="inventory/*" element={<InventoryRoute/>}/>
|
||||
<Route path='archive/*' element={<ArchiveRoute/>}/>
|
||||
<Route path='map/*' element={<MapRoute/>}/>
|
||||
<Route path='zim/*' element={<ZimRoute/>}/>
|
||||
<Route path='files/*' element={<FilesRoute/>}/>
|
||||
<Route path='videos/video/:videoId' exact element={<ErrorBoundary><VideoWrapper/></ErrorBoundary>}/>
|
||||
<Route path='videos/channel/:channelId/video/:videoId' exact element={<ErrorBoundary><VideoWrapper/></ErrorBoundary>}/>
|
||||
<Route path="videos/*" element={<ErrorBoundary><VideosRoute/></ErrorBoundary>}/>
|
||||
<Route path="admin/*" element={<ErrorBoundary><AdminRoute/></ErrorBoundary>}/>
|
||||
<Route path="more/*" element={<ErrorBoundary><MoreRoute/></ErrorBoundary>}/>
|
||||
<Route path="inventory/*" element={<ErrorBoundary><InventoryRoute/></ErrorBoundary>}/>
|
||||
<Route path='archive/*' element={<ErrorBoundary><ArchiveRoute/></ErrorBoundary>}/>
|
||||
<Route path='map/*' element={<ErrorBoundary><MapRoute/></ErrorBoundary>}/>
|
||||
<Route path='zim/*' element={<ErrorBoundary><ZimRoute/></ErrorBoundary>}/>
|
||||
<Route path='files/*' element={<ErrorBoundary><FilesRoute/></ErrorBoundary>}/>
|
||||
</Route>));
|
||||
|
||||
export default function App() {
|
||||
|
|
|
|||
|
|
@ -194,7 +194,7 @@ export function Getters() {
|
|||
</Button>
|
||||
</GridColumn>
|
||||
<GridColumn>
|
||||
<Button onClick={e => handleSetGetter(e, 'upload')}>
|
||||
<Button color='green' onClick={e => handleSetGetter(e, 'upload')}>
|
||||
<Icon name='upload'/>
|
||||
Upload
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -59,7 +59,15 @@ import {InputForm, useForm} from "../hooks/useForm";
|
|||
import {CollectionTagModal} from "./collections/CollectionTagModal";
|
||||
import {Link, Route, Routes, useLocation, useNavigate, useParams} from "react-router";
|
||||
import Message from "semantic-ui-react/dist/commonjs/collections/Message";
|
||||
import {useArchive, useDomain, useDomains, useOneQuery, useSearchArchives, useSearchDomain, useSearchOrder} from "../hooks/customHooks";
|
||||
import {
|
||||
useArchive,
|
||||
useDomain,
|
||||
useDomains,
|
||||
useOneQuery,
|
||||
useSearchArchives,
|
||||
useSearchDomain,
|
||||
useSearchOrder
|
||||
} from "../hooks/customHooks";
|
||||
import {FileCards, FileRowTagIcon, FilesView} from "./Files";
|
||||
import Grid from "semantic-ui-react/dist/commonjs/collections/Grid";
|
||||
import _ from "lodash";
|
||||
|
|
@ -218,9 +226,10 @@ function ArchivePage() {
|
|||
await fetchArchive();
|
||||
}
|
||||
|
||||
const archivedDatetimeString = isoDatetimeToAgoPopup(archiveFile.download_datetime, true);
|
||||
const publishedDatetimeString = archiveFile.published_datetime ? isoDatetimeToAgoPopup(archiveFile.published_datetime, true)
|
||||
: 'unknown';
|
||||
const downloadDatetimeString = archiveFile.download_datetime ?
|
||||
isoDatetimeToAgoPopup(archiveFile.download_datetime, true) : 'unknown';
|
||||
const publishedDatetimeString = archiveFile.published_datetime ?
|
||||
isoDatetimeToAgoPopup(archiveFile.published_datetime, true) : 'unknown';
|
||||
const modifiedDatetimeString = archiveFile.published_modified_datetime
|
||||
? isoDatetimeToAgoPopup(archiveFile.published_modified_datetime, true)
|
||||
: 'unknown';
|
||||
|
|
@ -295,7 +304,7 @@ function ArchivePage() {
|
|||
<Header as='h4'>Published: {publishedDatetimeString}</Header>
|
||||
</GridColumn>
|
||||
<GridColumn>
|
||||
<Header as='h4'>Archived: {archivedDatetimeString}</Header>
|
||||
<Header as='h4'>Downloaded: {downloadDatetimeString}</Header>
|
||||
</GridColumn>
|
||||
</GridRow>
|
||||
</Grid>
|
||||
|
|
|
|||
111
app/src/components/ErrorBoundary.js
Normal file
111
app/src/components/ErrorBoundary.js
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
import React from 'react';
|
||||
import {Button, Container, Icon, Message, Segment} from 'semantic-ui-react';
|
||||
|
||||
/**
|
||||
* Error Boundary component that catches JavaScript errors in child components
|
||||
* and displays a fallback UI instead of crashing the entire app.
|
||||
*
|
||||
* Usage:
|
||||
* <ErrorBoundary>
|
||||
* <ComponentThatMightThrow />
|
||||
* </ErrorBoundary>
|
||||
*
|
||||
* With custom fallback:
|
||||
* <ErrorBoundary fallback={<div>Something went wrong</div>}>
|
||||
* <ComponentThatMightThrow />
|
||||
* </ErrorBoundary>
|
||||
*/
|
||||
class ErrorBoundary extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {hasError: false, error: null, errorInfo: null};
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error) {
|
||||
// Update state so next render shows fallback UI
|
||||
return {hasError: true, error};
|
||||
}
|
||||
|
||||
componentDidCatch(error, errorInfo) {
|
||||
// Log the error for debugging
|
||||
console.error('ErrorBoundary caught an error:', error);
|
||||
console.error('Component stack:', errorInfo?.componentStack);
|
||||
this.setState({errorInfo});
|
||||
}
|
||||
|
||||
handleReset = () => {
|
||||
this.setState({hasError: false, error: null, errorInfo: null});
|
||||
};
|
||||
|
||||
handleReload = () => {
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
// Allow custom fallback UI
|
||||
if (this.props.fallback) {
|
||||
return this.props.fallback;
|
||||
}
|
||||
|
||||
const {error} = this.state;
|
||||
const errorMessage = error?.message || 'An unexpected error occurred';
|
||||
|
||||
return (
|
||||
<Container style={{padding: '2em'}}>
|
||||
<Message negative icon>
|
||||
<Icon name='warning sign'/>
|
||||
<Message.Content>
|
||||
<Message.Header>Something went wrong</Message.Header>
|
||||
<p>{errorMessage}</p>
|
||||
<Button.Group>
|
||||
<Button onClick={this.handleReset} primary>
|
||||
<Icon name='refresh'/> Try Again
|
||||
</Button>
|
||||
<Button onClick={this.handleReload}>
|
||||
<Icon name='redo'/> Reload Page
|
||||
</Button>
|
||||
</Button.Group>
|
||||
</Message.Content>
|
||||
</Message>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A minimal error boundary for inline components that shows a simple error message.
|
||||
*/
|
||||
class InlineErrorBoundary extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {hasError: false};
|
||||
}
|
||||
|
||||
static getDerivedStateFromError() {
|
||||
return {hasError: true};
|
||||
}
|
||||
|
||||
componentDidCatch(error, errorInfo) {
|
||||
console.error('InlineErrorBoundary caught:', error, errorInfo?.componentStack);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
return (
|
||||
<Segment basic>
|
||||
<Message negative size='small'>
|
||||
<Icon name='warning'/> Failed to load this section
|
||||
</Message>
|
||||
</Segment>
|
||||
);
|
||||
}
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
export default ErrorBoundary;
|
||||
export {ErrorBoundary, InlineErrorBoundary};
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
import {Button, Header, Icon, Modal, Placeholder, Table} from "./Theme";
|
||||
import {Button, Header, Icon, Modal, Placeholder, Progress, Segment, Table} from "./Theme";
|
||||
import {
|
||||
Checkbox,
|
||||
Form,
|
||||
Grid,
|
||||
IconGroup,
|
||||
Input,
|
||||
PlaceholderLine,
|
||||
|
|
@ -11,16 +12,27 @@ import {
|
|||
TableHeaderCell,
|
||||
TableRow
|
||||
} from "semantic-ui-react";
|
||||
import {APIButton, DirectorySearch, ErrorMessage, FileIcon, humanFileSize, useIsIgnoredDirectory} from "./Common";
|
||||
import {
|
||||
APIButton,
|
||||
DirectorySearch,
|
||||
ErrorMessage,
|
||||
FileIcon,
|
||||
humanFileSize,
|
||||
mimetypeIconName,
|
||||
Toggle,
|
||||
useIsIgnoredDirectory
|
||||
} from "./Common";
|
||||
import React, {useEffect, useState} from "react";
|
||||
import {useDropzone} from "react-dropzone";
|
||||
import {deleteFile, ignoreDirectory, makeDirectory, movePaths, renamePath, unignoreDirectory} from "../api";
|
||||
import _ from 'lodash';
|
||||
import {SortableTable} from "./SortableTable";
|
||||
import {useBrowseFiles, useMediaDirectory, useWROLMode} from "../hooks/customHooks";
|
||||
import {useBrowseFiles, useMediaDirectory, useUploadFile, useWROLMode} from "../hooks/customHooks";
|
||||
import {FileRowTagIcon, FilesRefreshButton} from "./Files";
|
||||
import {FilePreviewContext} from "./FilePreview";
|
||||
import {SettingsContext, ThemeContext} from "../contexts/contexts";
|
||||
import {BulkTagModal} from "./BulkTagModal";
|
||||
import {InlineErrorBoundary} from "./ErrorBoundary";
|
||||
|
||||
function depthIndentation(path) {
|
||||
// Repeated spaces for every folder a path is in.
|
||||
|
|
@ -77,8 +89,10 @@ function Folder({folder, onFolderClick, sortData, selectedPaths, onFileClick, on
|
|||
let childPaths = [];
|
||||
_.forEach(children, (p, k) => {
|
||||
childPaths = [...childPaths,
|
||||
<Path key={k} path={p} onFolderClick={onFolderClick} sortData={sortData} onFileClick={onFileClick}
|
||||
selectedPaths={selectedPaths} onSelect={onSelect} disabled={disabled}/>];
|
||||
<InlineErrorBoundary key={k}>
|
||||
<Path path={p} onFolderClick={onFolderClick} sortData={sortData} onFileClick={onFileClick}
|
||||
selectedPaths={selectedPaths} onSelect={onSelect} disabled={disabled}/>
|
||||
</InlineErrorBoundary>];
|
||||
});
|
||||
return <React.Fragment>
|
||||
{f}
|
||||
|
|
@ -138,6 +152,7 @@ export function FileBrowser() {
|
|||
const [makeDirectoryOpen, setMakeDirectoryOpen] = React.useState(false);
|
||||
const [ignoreDirectoryOpen, setIgnoreDirectoryOpen] = React.useState(false);
|
||||
const [bulkTagOpen, setBulkTagOpen] = React.useState(false);
|
||||
const [uploadOpen, setUploadOpen] = React.useState(false);
|
||||
const [pending, setPending] = React.useState(false);
|
||||
// Only true if only one path is selected, and it is a directory.
|
||||
const [singleDirectorySelected, setSingleDirectorySelected] = React.useState(false);
|
||||
|
|
@ -270,6 +285,18 @@ export function FileBrowser() {
|
|||
parent={selectedPaths.length ? selectedPaths[0] : null}
|
||||
onSubmit={handleMakeDirectory}
|
||||
/>
|
||||
<Button
|
||||
icon='upload'
|
||||
color='green'
|
||||
disabled={wrolModeEnabled || (selectedPathsCount > 0 && !singleDirectorySelected)}
|
||||
onClick={() => setUploadOpen(true)}
|
||||
/>
|
||||
<FileBrowserUploadModal
|
||||
open={uploadOpen}
|
||||
onClose={() => setUploadOpen(false)}
|
||||
destination={singleDirectorySelected && selectedPaths[0] ? selectedPaths[0].slice(0, -1) : ''}
|
||||
onComplete={reset}
|
||||
/>
|
||||
<Button
|
||||
color='grey'
|
||||
icon={isDirectoryIgnored ? 'eye' : 'eye slash'}
|
||||
|
|
@ -564,3 +591,151 @@ export function MakeDirectoryModal({open, onClose, parent, onSubmit}) {
|
|||
</Modal.Actions>
|
||||
</Modal>
|
||||
}
|
||||
|
||||
export function FileBrowserUploadModal({open, onClose, destination, onComplete}) {
|
||||
const mediaDirectory = useMediaDirectory();
|
||||
const {t} = React.useContext(ThemeContext);
|
||||
|
||||
const {
|
||||
setFiles,
|
||||
progresses,
|
||||
setDestination,
|
||||
doClear,
|
||||
tagsSelector,
|
||||
overwrite,
|
||||
setOverwrite,
|
||||
overallProgress,
|
||||
inProgress,
|
||||
} = useUploadFile();
|
||||
|
||||
const onDrop = React.useCallback(async (acceptedFiles) => {
|
||||
if (acceptedFiles && acceptedFiles.length > 0) {
|
||||
setFiles(acceptedFiles);
|
||||
}
|
||||
}, [setFiles]);
|
||||
|
||||
const {getRootProps, getInputProps} = useDropzone({onDrop, disabled: inProgress});
|
||||
|
||||
// Set destination when modal opens or destination changes
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setDestination(destination);
|
||||
}
|
||||
}, [open, destination, setDestination]);
|
||||
|
||||
// Clear upload state when destination is cleared
|
||||
useEffect(() => {
|
||||
if (!destination && open) {
|
||||
doClear();
|
||||
}
|
||||
}, [destination, open, doClear]);
|
||||
|
||||
const handleClose = () => {
|
||||
if (!inProgress) {
|
||||
doClear();
|
||||
onClose();
|
||||
if (onComplete) {
|
||||
onComplete();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let progressBars;
|
||||
if (!_.isEmpty(progresses)) {
|
||||
progressBars = Object.entries(progresses).map(([name, value]) => {
|
||||
const {percent, status, type} = value;
|
||||
let color = 'grey';
|
||||
let statusString;
|
||||
if (status === 'complete') {
|
||||
color = 'green';
|
||||
statusString = 'Complete:';
|
||||
} else if (status === 'pending') {
|
||||
statusString = 'Pending:';
|
||||
} else if (status === 'failed') {
|
||||
color = 'red';
|
||||
statusString = 'Failed:';
|
||||
} else if (status === 'conflicting') {
|
||||
color = 'orange';
|
||||
statusString = 'Already Exists:';
|
||||
}
|
||||
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>
|
||||
})
|
||||
}
|
||||
|
||||
const displayPath = destination ? `${mediaDirectory}/${destination}` : mediaDirectory;
|
||||
|
||||
return <Modal closeIcon open={open} onClose={handleClose} size='large'>
|
||||
<Modal.Header>Upload to: {displayPath}</Modal.Header>
|
||||
<Modal.Content>
|
||||
<Form>
|
||||
{tagsSelector}
|
||||
<div style={{marginTop: '1em'}}>
|
||||
<Toggle
|
||||
checked={overwrite}
|
||||
label='Overwrite existing files'
|
||||
onChange={() => setOverwrite(!overwrite)}
|
||||
disabled={inProgress}
|
||||
/>
|
||||
</div>
|
||||
</Form>
|
||||
|
||||
<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>
|
||||
|
||||
<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}>
|
||||
{progressBars}
|
||||
</Grid>
|
||||
</Modal.Content>
|
||||
<Modal.Actions>
|
||||
<Button
|
||||
onClick={handleClose}
|
||||
disabled={inProgress}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</Modal.Actions>
|
||||
</Modal>
|
||||
}
|
||||
|
|
|
|||
178
app/src/components/FileBrowser.test.js
Normal file
178
app/src/components/FileBrowser.test.js
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
import React from 'react';
|
||||
import {act, fireEvent, screen, waitFor} from '@testing-library/react';
|
||||
import {FileBrowser} from './FileBrowser';
|
||||
import {renderWithProviders} from '../test-utils';
|
||||
import {SettingsContext} from '../contexts/contexts';
|
||||
import {FilePreviewContext} from './FilePreview';
|
||||
|
||||
// Mock the API functions
|
||||
jest.mock('../api', () => ({
|
||||
deleteFile: jest.fn(),
|
||||
ignoreDirectory: jest.fn(),
|
||||
makeDirectory: jest.fn(),
|
||||
movePaths: jest.fn(),
|
||||
renamePath: jest.fn(),
|
||||
unignoreDirectory: jest.fn(),
|
||||
}));
|
||||
|
||||
// Mock the custom hooks
|
||||
const mockUseUploadFile = {
|
||||
setFiles: jest.fn(),
|
||||
progresses: {},
|
||||
setDestination: jest.fn(),
|
||||
doClear: jest.fn(),
|
||||
tagsSelector: null,
|
||||
overwrite: false,
|
||||
setOverwrite: jest.fn(),
|
||||
overallProgress: 0,
|
||||
inProgress: false,
|
||||
};
|
||||
|
||||
jest.mock('../hooks/customHooks', () => ({
|
||||
useBrowseFiles: jest.fn(),
|
||||
useMediaDirectory: jest.fn(() => '/media/wrolpi'),
|
||||
useWROLMode: jest.fn(() => false),
|
||||
useStatusFlag: jest.fn(() => false),
|
||||
useUploadFile: () => mockUseUploadFile,
|
||||
}));
|
||||
|
||||
// Mock react-dropzone
|
||||
jest.mock('react-dropzone', () => ({
|
||||
useDropzone: () => ({
|
||||
getRootProps: () => ({}),
|
||||
getInputProps: () => ({}),
|
||||
}),
|
||||
}));
|
||||
|
||||
// Import mocked hooks for manipulation
|
||||
import {useBrowseFiles, useWROLMode} from '../hooks/customHooks';
|
||||
|
||||
// Create wrapper with all required contexts
|
||||
const AllContextsWrapper = ({children}) => {
|
||||
const settingsValue = {
|
||||
settings: {ignored_directories: []},
|
||||
fetchSettings: jest.fn(),
|
||||
};
|
||||
const filePreviewValue = {
|
||||
setPreviewFile: jest.fn(),
|
||||
setCallbacks: jest.fn(),
|
||||
};
|
||||
|
||||
return (
|
||||
<SettingsContext.Provider value={settingsValue}>
|
||||
<FilePreviewContext.Provider value={filePreviewValue}>
|
||||
{children}
|
||||
</FilePreviewContext.Provider>
|
||||
</SettingsContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
// Custom render with all contexts
|
||||
const renderFileBrowser = (ui, options = {}) => {
|
||||
return renderWithProviders(
|
||||
<AllContextsWrapper>{ui}</AllContextsWrapper>,
|
||||
options
|
||||
);
|
||||
};
|
||||
|
||||
describe('FileBrowser', () => {
|
||||
// Note: Files are sorted alphabetically by path, so file.txt comes before testdir/
|
||||
const mockBrowseFiles = [
|
||||
{path: 'file.txt', size: 1024},
|
||||
{path: 'testdir/', children: null, is_empty: true},
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
useBrowseFiles.mockReturnValue({
|
||||
browseFiles: mockBrowseFiles,
|
||||
openFolders: [],
|
||||
setOpenFolders: jest.fn(),
|
||||
fetchFiles: jest.fn(),
|
||||
});
|
||||
useWROLMode.mockReturnValue(false);
|
||||
});
|
||||
|
||||
describe('Directory Selection', () => {
|
||||
it('renders without crashing', async () => {
|
||||
renderFileBrowser(<FileBrowser/>);
|
||||
// Should render the file browser table
|
||||
expect(screen.getByRole('table')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('handles selecting and unselecting a directory without error', async () => {
|
||||
renderFileBrowser(<FileBrowser/>);
|
||||
|
||||
// Find the directory - text includes trailing slash
|
||||
const directoryRow = screen.getByText('testdir/');
|
||||
expect(directoryRow).toBeInTheDocument();
|
||||
|
||||
// Find the checkbox in the same row - file.txt is first (index 0), testdir/ is second (index 1)
|
||||
const checkboxes = screen.getAllByRole('checkbox');
|
||||
const directoryCheckbox = checkboxes[1]; // Second checkbox is for the directory
|
||||
|
||||
// Select the directory
|
||||
await act(async () => {
|
||||
fireEvent.click(directoryCheckbox);
|
||||
});
|
||||
|
||||
// Unselect the directory - this should NOT throw an error
|
||||
await act(async () => {
|
||||
fireEvent.click(directoryCheckbox);
|
||||
});
|
||||
|
||||
// If we get here without error, the test passes
|
||||
expect(screen.getByRole('table')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('upload button is enabled when nothing is selected', async () => {
|
||||
renderFileBrowser(<FileBrowser/>);
|
||||
|
||||
// Find the upload button (green button with upload icon)
|
||||
const buttons = screen.getAllByRole('button');
|
||||
const uploadButton = buttons.find(btn =>
|
||||
btn.classList.contains('green') || btn.querySelector('[class*="upload"]')
|
||||
);
|
||||
|
||||
// Button should exist and not be disabled
|
||||
expect(uploadButton).toBeInTheDocument();
|
||||
expect(uploadButton).not.toBeDisabled();
|
||||
});
|
||||
|
||||
it('upload button is enabled when single directory is selected', async () => {
|
||||
renderFileBrowser(<FileBrowser/>);
|
||||
|
||||
// Select the directory (second checkbox - testdir/)
|
||||
const checkboxes = screen.getAllByRole('checkbox');
|
||||
const directoryCheckbox = checkboxes[1];
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(directoryCheckbox);
|
||||
});
|
||||
|
||||
// Find the upload button
|
||||
const buttons = screen.getAllByRole('button');
|
||||
const uploadButton = buttons.find(btn => btn.classList.contains('green'));
|
||||
|
||||
expect(uploadButton).not.toBeDisabled();
|
||||
});
|
||||
|
||||
it('upload button is disabled when file is selected', async () => {
|
||||
renderFileBrowser(<FileBrowser/>);
|
||||
|
||||
// Select the file (first checkbox - file.txt)
|
||||
const checkboxes = screen.getAllByRole('checkbox');
|
||||
const fileCheckbox = checkboxes[0];
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(fileCheckbox);
|
||||
});
|
||||
|
||||
// Find the upload button
|
||||
const buttons = screen.getAllByRole('button');
|
||||
const uploadButton = buttons.find(btn => btn.classList.contains('green'));
|
||||
|
||||
expect(uploadButton).toBeDisabled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -11,6 +11,7 @@ import {toast} from "react-semantic-toasts-2";
|
|||
import {useOneQuery} from "../hooks/customHooks";
|
||||
import {ShareButton} from "./Share";
|
||||
import {pathDirectory} from "./FileBrowser";
|
||||
import {InlineErrorBoundary} from "./ErrorBoundary";
|
||||
|
||||
function getMediaPathURL(previewFile) {
|
||||
if (previewFile['primary_path']) {
|
||||
|
|
@ -116,11 +117,13 @@ function getSTLPreviewModal(previewFile) {
|
|||
return <React.Fragment>
|
||||
<Modal.Header>{name}</Modal.Header>
|
||||
<Modal.Content>
|
||||
<StlViewer orbitControls shadows
|
||||
url={url}
|
||||
style={{height: '80vw', width: '80vw'}}
|
||||
modelProps={{color: '#7353a8'}}
|
||||
/>
|
||||
<InlineErrorBoundary>
|
||||
<StlViewer orbitControls shadows
|
||||
url={url}
|
||||
style={{height: '80vw', width: '80vw'}}
|
||||
modelProps={{color: '#7353a8'}}
|
||||
/>
|
||||
</InlineErrorBoundary>
|
||||
</Modal.Content>
|
||||
</React.Fragment>
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue