mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-16 19:33:11 +02:00
couple of small fixes for text editor (#5155)
# Description of Changes - Workbench.tsx: Allow PDF text editor to handle it's own custom state (this is a bit of a hotfix, we're going to handle the PDF text editors file upload flow better in a future PR). - PdfTextEditorView.tsx: Added a dropzone and some helper text to upload the first file. - PdfTextEditorView.tsx: Hide document view when isConverting is true. Prevents showing stale content from previous file during conversion. - useTranslatedToolRegistry.tsx: Moved PDF Text Editor to top of Recommended tools list. Increased visibility of the new feature. - PdfTextEditor.tsx: Removed auto-navigation to PDF Editor workbench on file selection. Stops the "jumpy" behavior when selecting files. - HomePage.tsx: Check specifically for pdfTextEditor tool instead of any custom workbench. Prevents auto-switch to viewer when uploading files while in PDF Text Editor. <img width="2056" height="1073" alt="Screenshot 2025-12-03 at 6 01 14 PM" src="https://github.com/user-attachments/assets/dfc63a46-7991-486c-ba00-0ce7637502f5" /> --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details. --------- Co-authored-by: Anthony Stirling <[email protected]>
This commit is contained in:
co-authored by
Anthony Stirling
parent
f8dbf171e1
commit
c9bf436895
@@ -71,6 +71,20 @@ export default function Workbench() {
|
||||
};
|
||||
|
||||
const renderMainContent = () => {
|
||||
// Check for custom workbench views first
|
||||
if (!isBaseWorkbench(currentView)) {
|
||||
const customView = customWorkbenchViews.find((view) => view.workbenchId === currentView && view.data != null);
|
||||
if (customView) {
|
||||
// PDF text editor handles its own empty state (shows dropzone when no document)
|
||||
const handlesOwnEmptyState = currentView === 'custom:pdfTextEditor';
|
||||
if (handlesOwnEmptyState || activeFiles.length > 0) {
|
||||
const CustomComponent = customView.component;
|
||||
return <CustomComponent data={customView.data} />;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For base workbenches (or custom views that don't handle empty state), show landing page when no files
|
||||
if (activeFiles.length === 0) {
|
||||
return (
|
||||
<LandingPage
|
||||
@@ -143,15 +157,6 @@ export default function Workbench() {
|
||||
);
|
||||
|
||||
default:
|
||||
if (!isBaseWorkbench(currentView)) {
|
||||
const customView = customWorkbenchViews.find((view) => view.workbenchId === currentView && view.data != null);
|
||||
|
||||
|
||||
if (customView) {
|
||||
const CustomComponent = customView.component;
|
||||
return <CustomComponent data={customView.data} />;
|
||||
}
|
||||
}
|
||||
return <LandingPage />;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { Tooltip } from '@app/components/shared/Tooltip';
|
||||
import AppsIcon from '@mui/icons-material/AppsRounded';
|
||||
import { useToolWorkflow } from '@app/contexts/ToolWorkflowContext';
|
||||
import { useNavigationState, useNavigationActions } from '@app/contexts/NavigationContext';
|
||||
import { useSidebarNavigation } from '@app/hooks/useSidebarNavigation';
|
||||
import { handleUnlessSpecialClick } from '@app/utils/clickHandlers';
|
||||
|
||||
@@ -20,21 +21,36 @@ const AllToolsNavButton: React.FC<AllToolsNavButtonProps> = ({
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { handleReaderToggle, handleBackToTools, selectedToolKey, leftPanelView } = useToolWorkflow();
|
||||
const { hasUnsavedChanges } = useNavigationState();
|
||||
const { actions: navigationActions } = useNavigationActions();
|
||||
const { getHomeNavigation } = useSidebarNavigation();
|
||||
|
||||
const handleClick = () => {
|
||||
const performNavigation = () => {
|
||||
setActiveButton('tools');
|
||||
// Preserve existing behavior used in QuickAccessBar header
|
||||
handleReaderToggle();
|
||||
handleBackToTools();
|
||||
};
|
||||
|
||||
const handleClick = () => {
|
||||
if (hasUnsavedChanges) {
|
||||
navigationActions.requestNavigation(performNavigation);
|
||||
return;
|
||||
}
|
||||
performNavigation();
|
||||
};
|
||||
|
||||
// Do not highlight All Tools when a specific tool is open (indicator is shown)
|
||||
const isActive = activeButton === 'tools' && !selectedToolKey && leftPanelView === 'toolPicker';
|
||||
|
||||
const navProps = getHomeNavigation();
|
||||
|
||||
const handleNavClick = (e: React.MouseEvent) => {
|
||||
if (hasUnsavedChanges) {
|
||||
e.preventDefault();
|
||||
navigationActions.requestNavigation(performNavigation);
|
||||
return;
|
||||
}
|
||||
handleUnlessSpecialClick(e, handleClick);
|
||||
};
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ interface NavigationWarningModalProps {
|
||||
|
||||
const NavigationWarningModal = ({ onApplyAndContinue, onExportAndContinue }: NavigationWarningModalProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { showNavigationWarning, hasUnsavedChanges, cancelNavigation, confirmNavigation, setHasUnsavedChanges } =
|
||||
const { showNavigationWarning, hasUnsavedChanges, pendingNavigation, cancelNavigation, confirmNavigation, setHasUnsavedChanges } =
|
||||
useNavigationGuard();
|
||||
|
||||
const handleKeepWorking = () => {
|
||||
@@ -41,7 +41,9 @@ const NavigationWarningModal = ({ onApplyAndContinue, onExportAndContinue }: Nav
|
||||
};
|
||||
const BUTTON_WIDTH = "10rem";
|
||||
|
||||
if (!hasUnsavedChanges) {
|
||||
// Only show modal if there are unsaved changes AND there's an actual pending navigation
|
||||
// This prevents the modal from showing due to spurious state updates
|
||||
if (!hasUnsavedChanges || !pendingNavigation) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useRainbowThemeContext } from "@app/components/shared/RainbowThemeProvi
|
||||
import { useIsOverflowing } from '@app/hooks/useIsOverflowing';
|
||||
import { useFilesModalContext } from '@app/contexts/FilesModalContext';
|
||||
import { useToolWorkflow } from '@app/contexts/ToolWorkflowContext';
|
||||
import { useNavigationState, useNavigationActions } from '@app/contexts/NavigationContext';
|
||||
import { useSidebarNavigation } from '@app/hooks/useSidebarNavigation';
|
||||
import { handleUnlessSpecialClick } from '@app/utils/clickHandlers';
|
||||
import { ButtonConfig } from '@app/types/sidebar';
|
||||
@@ -32,6 +33,8 @@ const QuickAccessBar = forwardRef<HTMLDivElement>((_, ref) => {
|
||||
const { isRainbowMode } = useRainbowThemeContext();
|
||||
const { openFilesModal, isFilesModalOpen } = useFilesModalContext();
|
||||
const { handleReaderToggle, handleToolSelect, selectedToolKey, leftPanelView, toolRegistry, readerMode, resetTool } = useToolWorkflow();
|
||||
const { hasUnsavedChanges } = useNavigationState();
|
||||
const { actions: navigationActions } = useNavigationActions();
|
||||
const { getToolNavigation } = useSidebarNavigation();
|
||||
const { config } = useAppConfig();
|
||||
const licenseAlert = useLicenseAlert();
|
||||
@@ -58,7 +61,7 @@ const QuickAccessBar = forwardRef<HTMLDivElement>((_, ref) => {
|
||||
};
|
||||
|
||||
// Helper function to render navigation buttons with URL support
|
||||
const renderNavButton = (config: ButtonConfig, index: number) => {
|
||||
const renderNavButton = (config: ButtonConfig, index: number, shouldGuardNavigation = false) => {
|
||||
const isActive = isNavButtonActive(config, activeButton, isFilesModalOpen, configModalOpen, selectedToolKey, leftPanelView);
|
||||
|
||||
// Check if this button has URL navigation support
|
||||
@@ -67,6 +70,14 @@ const QuickAccessBar = forwardRef<HTMLDivElement>((_, ref) => {
|
||||
: null;
|
||||
|
||||
const handleClick = (e?: React.MouseEvent) => {
|
||||
// If there are unsaved changes and this button should guard navigation, show warning modal
|
||||
if (shouldGuardNavigation && hasUnsavedChanges) {
|
||||
e?.preventDefault();
|
||||
navigationActions.requestNavigation(() => {
|
||||
config.onClick();
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (navProps && e) {
|
||||
handleUnlessSpecialClick(e, config.onClick);
|
||||
} else {
|
||||
@@ -89,7 +100,7 @@ const QuickAccessBar = forwardRef<HTMLDivElement>((_, ref) => {
|
||||
onClick: (e: React.MouseEvent) => handleClick(e),
|
||||
'aria-label': config.name
|
||||
} : {
|
||||
onClick: () => handleClick(),
|
||||
onClick: (e: React.MouseEvent) => handleClick(e),
|
||||
'aria-label': config.name
|
||||
})}
|
||||
size={isActive ? 'lg' : 'md'}
|
||||
@@ -222,7 +233,7 @@ const QuickAccessBar = forwardRef<HTMLDivElement>((_, ref) => {
|
||||
<Stack gap="lg" align="center">
|
||||
{mainButtons.map((config, index) => (
|
||||
<React.Fragment key={config.id}>
|
||||
{renderNavButton(config, index)}
|
||||
{renderNavButton(config, index, config.id === 'read' || config.id === 'automate')}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</Stack>
|
||||
|
||||
@@ -16,6 +16,7 @@ import React, { useEffect, useRef, useState } from 'react';
|
||||
import { ActionIcon } from '@mantine/core';
|
||||
import ArrowBackRoundedIcon from '@mui/icons-material/ArrowBackRounded';
|
||||
import { useToolWorkflow } from '@app/contexts/ToolWorkflowContext';
|
||||
import { useNavigationState, useNavigationActions } from '@app/contexts/NavigationContext';
|
||||
import { useSidebarNavigation } from '@app/hooks/useSidebarNavigation';
|
||||
import { handleUnlessSpecialClick } from '@app/utils/clickHandlers';
|
||||
import FitText from '@app/components/shared/FitText';
|
||||
@@ -31,6 +32,8 @@ const NAV_IDS = ['read', 'sign', 'automate'];
|
||||
|
||||
const ActiveToolButton: React.FC<ActiveToolButtonProps> = ({ setActiveButton, tooltipPosition = 'right' }) => {
|
||||
const { selectedTool, selectedToolKey, leftPanelView, handleBackToTools } = useToolWorkflow();
|
||||
const { hasUnsavedChanges } = useNavigationState();
|
||||
const { actions: navigationActions } = useNavigationActions();
|
||||
const { getHomeNavigation } = useSidebarNavigation();
|
||||
|
||||
// Determine if the indicator should be visible (do not require selectedTool to be resolved yet)
|
||||
@@ -150,10 +153,16 @@ const ActiveToolButton: React.FC<ActiveToolButtonProps> = ({ setActiveButton, to
|
||||
component="a"
|
||||
href={getHomeNavigation().href}
|
||||
onClick={(e: React.MouseEvent) => {
|
||||
handleUnlessSpecialClick(e, () => {
|
||||
const performNavigation = () => {
|
||||
setActiveButton('tools');
|
||||
handleBackToTools();
|
||||
});
|
||||
};
|
||||
if (hasUnsavedChanges) {
|
||||
e.preventDefault();
|
||||
navigationActions.requestNavigation(performNavigation);
|
||||
return;
|
||||
}
|
||||
handleUnlessSpecialClick(e, performNavigation);
|
||||
}}
|
||||
size={'lg'}
|
||||
variant="subtle"
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
Title,
|
||||
Tooltip,
|
||||
} from '@mantine/core';
|
||||
import { Dropzone } from '@mantine/dropzone';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import DescriptionIcon from '@mui/icons-material/DescriptionOutlined';
|
||||
import FileDownloadIcon from '@mui/icons-material/FileDownloadOutlined';
|
||||
@@ -32,9 +33,12 @@ import CloseIcon from '@mui/icons-material/Close';
|
||||
import MergeTypeIcon from '@mui/icons-material/MergeType';
|
||||
import CallSplitIcon from '@mui/icons-material/CallSplit';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
import UploadFileIcon from '@mui/icons-material/UploadFileOutlined';
|
||||
import SaveIcon from '@mui/icons-material/SaveOutlined';
|
||||
import { Rnd } from 'react-rnd';
|
||||
import NavigationWarningModal from '@app/components/shared/NavigationWarningModal';
|
||||
|
||||
import { useFileContext } from '@app/contexts/FileContext';
|
||||
import {
|
||||
PdfTextEditorViewData,
|
||||
PdfJsonFont,
|
||||
@@ -313,6 +317,7 @@ type GroupingMode = 'auto' | 'paragraph' | 'singleLine';
|
||||
|
||||
const PdfTextEditorView = ({ data }: PdfTextEditorViewProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { activeFiles } = useFileContext();
|
||||
const [activeGroupId, setActiveGroupId] = useState<string | null>(null);
|
||||
const [editingGroupId, setEditingGroupId] = useState<string | null>(null);
|
||||
const [activeImageId, setActiveImageId] = useState<string | null>(null);
|
||||
@@ -375,6 +380,7 @@ const PdfTextEditorView = ({ data }: PdfTextEditorViewProps) => {
|
||||
fileName,
|
||||
errorMessage,
|
||||
isGeneratingPdf,
|
||||
isSavingToWorkbench,
|
||||
isConverting,
|
||||
conversionProgress,
|
||||
hasChanges,
|
||||
@@ -389,11 +395,12 @@ const PdfTextEditorView = ({ data }: PdfTextEditorViewProps) => {
|
||||
onReset,
|
||||
onDownloadJson,
|
||||
onGeneratePdf,
|
||||
onGeneratePdfForNavigation,
|
||||
onSaveToWorkbench,
|
||||
onForceSingleTextElementChange,
|
||||
onGroupingModeChange,
|
||||
onMergeGroups,
|
||||
onUngroupGroup,
|
||||
onLoadFile,
|
||||
} = data;
|
||||
|
||||
// Define derived variables immediately after props destructuring, before any hooks
|
||||
@@ -1430,7 +1437,8 @@ const selectionToolbarPosition = useMemo(() => {
|
||||
height: '100%',
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'minmax(0, 1fr) 320px',
|
||||
alignItems: 'start',
|
||||
gridTemplateRows: '1fr',
|
||||
alignItems: hasDocument ? 'start' : 'stretch',
|
||||
gap: '1.5rem',
|
||||
}}
|
||||
>
|
||||
@@ -1486,6 +1494,17 @@ const selectionToolbarPosition = useMemo(() => {
|
||||
>
|
||||
{t('pdfTextEditor.actions.generatePdf', 'Generate PDF')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="filled"
|
||||
color="green"
|
||||
leftSection={<SaveIcon fontSize="small" />}
|
||||
onClick={onSaveToWorkbench}
|
||||
loading={isSavingToWorkbench}
|
||||
disabled={!hasDocument || !hasChanges || isConverting}
|
||||
fullWidth
|
||||
>
|
||||
{t('pdfTextEditor.actions.saveChanges', 'Save Changes')}
|
||||
</Button>
|
||||
</Stack>
|
||||
|
||||
{fileName && (
|
||||
@@ -1639,17 +1658,45 @@ const selectionToolbarPosition = useMemo(() => {
|
||||
)}
|
||||
|
||||
{!hasDocument && !isConverting && (
|
||||
<Card withBorder radius="md" padding="xl" style={{ gridColumn: '1 / 2', gridRow: 1 }}>
|
||||
<Stack align="center" gap="md">
|
||||
<DescriptionIcon sx={{ fontSize: 48 }} />
|
||||
<Text size="lg" fw={600}>
|
||||
{t('pdfTextEditor.empty.title', 'No document loaded')}
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed" ta="center" maw={420}>
|
||||
{t('pdfTextEditor.empty.subtitle', 'Load a PDF or JSON file to begin editing text content.')}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Card>
|
||||
<Stack
|
||||
align="center"
|
||||
justify="center"
|
||||
style={{ gridColumn: '1 / 2', gridRow: 1, height: '100%' }}
|
||||
>
|
||||
<Dropzone
|
||||
onDrop={(files) => {
|
||||
if (files.length > 0) {
|
||||
onLoadFile(files[0]);
|
||||
}
|
||||
}}
|
||||
accept={['application/pdf', 'application/json']}
|
||||
maxFiles={1}
|
||||
style={{
|
||||
width: '100%',
|
||||
maxWidth: 480,
|
||||
minHeight: 200,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
border: '2px dashed var(--mantine-color-gray-4)',
|
||||
borderRadius: 'var(--mantine-radius-lg)',
|
||||
cursor: 'pointer',
|
||||
transition: 'border-color 150ms ease, background-color 150ms ease',
|
||||
}}
|
||||
>
|
||||
<Stack align="center" gap="md" style={{ pointerEvents: 'none' }}>
|
||||
<UploadFileIcon sx={{ fontSize: 48, color: 'var(--mantine-color-blue-5)' }} />
|
||||
<Text size="lg" fw={600}>
|
||||
{t('pdfTextEditor.empty.title', 'No document loaded')}
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed" ta="center" maw={420}>
|
||||
{activeFiles.length > 0
|
||||
? t('pdfTextEditor.empty.dropzoneWithFiles', 'Select a file from the Files tab, or drag and drop a PDF or JSON file here, or click to browse')
|
||||
: t('pdfTextEditor.empty.dropzone', 'Drag and drop a PDF or JSON file here, or click to browse')}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Dropzone>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{isConverting && (
|
||||
@@ -1683,7 +1730,7 @@ const selectionToolbarPosition = useMemo(() => {
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{hasDocument && (
|
||||
{hasDocument && !isConverting && (
|
||||
<Stack
|
||||
gap="lg"
|
||||
className="flex-1"
|
||||
@@ -2444,7 +2491,7 @@ const selectionToolbarPosition = useMemo(() => {
|
||||
|
||||
{/* Navigation Warning Modal */}
|
||||
<NavigationWarningModal
|
||||
onApplyAndContinue={onGeneratePdfForNavigation}
|
||||
onApplyAndContinue={onSaveToWorkbench}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user