mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2026-03-13 02:18:16 +01:00
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:
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
36
frontend/src/components/shared/FileUploadModal.tsx
Normal file
36
frontend/src/components/shared/FileUploadModal.tsx
Normal 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;
|
||||
30
frontend/src/components/shared/LandingPage.tsx
Normal file
30
frontend/src/components/shared/LandingPage.tsx
Normal 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;
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user