Added structure for filemanager (#4078)

Overview

Replaced scattered file inputs with a unified modal-based upload system.
Users now upload files via a global Files button with intelligent
tool-aware filtering.

  Key Changes

  🔄 New Upload Flow

  - Before: Direct file inputs throughout the UI
- After: Single Files button → Modal → Tool filters files automatically

  🎯 Smart File Filtering

  - Modal shows only supported file types based on selected tool
  - Visual indicators for unsupported files (grayed out + badges)
  - Automatic duplicate detection

   Enhanced UX

  - Files button shows active state when modal is open
  - Consistent upload experience across all tools
  - Professional modal workflow

  Architecture

  New Components

  FilesModalProvider → FileUploadModal → Tool-aware filtering

  Button System Redesign

  type: 'navigation' | 'modal' | 'action'
  // Only navigation buttons stay active
  // Modal buttons show active when modal open

  Files Changed

  -  QuickAccessBar.tsx - Added Files button
  -  FileUploadModal.tsx - New tool-aware modal
  -  HomePage.tsx - Integrated modal system
  -  ConvertE2E.spec.ts - Updated tests for modal workflow

  Benefits

  - Unified UX: One place to upload files
  - Smart Filtering: Only see relevant file types
  - Better Architecture: Clean separation of concerns
  - Improved Testing: Reliable test automation

Migration: File uploads now go through Files button → modal instead of
direct inputs. All existing functionality preserved.

---------

Co-authored-by: Connor Yoh <connor@stirlingpdf.com>
This commit is contained in:
ConnorYoh
2025-08-04 15:01:36 +01:00
committed by GitHub
parent 24a9104ebf
commit 90f0c5826a
11 changed files with 318 additions and 140 deletions

View File

@@ -665,46 +665,35 @@ const FileEditor = ({
return (
<Box pos="relative" h="100vh" style={{ overflow: 'auto' }}>
<LoadingOverlay visible={false} />
<Dropzone
onDrop={handleFileUpload}
accept={["*/*"]}
multiple={true}
maxSize={2 * 1024 * 1024 * 1024}
style={{
height: '100vh',
border: 'none',
borderRadius: 0,
backgroundColor: 'transparent'
}}
activateOnClick={false}
activateOnDrag={true}
>
<Box pos="relative" h="100vh" style={{ overflow: 'auto' }}>
<LoadingOverlay visible={false} />
<Box p="md" pt="xl">
<Group mb="md">
{showBulkActions && !toolMode && (
<>
<Button onClick={selectAll} variant="light">Select All</Button>
<Button onClick={deselectAll} variant="light">Deselect All</Button>
<Button onClick={closeAllFiles} variant="light" color="orange">
Close All
</Button>
</>
)}
{/* Load from storage and upload buttons */}
{showUpload && (
<>
<Button
variant="outline"
color="blue"
onClick={() => setShowFilePickerModal(true)}
>
Load from Storage
</Button>
<Dropzone
onDrop={handleFileUpload}
accept={["*/*"]}
multiple={true}
maxSize={2 * 1024 * 1024 * 1024}
style={{ display: 'contents' }}
>
<Button variant="outline" color="green">
Upload Files
<Box p="md" pt="xl">
<Group mb="md">
{showBulkActions && !toolMode && (
<>
<Button onClick={selectAll} variant="light">Select All</Button>
<Button onClick={deselectAll} variant="light">Deselect All</Button>
<Button onClick={closeAllFiles} variant="light" color="orange">
Close All
</Button>
</Dropzone>
</>
)}
</Group>
</>
)}
</Group>
{files.length === 0 && !localLoading && !zipExtractionProgress.isExtracting ? (
@@ -866,7 +855,8 @@ const FileEditor = ({
{error}
</Notification>
)}
</Box>
</Box>
</Dropzone>
);
};

View File

@@ -0,0 +1,36 @@
import React from 'react';
import { Modal } from '@mantine/core';
import FileUploadSelector from './FileUploadSelector';
import { useFilesModalContext } from '../../contexts/FilesModalContext';
import { Tool } from '../../types/tool';
interface FileUploadModalProps {
selectedTool?: Tool | null;
}
const FileUploadModal: React.FC<FileUploadModalProps> = ({ selectedTool }) => {
const { isFilesModalOpen, closeFilesModal, onFileSelect, onFilesSelect } = useFilesModalContext();
return (
<Modal
opened={isFilesModalOpen}
onClose={closeFilesModal}
title="Upload Files"
size="xl"
centered
>
<FileUploadSelector
title="Upload Files"
subtitle="Choose files from storage or upload new files"
onFileSelect={onFileSelect}
onFilesSelect={onFilesSelect}
accept={["*/*"]}
supportedExtensions={selectedTool?.supportedFormats || ["pdf"]}
data-testid="file-upload-modal"
/>
</Modal>
);
};
export default FileUploadModal;

View File

@@ -0,0 +1,30 @@
import React from 'react';
import { Container, Stack, Text, Button } from '@mantine/core';
import FolderIcon from '@mui/icons-material/FolderRounded';
import { useFilesModalContext } from '../../contexts/FilesModalContext';
interface LandingPageProps {
title: string;
}
const LandingPage = ({ title }: LandingPageProps) => {
const { openFilesModal } = useFilesModalContext();
return (
<Container size="lg" p="xl" h="100%" style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<Stack align="center" gap="lg">
<Text size="xl" fw={500} c="dimmed">
{title}
</Text>
<Button
leftSection={<FolderIcon />}
size="lg"
onClick={openFilesModal}
>
Open Files
</Button>
</Stack>
</Container>
);
};
export default LandingPage;

View File

@@ -11,6 +11,7 @@ import { useRainbowThemeContext } from "./RainbowThemeProvider";
import rainbowStyles from '../../styles/rainbow.module.css';
import AppConfigModal from './AppConfigModal';
import { useIsOverflowing } from '../../hooks/useIsOverflowing';
import { useFilesModalContext } from '../../contexts/FilesModalContext';
import './QuickAccessBar.css';
interface QuickAccessBarProps {
@@ -30,6 +31,7 @@ interface ButtonConfig {
isRound?: boolean;
size?: 'sm' | 'md' | 'lg' | 'xl';
onClick: () => void;
type?: 'navigation' | 'modal' | 'action'; // navigation = main nav, modal = triggers modal, action = other actions
}
function NavHeader({
@@ -111,11 +113,16 @@ const QuickAccessBar = ({
readerMode,
}: QuickAccessBarProps) => {
const { isRainbowMode } = useRainbowThemeContext();
const { openFilesModal, isFilesModalOpen } = useFilesModalContext();
const [configModalOpen, setConfigModalOpen] = useState(false);
const [activeButton, setActiveButton] = useState<string>('tools');
const scrollableRef = useRef<HTMLDivElement>(null);
const isOverflow = useIsOverflowing(scrollableRef);
const handleFilesButtonClick = () => {
openFilesModal();
};
const buttonConfigs: ButtonConfig[] = [
{
id: 'read',
@@ -124,6 +131,7 @@ const QuickAccessBar = ({
tooltip: 'Read documents',
size: 'lg',
isRound: false,
type: 'navigation',
onClick: () => {
setActiveButton('read');
onReaderToggle();
@@ -139,6 +147,7 @@ const QuickAccessBar = ({
tooltip: 'Sign your document',
size: 'lg',
isRound: false,
type: 'navigation',
onClick: () => setActiveButton('sign')
},
{
@@ -148,6 +157,7 @@ const QuickAccessBar = ({
tooltip: 'Automate workflows',
size: 'lg',
isRound: false,
type: 'navigation',
onClick: () => setActiveButton('automate')
},
{
@@ -157,7 +167,8 @@ const QuickAccessBar = ({
tooltip: 'Manage files',
isRound: true,
size: 'lg',
onClick: () => setActiveButton('files')
type: 'modal',
onClick: handleFilesButtonClick
},
{
id: 'activity',
@@ -169,6 +180,7 @@ const QuickAccessBar = ({
tooltip: 'View activity and analytics',
isRound: true,
size: 'lg',
type: 'navigation',
onClick: () => setActiveButton('activity')
},
{
@@ -177,6 +189,7 @@ const QuickAccessBar = ({
icon: <SettingsIcon sx={{ fontSize: "1rem" }} />,
tooltip: 'Configure settings',
size: 'lg',
type: 'modal',
onClick: () => {
setConfigModalOpen(true);
}
@@ -190,8 +203,16 @@ const QuickAccessBar = ({
return config.isRound ? CIRCULAR_BORDER_RADIUS : ROUND_BORDER_RADIUS;
};
const isButtonActive = (config: ButtonConfig): boolean => {
return (
(config.type === 'navigation' && activeButton === config.id) ||
(config.type === 'modal' && config.id === 'files' && isFilesModalOpen) ||
(config.type === 'modal' && config.id === 'config' && configModalOpen)
);
};
const getButtonStyle = (config: ButtonConfig) => {
const isActive = activeButton === config.id;
const isActive = isButtonActive(config);
if (isActive) {
return {
@@ -202,7 +223,7 @@ const QuickAccessBar = ({
};
}
// Inactive state - use consistent inactive colors
// Inactive state for all buttons
return {
backgroundColor: 'var(--icon-inactive-bg)',
color: 'var(--icon-inactive-color)',
@@ -254,13 +275,14 @@ const QuickAccessBar = ({
variant="subtle"
onClick={config.onClick}
style={getButtonStyle(config)}
className={activeButton === config.id ? 'activeIconScale' : ''}
className={isButtonActive(config) ? 'activeIconScale' : ''}
data-testid={`${config.id}-button`}
>
<span className="iconContainer">
{config.icon}
</span>
</ActionIcon>
<span className={`button-text ${activeButton === config.id ? 'active' : 'inactive'}`}>
<span className={`button-text ${isButtonActive(config) ? 'active' : 'inactive'}`}>
{config.name}
</span>
</div>
@@ -281,30 +303,29 @@ const QuickAccessBar = ({
<div className="spacer" />
{/* Config button at the bottom */}
<Tooltip label="Configure settings" position="right">
<div className="flex flex-col items-center gap-1">
<ActionIcon
size="lg"
variant="subtle"
onClick={() => {
setConfigModalOpen(true);
}}
style={{
backgroundColor: 'var(--icon-inactive-bg)',
color: 'var(--icon-inactive-color)',
border: 'none',
borderRadius: '8px',
}}
>
<span className="iconContainer">
<SettingsIcon sx={{ fontSize: "1rem" }} />
</span>
</ActionIcon>
<span className="config-button-text">
Config
</span>
</div>
</Tooltip>
{buttonConfigs
.filter(config => config.id === 'config')
.map(config => (
<Tooltip key={config.id} label={config.tooltip} position="right">
<div className="flex flex-col items-center gap-1">
<ActionIcon
size={config.size || 'lg'}
variant="subtle"
onClick={config.onClick}
style={getButtonStyle(config)}
className={isButtonActive(config) ? 'activeIconScale' : ''}
data-testid={`${config.id}-button`}
>
<span className="iconContainer">
{config.icon}
</span>
</ActionIcon>
<span className={`button-text ${isButtonActive(config) ? 'active' : 'inactive'}`}>
{config.name}
</span>
</div>
</Tooltip>
))}
</div>
</div>

View File

@@ -198,7 +198,7 @@ const ConvertSettings = ({
</Text>
<GroupedFormatDropdown
name="convert-from-dropdown"
data-testid="from-format-dropdown"
data-testid="convert-from-dropdown"
value={parameters.fromExtension}
placeholder={t("convert.sourceFormatPlaceholder", "Source format")}
options={enhancedFromOptions}
@@ -236,7 +236,7 @@ const ConvertSettings = ({
) : (
<GroupedFormatDropdown
name="convert-to-dropdown"
data-testid="to-format-dropdown"
data-testid="convert-to-dropdown"
value={parameters.toExtension}
placeholder={t("convert.targetFormatPlaceholder", "Target format")}
options={enhancedToOptions}