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
+1
-1
@@ -57,7 +57,7 @@ repositories {
|
|||||||
|
|
||||||
allprojects {
|
allprojects {
|
||||||
group = 'stirling.software'
|
group = 'stirling.software'
|
||||||
version = '2.0.3'
|
version = '2.1.0'
|
||||||
|
|
||||||
configurations.configureEach {
|
configurations.configureEach {
|
||||||
exclude group: 'commons-logging', module: 'commons-logging'
|
exclude group: 'commons-logging', module: 'commons-logging'
|
||||||
|
|||||||
@@ -5991,6 +5991,7 @@ earlyAccess = "Early Access"
|
|||||||
reset = "Reset Changes"
|
reset = "Reset Changes"
|
||||||
downloadJson = "Download JSON"
|
downloadJson = "Download JSON"
|
||||||
generatePdf = "Generate PDF"
|
generatePdf = "Generate PDF"
|
||||||
|
saveChanges = "Save Changes"
|
||||||
|
|
||||||
[pdfTextEditor.options.autoScaleText]
|
[pdfTextEditor.options.autoScaleText]
|
||||||
title = "Auto-scale text to fit boxes"
|
title = "Auto-scale text to fit boxes"
|
||||||
@@ -6028,6 +6029,8 @@ alpha = "This alpha viewer is still evolving—certain fonts, colours, transpare
|
|||||||
[pdfTextEditor.empty]
|
[pdfTextEditor.empty]
|
||||||
title = "No document loaded"
|
title = "No document loaded"
|
||||||
subtitle = "Load a PDF or JSON file to begin editing text content."
|
subtitle = "Load a PDF or JSON file to begin editing text content."
|
||||||
|
dropzone = "Drag and drop a PDF or JSON file here, or click to browse"
|
||||||
|
dropzoneWithFiles = "Select a file from the Files tab, or drag and drop a PDF or JSON file here, or click to browse"
|
||||||
|
|
||||||
[pdfTextEditor.welcomeBanner]
|
[pdfTextEditor.welcomeBanner]
|
||||||
title = "Welcome to PDF Text Editor (Early Access)"
|
title = "Welcome to PDF Text Editor (Early Access)"
|
||||||
|
|||||||
@@ -71,6 +71,20 @@ export default function Workbench() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const renderMainContent = () => {
|
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) {
|
if (activeFiles.length === 0) {
|
||||||
return (
|
return (
|
||||||
<LandingPage
|
<LandingPage
|
||||||
@@ -143,15 +157,6 @@ export default function Workbench() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
default:
|
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 />;
|
return <LandingPage />;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { Tooltip } from '@app/components/shared/Tooltip';
|
import { Tooltip } from '@app/components/shared/Tooltip';
|
||||||
import AppsIcon from '@mui/icons-material/AppsRounded';
|
import AppsIcon from '@mui/icons-material/AppsRounded';
|
||||||
import { useToolWorkflow } from '@app/contexts/ToolWorkflowContext';
|
import { useToolWorkflow } from '@app/contexts/ToolWorkflowContext';
|
||||||
|
import { useNavigationState, useNavigationActions } from '@app/contexts/NavigationContext';
|
||||||
import { useSidebarNavigation } from '@app/hooks/useSidebarNavigation';
|
import { useSidebarNavigation } from '@app/hooks/useSidebarNavigation';
|
||||||
import { handleUnlessSpecialClick } from '@app/utils/clickHandlers';
|
import { handleUnlessSpecialClick } from '@app/utils/clickHandlers';
|
||||||
|
|
||||||
@@ -20,21 +21,36 @@ const AllToolsNavButton: React.FC<AllToolsNavButtonProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { handleReaderToggle, handleBackToTools, selectedToolKey, leftPanelView } = useToolWorkflow();
|
const { handleReaderToggle, handleBackToTools, selectedToolKey, leftPanelView } = useToolWorkflow();
|
||||||
|
const { hasUnsavedChanges } = useNavigationState();
|
||||||
|
const { actions: navigationActions } = useNavigationActions();
|
||||||
const { getHomeNavigation } = useSidebarNavigation();
|
const { getHomeNavigation } = useSidebarNavigation();
|
||||||
|
|
||||||
const handleClick = () => {
|
const performNavigation = () => {
|
||||||
setActiveButton('tools');
|
setActiveButton('tools');
|
||||||
// Preserve existing behavior used in QuickAccessBar header
|
// Preserve existing behavior used in QuickAccessBar header
|
||||||
handleReaderToggle();
|
handleReaderToggle();
|
||||||
handleBackToTools();
|
handleBackToTools();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
if (hasUnsavedChanges) {
|
||||||
|
navigationActions.requestNavigation(performNavigation);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
performNavigation();
|
||||||
|
};
|
||||||
|
|
||||||
// Do not highlight All Tools when a specific tool is open (indicator is shown)
|
// Do not highlight All Tools when a specific tool is open (indicator is shown)
|
||||||
const isActive = activeButton === 'tools' && !selectedToolKey && leftPanelView === 'toolPicker';
|
const isActive = activeButton === 'tools' && !selectedToolKey && leftPanelView === 'toolPicker';
|
||||||
|
|
||||||
const navProps = getHomeNavigation();
|
const navProps = getHomeNavigation();
|
||||||
|
|
||||||
const handleNavClick = (e: React.MouseEvent) => {
|
const handleNavClick = (e: React.MouseEvent) => {
|
||||||
|
if (hasUnsavedChanges) {
|
||||||
|
e.preventDefault();
|
||||||
|
navigationActions.requestNavigation(performNavigation);
|
||||||
|
return;
|
||||||
|
}
|
||||||
handleUnlessSpecialClick(e, handleClick);
|
handleUnlessSpecialClick(e, handleClick);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ interface NavigationWarningModalProps {
|
|||||||
|
|
||||||
const NavigationWarningModal = ({ onApplyAndContinue, onExportAndContinue }: NavigationWarningModalProps) => {
|
const NavigationWarningModal = ({ onApplyAndContinue, onExportAndContinue }: NavigationWarningModalProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { showNavigationWarning, hasUnsavedChanges, cancelNavigation, confirmNavigation, setHasUnsavedChanges } =
|
const { showNavigationWarning, hasUnsavedChanges, pendingNavigation, cancelNavigation, confirmNavigation, setHasUnsavedChanges } =
|
||||||
useNavigationGuard();
|
useNavigationGuard();
|
||||||
|
|
||||||
const handleKeepWorking = () => {
|
const handleKeepWorking = () => {
|
||||||
@@ -41,7 +41,9 @@ const NavigationWarningModal = ({ onApplyAndContinue, onExportAndContinue }: Nav
|
|||||||
};
|
};
|
||||||
const BUTTON_WIDTH = "10rem";
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { useRainbowThemeContext } from "@app/components/shared/RainbowThemeProvi
|
|||||||
import { useIsOverflowing } from '@app/hooks/useIsOverflowing';
|
import { useIsOverflowing } from '@app/hooks/useIsOverflowing';
|
||||||
import { useFilesModalContext } from '@app/contexts/FilesModalContext';
|
import { useFilesModalContext } from '@app/contexts/FilesModalContext';
|
||||||
import { useToolWorkflow } from '@app/contexts/ToolWorkflowContext';
|
import { useToolWorkflow } from '@app/contexts/ToolWorkflowContext';
|
||||||
|
import { useNavigationState, useNavigationActions } from '@app/contexts/NavigationContext';
|
||||||
import { useSidebarNavigation } from '@app/hooks/useSidebarNavigation';
|
import { useSidebarNavigation } from '@app/hooks/useSidebarNavigation';
|
||||||
import { handleUnlessSpecialClick } from '@app/utils/clickHandlers';
|
import { handleUnlessSpecialClick } from '@app/utils/clickHandlers';
|
||||||
import { ButtonConfig } from '@app/types/sidebar';
|
import { ButtonConfig } from '@app/types/sidebar';
|
||||||
@@ -32,6 +33,8 @@ const QuickAccessBar = forwardRef<HTMLDivElement>((_, ref) => {
|
|||||||
const { isRainbowMode } = useRainbowThemeContext();
|
const { isRainbowMode } = useRainbowThemeContext();
|
||||||
const { openFilesModal, isFilesModalOpen } = useFilesModalContext();
|
const { openFilesModal, isFilesModalOpen } = useFilesModalContext();
|
||||||
const { handleReaderToggle, handleToolSelect, selectedToolKey, leftPanelView, toolRegistry, readerMode, resetTool } = useToolWorkflow();
|
const { handleReaderToggle, handleToolSelect, selectedToolKey, leftPanelView, toolRegistry, readerMode, resetTool } = useToolWorkflow();
|
||||||
|
const { hasUnsavedChanges } = useNavigationState();
|
||||||
|
const { actions: navigationActions } = useNavigationActions();
|
||||||
const { getToolNavigation } = useSidebarNavigation();
|
const { getToolNavigation } = useSidebarNavigation();
|
||||||
const { config } = useAppConfig();
|
const { config } = useAppConfig();
|
||||||
const licenseAlert = useLicenseAlert();
|
const licenseAlert = useLicenseAlert();
|
||||||
@@ -58,7 +61,7 @@ const QuickAccessBar = forwardRef<HTMLDivElement>((_, ref) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Helper function to render navigation buttons with URL support
|
// 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);
|
const isActive = isNavButtonActive(config, activeButton, isFilesModalOpen, configModalOpen, selectedToolKey, leftPanelView);
|
||||||
|
|
||||||
// Check if this button has URL navigation support
|
// Check if this button has URL navigation support
|
||||||
@@ -67,6 +70,14 @@ const QuickAccessBar = forwardRef<HTMLDivElement>((_, ref) => {
|
|||||||
: null;
|
: null;
|
||||||
|
|
||||||
const handleClick = (e?: React.MouseEvent) => {
|
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) {
|
if (navProps && e) {
|
||||||
handleUnlessSpecialClick(e, config.onClick);
|
handleUnlessSpecialClick(e, config.onClick);
|
||||||
} else {
|
} else {
|
||||||
@@ -89,7 +100,7 @@ const QuickAccessBar = forwardRef<HTMLDivElement>((_, ref) => {
|
|||||||
onClick: (e: React.MouseEvent) => handleClick(e),
|
onClick: (e: React.MouseEvent) => handleClick(e),
|
||||||
'aria-label': config.name
|
'aria-label': config.name
|
||||||
} : {
|
} : {
|
||||||
onClick: () => handleClick(),
|
onClick: (e: React.MouseEvent) => handleClick(e),
|
||||||
'aria-label': config.name
|
'aria-label': config.name
|
||||||
})}
|
})}
|
||||||
size={isActive ? 'lg' : 'md'}
|
size={isActive ? 'lg' : 'md'}
|
||||||
@@ -222,7 +233,7 @@ const QuickAccessBar = forwardRef<HTMLDivElement>((_, ref) => {
|
|||||||
<Stack gap="lg" align="center">
|
<Stack gap="lg" align="center">
|
||||||
{mainButtons.map((config, index) => (
|
{mainButtons.map((config, index) => (
|
||||||
<React.Fragment key={config.id}>
|
<React.Fragment key={config.id}>
|
||||||
{renderNavButton(config, index)}
|
{renderNavButton(config, index, config.id === 'read' || config.id === 'automate')}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
))}
|
))}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import React, { useEffect, useRef, useState } from 'react';
|
|||||||
import { ActionIcon } from '@mantine/core';
|
import { ActionIcon } from '@mantine/core';
|
||||||
import ArrowBackRoundedIcon from '@mui/icons-material/ArrowBackRounded';
|
import ArrowBackRoundedIcon from '@mui/icons-material/ArrowBackRounded';
|
||||||
import { useToolWorkflow } from '@app/contexts/ToolWorkflowContext';
|
import { useToolWorkflow } from '@app/contexts/ToolWorkflowContext';
|
||||||
|
import { useNavigationState, useNavigationActions } from '@app/contexts/NavigationContext';
|
||||||
import { useSidebarNavigation } from '@app/hooks/useSidebarNavigation';
|
import { useSidebarNavigation } from '@app/hooks/useSidebarNavigation';
|
||||||
import { handleUnlessSpecialClick } from '@app/utils/clickHandlers';
|
import { handleUnlessSpecialClick } from '@app/utils/clickHandlers';
|
||||||
import FitText from '@app/components/shared/FitText';
|
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 ActiveToolButton: React.FC<ActiveToolButtonProps> = ({ setActiveButton, tooltipPosition = 'right' }) => {
|
||||||
const { selectedTool, selectedToolKey, leftPanelView, handleBackToTools } = useToolWorkflow();
|
const { selectedTool, selectedToolKey, leftPanelView, handleBackToTools } = useToolWorkflow();
|
||||||
|
const { hasUnsavedChanges } = useNavigationState();
|
||||||
|
const { actions: navigationActions } = useNavigationActions();
|
||||||
const { getHomeNavigation } = useSidebarNavigation();
|
const { getHomeNavigation } = useSidebarNavigation();
|
||||||
|
|
||||||
// Determine if the indicator should be visible (do not require selectedTool to be resolved yet)
|
// 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"
|
component="a"
|
||||||
href={getHomeNavigation().href}
|
href={getHomeNavigation().href}
|
||||||
onClick={(e: React.MouseEvent) => {
|
onClick={(e: React.MouseEvent) => {
|
||||||
handleUnlessSpecialClick(e, () => {
|
const performNavigation = () => {
|
||||||
setActiveButton('tools');
|
setActiveButton('tools');
|
||||||
handleBackToTools();
|
handleBackToTools();
|
||||||
});
|
};
|
||||||
|
if (hasUnsavedChanges) {
|
||||||
|
e.preventDefault();
|
||||||
|
navigationActions.requestNavigation(performNavigation);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handleUnlessSpecialClick(e, performNavigation);
|
||||||
}}
|
}}
|
||||||
size={'lg'}
|
size={'lg'}
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
Title,
|
Title,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
|
import { Dropzone } from '@mantine/dropzone';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import DescriptionIcon from '@mui/icons-material/DescriptionOutlined';
|
import DescriptionIcon from '@mui/icons-material/DescriptionOutlined';
|
||||||
import FileDownloadIcon from '@mui/icons-material/FileDownloadOutlined';
|
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 MergeTypeIcon from '@mui/icons-material/MergeType';
|
||||||
import CallSplitIcon from '@mui/icons-material/CallSplit';
|
import CallSplitIcon from '@mui/icons-material/CallSplit';
|
||||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
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 { Rnd } from 'react-rnd';
|
||||||
import NavigationWarningModal from '@app/components/shared/NavigationWarningModal';
|
import NavigationWarningModal from '@app/components/shared/NavigationWarningModal';
|
||||||
|
|
||||||
|
import { useFileContext } from '@app/contexts/FileContext';
|
||||||
import {
|
import {
|
||||||
PdfTextEditorViewData,
|
PdfTextEditorViewData,
|
||||||
PdfJsonFont,
|
PdfJsonFont,
|
||||||
@@ -313,6 +317,7 @@ type GroupingMode = 'auto' | 'paragraph' | 'singleLine';
|
|||||||
|
|
||||||
const PdfTextEditorView = ({ data }: PdfTextEditorViewProps) => {
|
const PdfTextEditorView = ({ data }: PdfTextEditorViewProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const { activeFiles } = useFileContext();
|
||||||
const [activeGroupId, setActiveGroupId] = useState<string | null>(null);
|
const [activeGroupId, setActiveGroupId] = useState<string | null>(null);
|
||||||
const [editingGroupId, setEditingGroupId] = useState<string | null>(null);
|
const [editingGroupId, setEditingGroupId] = useState<string | null>(null);
|
||||||
const [activeImageId, setActiveImageId] = useState<string | null>(null);
|
const [activeImageId, setActiveImageId] = useState<string | null>(null);
|
||||||
@@ -375,6 +380,7 @@ const PdfTextEditorView = ({ data }: PdfTextEditorViewProps) => {
|
|||||||
fileName,
|
fileName,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
isGeneratingPdf,
|
isGeneratingPdf,
|
||||||
|
isSavingToWorkbench,
|
||||||
isConverting,
|
isConverting,
|
||||||
conversionProgress,
|
conversionProgress,
|
||||||
hasChanges,
|
hasChanges,
|
||||||
@@ -389,11 +395,12 @@ const PdfTextEditorView = ({ data }: PdfTextEditorViewProps) => {
|
|||||||
onReset,
|
onReset,
|
||||||
onDownloadJson,
|
onDownloadJson,
|
||||||
onGeneratePdf,
|
onGeneratePdf,
|
||||||
onGeneratePdfForNavigation,
|
onSaveToWorkbench,
|
||||||
onForceSingleTextElementChange,
|
onForceSingleTextElementChange,
|
||||||
onGroupingModeChange,
|
onGroupingModeChange,
|
||||||
onMergeGroups,
|
onMergeGroups,
|
||||||
onUngroupGroup,
|
onUngroupGroup,
|
||||||
|
onLoadFile,
|
||||||
} = data;
|
} = data;
|
||||||
|
|
||||||
// Define derived variables immediately after props destructuring, before any hooks
|
// Define derived variables immediately after props destructuring, before any hooks
|
||||||
@@ -1430,7 +1437,8 @@ const selectionToolbarPosition = useMemo(() => {
|
|||||||
height: '100%',
|
height: '100%',
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
gridTemplateColumns: 'minmax(0, 1fr) 320px',
|
gridTemplateColumns: 'minmax(0, 1fr) 320px',
|
||||||
alignItems: 'start',
|
gridTemplateRows: '1fr',
|
||||||
|
alignItems: hasDocument ? 'start' : 'stretch',
|
||||||
gap: '1.5rem',
|
gap: '1.5rem',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -1486,6 +1494,17 @@ const selectionToolbarPosition = useMemo(() => {
|
|||||||
>
|
>
|
||||||
{t('pdfTextEditor.actions.generatePdf', 'Generate PDF')}
|
{t('pdfTextEditor.actions.generatePdf', 'Generate PDF')}
|
||||||
</Button>
|
</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>
|
</Stack>
|
||||||
|
|
||||||
{fileName && (
|
{fileName && (
|
||||||
@@ -1639,17 +1658,45 @@ const selectionToolbarPosition = useMemo(() => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{!hasDocument && !isConverting && (
|
{!hasDocument && !isConverting && (
|
||||||
<Card withBorder radius="md" padding="xl" style={{ gridColumn: '1 / 2', gridRow: 1 }}>
|
<Stack
|
||||||
<Stack align="center" gap="md">
|
align="center"
|
||||||
<DescriptionIcon sx={{ fontSize: 48 }} />
|
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}>
|
<Text size="lg" fw={600}>
|
||||||
{t('pdfTextEditor.empty.title', 'No document loaded')}
|
{t('pdfTextEditor.empty.title', 'No document loaded')}
|
||||||
</Text>
|
</Text>
|
||||||
<Text size="sm" c="dimmed" ta="center" maw={420}>
|
<Text size="sm" c="dimmed" ta="center" maw={420}>
|
||||||
{t('pdfTextEditor.empty.subtitle', 'Load a PDF or JSON file to begin editing text content.')}
|
{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>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Card>
|
</Dropzone>
|
||||||
|
</Stack>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isConverting && (
|
{isConverting && (
|
||||||
@@ -1683,7 +1730,7 @@ const selectionToolbarPosition = useMemo(() => {
|
|||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{hasDocument && (
|
{hasDocument && !isConverting && (
|
||||||
<Stack
|
<Stack
|
||||||
gap="lg"
|
gap="lg"
|
||||||
className="flex-1"
|
className="flex-1"
|
||||||
@@ -2444,7 +2491,7 @@ const selectionToolbarPosition = useMemo(() => {
|
|||||||
|
|
||||||
{/* Navigation Warning Modal */}
|
{/* Navigation Warning Modal */}
|
||||||
<NavigationWarningModal
|
<NavigationWarningModal
|
||||||
onApplyAndContinue={onGeneratePdfForNavigation}
|
onApplyAndContinue={onSaveToWorkbench}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { createContext, useContext, useReducer, useCallback } from 'react';
|
import React, { createContext, useContext, useReducer, useCallback, useMemo } from 'react';
|
||||||
import { WorkbenchType, getDefaultWorkbench } from '@app/types/workbench';
|
import { WorkbenchType, getDefaultWorkbench } from '@app/types/workbench';
|
||||||
import { ToolId, isValidToolId } from '@app/types/toolId';
|
import { ToolId, isValidToolId } from '@app/types/toolId';
|
||||||
import { useToolRegistry } from '@app/contexts/ToolRegistryContext';
|
import { useToolRegistry } from '@app/contexts/ToolRegistryContext';
|
||||||
@@ -110,8 +110,8 @@ export const NavigationProvider: React.FC<{
|
|||||||
const { allTools: toolRegistry } = useToolRegistry();
|
const { allTools: toolRegistry } = useToolRegistry();
|
||||||
const unsavedChangesCheckerRef = React.useRef<(() => boolean) | null>(null);
|
const unsavedChangesCheckerRef = React.useRef<(() => boolean) | null>(null);
|
||||||
|
|
||||||
const actions: NavigationContextActions = {
|
// Memoize individual callbacks
|
||||||
setWorkbench: useCallback((workbench: WorkbenchType) => {
|
const setWorkbench = useCallback((workbench: WorkbenchType) => {
|
||||||
// Check for unsaved changes using registered checker or state
|
// Check for unsaved changes using registered checker or state
|
||||||
const hasUnsavedChanges = unsavedChangesCheckerRef.current?.() || state.hasUnsavedChanges;
|
const hasUnsavedChanges = unsavedChangesCheckerRef.current?.() || state.hasUnsavedChanges;
|
||||||
console.log('[NavigationContext] setWorkbench:', {
|
console.log('[NavigationContext] setWorkbench:', {
|
||||||
@@ -152,13 +152,13 @@ export const NavigationProvider: React.FC<{
|
|||||||
} else {
|
} else {
|
||||||
dispatch({ type: 'SET_WORKBENCH', payload: { workbench } });
|
dispatch({ type: 'SET_WORKBENCH', payload: { workbench } });
|
||||||
}
|
}
|
||||||
}, [state.workbench, state.hasUnsavedChanges]),
|
}, [state.workbench, state.hasUnsavedChanges]);
|
||||||
|
|
||||||
setSelectedTool: useCallback((toolId: ToolId | null) => {
|
const setSelectedTool = useCallback((toolId: ToolId | null) => {
|
||||||
dispatch({ type: 'SET_SELECTED_TOOL', payload: { toolId } });
|
dispatch({ type: 'SET_SELECTED_TOOL', payload: { toolId } });
|
||||||
}, []),
|
}, []);
|
||||||
|
|
||||||
setToolAndWorkbench: useCallback((toolId: ToolId | null, workbench: WorkbenchType) => {
|
const setToolAndWorkbench = useCallback((toolId: ToolId | null, workbench: WorkbenchType) => {
|
||||||
// Check for unsaved changes using registered checker or state
|
// Check for unsaved changes using registered checker or state
|
||||||
const hasUnsavedChanges = unsavedChangesCheckerRef.current?.() || state.hasUnsavedChanges;
|
const hasUnsavedChanges = unsavedChangesCheckerRef.current?.() || state.hasUnsavedChanges;
|
||||||
|
|
||||||
@@ -177,25 +177,25 @@ export const NavigationProvider: React.FC<{
|
|||||||
} else {
|
} else {
|
||||||
dispatch({ type: 'SET_TOOL_AND_WORKBENCH', payload: { toolId, workbench } });
|
dispatch({ type: 'SET_TOOL_AND_WORKBENCH', payload: { toolId, workbench } });
|
||||||
}
|
}
|
||||||
}, [state.workbench, state.hasUnsavedChanges]),
|
}, [state.workbench, state.hasUnsavedChanges]);
|
||||||
|
|
||||||
setHasUnsavedChanges: useCallback((hasChanges: boolean) => {
|
const setHasUnsavedChanges = useCallback((hasChanges: boolean) => {
|
||||||
dispatch({ type: 'SET_UNSAVED_CHANGES', payload: { hasChanges } });
|
dispatch({ type: 'SET_UNSAVED_CHANGES', payload: { hasChanges } });
|
||||||
}, []),
|
}, []);
|
||||||
|
|
||||||
registerUnsavedChangesChecker: useCallback((checker: () => boolean) => {
|
const registerUnsavedChangesChecker = useCallback((checker: () => boolean) => {
|
||||||
unsavedChangesCheckerRef.current = checker;
|
unsavedChangesCheckerRef.current = checker;
|
||||||
}, []),
|
}, []);
|
||||||
|
|
||||||
unregisterUnsavedChangesChecker: useCallback(() => {
|
const unregisterUnsavedChangesChecker = useCallback(() => {
|
||||||
unsavedChangesCheckerRef.current = null;
|
unsavedChangesCheckerRef.current = null;
|
||||||
}, []),
|
}, []);
|
||||||
|
|
||||||
showNavigationWarning: useCallback((show: boolean) => {
|
const showNavigationWarning = useCallback((show: boolean) => {
|
||||||
dispatch({ type: 'SHOW_NAVIGATION_WARNING', payload: { show } });
|
dispatch({ type: 'SHOW_NAVIGATION_WARNING', payload: { show } });
|
||||||
}, []),
|
}, []);
|
||||||
|
|
||||||
requestNavigation: useCallback((navigationFn: () => void) => {
|
const requestNavigation = useCallback((navigationFn: () => void) => {
|
||||||
if (!state.hasUnsavedChanges) {
|
if (!state.hasUnsavedChanges) {
|
||||||
navigationFn();
|
navigationFn();
|
||||||
return;
|
return;
|
||||||
@@ -203,9 +203,9 @@ export const NavigationProvider: React.FC<{
|
|||||||
|
|
||||||
dispatch({ type: 'SET_PENDING_NAVIGATION', payload: { navigationFn } });
|
dispatch({ type: 'SET_PENDING_NAVIGATION', payload: { navigationFn } });
|
||||||
dispatch({ type: 'SHOW_NAVIGATION_WARNING', payload: { show: true } });
|
dispatch({ type: 'SHOW_NAVIGATION_WARNING', payload: { show: true } });
|
||||||
}, [state.hasUnsavedChanges]),
|
}, [state.hasUnsavedChanges]);
|
||||||
|
|
||||||
confirmNavigation: useCallback(() => {
|
const confirmNavigation = useCallback(() => {
|
||||||
console.log('[NavigationContext] confirmNavigation called', {
|
console.log('[NavigationContext] confirmNavigation called', {
|
||||||
hasPendingNav: !!state.pendingNavigation,
|
hasPendingNav: !!state.pendingNavigation,
|
||||||
currentWorkbench: state.workbench,
|
currentWorkbench: state.workbench,
|
||||||
@@ -218,18 +218,18 @@ export const NavigationProvider: React.FC<{
|
|||||||
dispatch({ type: 'SET_PENDING_NAVIGATION', payload: { navigationFn: null } });
|
dispatch({ type: 'SET_PENDING_NAVIGATION', payload: { navigationFn: null } });
|
||||||
dispatch({ type: 'SHOW_NAVIGATION_WARNING', payload: { show: false } });
|
dispatch({ type: 'SHOW_NAVIGATION_WARNING', payload: { show: false } });
|
||||||
console.log('[NavigationContext] confirmNavigation completed');
|
console.log('[NavigationContext] confirmNavigation completed');
|
||||||
}, [state.pendingNavigation, state.workbench, state.selectedTool]),
|
}, [state.pendingNavigation, state.workbench, state.selectedTool]);
|
||||||
|
|
||||||
cancelNavigation: useCallback(() => {
|
const cancelNavigation = useCallback(() => {
|
||||||
dispatch({ type: 'SET_PENDING_NAVIGATION', payload: { navigationFn: null } });
|
dispatch({ type: 'SET_PENDING_NAVIGATION', payload: { navigationFn: null } });
|
||||||
dispatch({ type: 'SHOW_NAVIGATION_WARNING', payload: { show: false } });
|
dispatch({ type: 'SHOW_NAVIGATION_WARNING', payload: { show: false } });
|
||||||
}, []),
|
}, []);
|
||||||
|
|
||||||
clearToolSelection: useCallback(() => {
|
const clearToolSelection = useCallback(() => {
|
||||||
dispatch({ type: 'SET_TOOL_AND_WORKBENCH', payload: { toolId: null, workbench: getDefaultWorkbench() } });
|
dispatch({ type: 'SET_TOOL_AND_WORKBENCH', payload: { toolId: null, workbench: getDefaultWorkbench() } });
|
||||||
}, []),
|
}, []);
|
||||||
|
|
||||||
handleToolSelect: useCallback((toolId: string) => {
|
const handleToolSelect = useCallback((toolId: string) => {
|
||||||
if (toolId === 'allTools') {
|
if (toolId === 'allTools') {
|
||||||
dispatch({ type: 'SET_TOOL_AND_WORKBENCH', payload: { toolId: null, workbench: getDefaultWorkbench() } });
|
dispatch({ type: 'SET_TOOL_AND_WORKBENCH', payload: { toolId: null, workbench: getDefaultWorkbench() } });
|
||||||
return;
|
return;
|
||||||
@@ -248,8 +248,37 @@ export const NavigationProvider: React.FC<{
|
|||||||
// Validate toolId and convert to ToolId type
|
// Validate toolId and convert to ToolId type
|
||||||
const validToolId = isValidToolId(toolId) ? toolId : null;
|
const validToolId = isValidToolId(toolId) ? toolId : null;
|
||||||
dispatch({ type: 'SET_TOOL_AND_WORKBENCH', payload: { toolId: validToolId, workbench } });
|
dispatch({ type: 'SET_TOOL_AND_WORKBENCH', payload: { toolId: validToolId, workbench } });
|
||||||
}, [toolRegistry])
|
}, [toolRegistry]);
|
||||||
};
|
|
||||||
|
// Memoize the actions object to prevent unnecessary context updates
|
||||||
|
// This is critical to avoid infinite loops when effects depend on actions
|
||||||
|
const actions: NavigationContextActions = useMemo(() => ({
|
||||||
|
setWorkbench,
|
||||||
|
setSelectedTool,
|
||||||
|
setToolAndWorkbench,
|
||||||
|
setHasUnsavedChanges,
|
||||||
|
registerUnsavedChangesChecker,
|
||||||
|
unregisterUnsavedChangesChecker,
|
||||||
|
showNavigationWarning,
|
||||||
|
requestNavigation,
|
||||||
|
confirmNavigation,
|
||||||
|
cancelNavigation,
|
||||||
|
clearToolSelection,
|
||||||
|
handleToolSelect,
|
||||||
|
}), [
|
||||||
|
setWorkbench,
|
||||||
|
setSelectedTool,
|
||||||
|
setToolAndWorkbench,
|
||||||
|
setHasUnsavedChanges,
|
||||||
|
registerUnsavedChangesChecker,
|
||||||
|
unregisterUnsavedChangesChecker,
|
||||||
|
showNavigationWarning,
|
||||||
|
requestNavigation,
|
||||||
|
confirmNavigation,
|
||||||
|
cancelNavigation,
|
||||||
|
clearToolSelection,
|
||||||
|
handleToolSelect,
|
||||||
|
]);
|
||||||
|
|
||||||
const stateValue: NavigationContextStateValue = {
|
const stateValue: NavigationContextStateValue = {
|
||||||
workbench: state.workbench,
|
workbench: state.workbench,
|
||||||
@@ -259,9 +288,10 @@ export const NavigationProvider: React.FC<{
|
|||||||
showNavigationWarning: state.showNavigationWarning
|
showNavigationWarning: state.showNavigationWarning
|
||||||
};
|
};
|
||||||
|
|
||||||
const actionsValue: NavigationContextActionsValue = {
|
// Also memoize the context value to prevent unnecessary re-renders
|
||||||
|
const actionsValue: NavigationContextActionsValue = useMemo(() => ({
|
||||||
actions
|
actions
|
||||||
};
|
}), [actions]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NavigationStateContext.Provider value={stateValue}>
|
<NavigationStateContext.Provider value={stateValue}>
|
||||||
|
|||||||
@@ -224,11 +224,15 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (navigationState.pendingNavigation || navigationState.showNavigationWarning) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const currentCustomView = customWorkbenchViews.find(view => view.workbenchId === navigationState.workbench);
|
const currentCustomView = customWorkbenchViews.find(view => view.workbenchId === navigationState.workbench);
|
||||||
if (!currentCustomView || currentCustomView.data == null) {
|
if (!currentCustomView || currentCustomView.data == null) {
|
||||||
actions.setWorkbench(getDefaultWorkbench());
|
actions.setWorkbench(getDefaultWorkbench());
|
||||||
}
|
}
|
||||||
}, [actions, customWorkbenchViews, navigationState.workbench]);
|
}, [actions, customWorkbenchViews, navigationState.workbench, navigationState.pendingNavigation, navigationState.showNavigationWarning]);
|
||||||
|
|
||||||
// Persisted via PreferencesContext; no direct localStorage writes needed here
|
// Persisted via PreferencesContext; no direct localStorage writes needed here
|
||||||
|
|
||||||
|
|||||||
@@ -152,6 +152,23 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog {
|
|||||||
// Proprietary tools (if any)
|
// Proprietary tools (if any)
|
||||||
...proprietaryTools,
|
...proprietaryTools,
|
||||||
// Recommended Tools in order
|
// Recommended Tools in order
|
||||||
|
pdfTextEditor: {
|
||||||
|
icon: <LocalIcon icon="edit-square-outline-rounded" width="1.5rem" height="1.5rem" />,
|
||||||
|
name: t("home.pdfTextEditor.title", "PDF Text Editor"),
|
||||||
|
component: PdfTextEditor,
|
||||||
|
description: t(
|
||||||
|
"home.pdfTextEditor.desc",
|
||||||
|
"Review and edit text and images in PDFs with grouped text editing and PDF regeneration"
|
||||||
|
),
|
||||||
|
categoryId: ToolCategoryId.RECOMMENDED_TOOLS,
|
||||||
|
subcategoryId: SubcategoryId.GENERAL,
|
||||||
|
maxFiles: 1,
|
||||||
|
endpoints: ["text-editor-pdf"],
|
||||||
|
synonyms: getSynonyms(t, "pdfTextEditor"),
|
||||||
|
supportsAutomate: false,
|
||||||
|
automationSettings: null,
|
||||||
|
versionStatus: "alpha",
|
||||||
|
},
|
||||||
multiTool: {
|
multiTool: {
|
||||||
icon: <LocalIcon icon="dashboard-customize-rounded" width="1.5rem" height="1.5rem" />,
|
icon: <LocalIcon icon="dashboard-customize-rounded" width="1.5rem" height="1.5rem" />,
|
||||||
name: t("home.multiTool.title", "Multi-Tool"),
|
name: t("home.multiTool.title", "Multi-Tool"),
|
||||||
@@ -893,23 +910,6 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog {
|
|||||||
automationSettings: RedactSingleStepSettings,
|
automationSettings: RedactSingleStepSettings,
|
||||||
synonyms: getSynonyms(t, "redact")
|
synonyms: getSynonyms(t, "redact")
|
||||||
},
|
},
|
||||||
pdfTextEditor: {
|
|
||||||
icon: <LocalIcon icon="edit-square-outline-rounded" width="1.5rem" height="1.5rem" />,
|
|
||||||
name: t("home.pdfTextEditor.title", "PDF Text Editor"),
|
|
||||||
component: PdfTextEditor,
|
|
||||||
description: t(
|
|
||||||
"home.pdfTextEditor.desc",
|
|
||||||
"Review and edit text and images in PDFs with grouped text editing and PDF regeneration"
|
|
||||||
),
|
|
||||||
categoryId: ToolCategoryId.RECOMMENDED_TOOLS,
|
|
||||||
subcategoryId: SubcategoryId.GENERAL,
|
|
||||||
maxFiles: 1,
|
|
||||||
endpoints: ["text-editor-pdf"],
|
|
||||||
synonyms: getSynonyms(t, "pdfTextEditor"),
|
|
||||||
supportsAutomate: false,
|
|
||||||
automationSettings: null,
|
|
||||||
versionStatus: "alpha",
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const regularTools = {} as RegularToolRegistry;
|
const regularTools = {} as RegularToolRegistry;
|
||||||
|
|||||||
@@ -59,17 +59,21 @@ export default function HomePage() {
|
|||||||
const prevFileCountRef = useRef(activeFiles.length);
|
const prevFileCountRef = useRef(activeFiles.length);
|
||||||
|
|
||||||
// Auto-switch to viewer when going from 0 to 1 file
|
// Auto-switch to viewer when going from 0 to 1 file
|
||||||
|
// Skip this if PDF Text Editor is active - it handles its own empty state
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const prevCount = prevFileCountRef.current;
|
const prevCount = prevFileCountRef.current;
|
||||||
const currentCount = activeFiles.length;
|
const currentCount = activeFiles.length;
|
||||||
|
|
||||||
if (prevCount === 0 && currentCount === 1) {
|
if (prevCount === 0 && currentCount === 1) {
|
||||||
|
// PDF Text Editor handles its own empty state with a dropzone
|
||||||
|
if (selectedToolKey !== 'pdfTextEditor') {
|
||||||
actions.setWorkbench('viewer');
|
actions.setWorkbench('viewer');
|
||||||
setActiveFileIndex(0);
|
setActiveFileIndex(0);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
prevFileCountRef.current = currentCount;
|
prevFileCountRef.current = currentCount;
|
||||||
}, [activeFiles.length, actions, setActiveFileIndex]);
|
}, [activeFiles.length, actions, setActiveFileIndex, selectedToolKey]);
|
||||||
|
|
||||||
const brandAltText = t("home.mobile.brandAlt", "Stirling PDF logo");
|
const brandAltText = t("home.mobile.brandAlt", "Stirling PDF logo");
|
||||||
const brandIconSrc = useLogoPath();
|
const brandIconSrc = useLogoPath();
|
||||||
|
|||||||
@@ -3,9 +3,11 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import DescriptionIcon from '@mui/icons-material/DescriptionOutlined';
|
import DescriptionIcon from '@mui/icons-material/DescriptionOutlined';
|
||||||
|
|
||||||
import { useToolWorkflow } from '@app/contexts/ToolWorkflowContext';
|
import { useToolWorkflow } from '@app/contexts/ToolWorkflowContext';
|
||||||
import { useFileSelection } from '@app/contexts/FileContext';
|
import { useFileSelection, useFileManagement, useFileContext } from '@app/contexts/FileContext';
|
||||||
import { useNavigationActions, useNavigationState } from '@app/contexts/NavigationContext';
|
import { useNavigationActions, useNavigationState } from '@app/contexts/NavigationContext';
|
||||||
|
import { createStirlingFilesAndStubs } from '@app/services/fileStubHelpers';
|
||||||
import { BaseToolProps, ToolComponent } from '@app/types/tool';
|
import { BaseToolProps, ToolComponent } from '@app/types/tool';
|
||||||
|
import { getDefaultWorkbench } from '@app/types/workbench';
|
||||||
import { CONVERSION_ENDPOINTS } from '@app/constants/convertConstants';
|
import { CONVERSION_ENDPOINTS } from '@app/constants/convertConstants';
|
||||||
import apiClient from '@app/services/apiClient';
|
import apiClient from '@app/services/apiClient';
|
||||||
import { downloadBlob, downloadTextAsFile } from '@app/utils/downloadUtils';
|
import { downloadBlob, downloadTextAsFile } from '@app/utils/downloadUtils';
|
||||||
@@ -208,7 +210,8 @@ const PdfTextEditor = ({ onComplete, onError }: BaseToolProps) => {
|
|||||||
} = useToolWorkflow();
|
} = useToolWorkflow();
|
||||||
const { actions: navigationActions } = useNavigationActions();
|
const { actions: navigationActions } = useNavigationActions();
|
||||||
const navigationState = useNavigationState();
|
const navigationState = useNavigationState();
|
||||||
const { registerUnsavedChangesChecker, unregisterUnsavedChangesChecker } = navigationActions;
|
const { addFiles } = useFileManagement();
|
||||||
|
const { consumeFiles, selectors } = useFileContext();
|
||||||
|
|
||||||
const [loadedDocument, setLoadedDocument] = useState<PdfJsonDocument | null>(null);
|
const [loadedDocument, setLoadedDocument] = useState<PdfJsonDocument | null>(null);
|
||||||
const [groupsByPage, setGroupsByPage] = useState<TextGroup[][]>([]);
|
const [groupsByPage, setGroupsByPage] = useState<TextGroup[][]>([]);
|
||||||
@@ -217,6 +220,8 @@ const PdfTextEditor = ({ onComplete, onError }: BaseToolProps) => {
|
|||||||
const [fileName, setFileName] = useState('');
|
const [fileName, setFileName] = useState('');
|
||||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||||
const [isGeneratingPdf, setIsGeneratingPdf] = useState(false);
|
const [isGeneratingPdf, setIsGeneratingPdf] = useState(false);
|
||||||
|
const [isSavingToWorkbench, setIsSavingToWorkbench] = useState(false);
|
||||||
|
const [shouldNavigateAfterSave, setShouldNavigateAfterSave] = useState(false);
|
||||||
const [isConverting, setIsConverting] = useState(false);
|
const [isConverting, setIsConverting] = useState(false);
|
||||||
const [conversionProgress, setConversionProgress] = useState<ConversionProgress | null>(null);
|
const [conversionProgress, setConversionProgress] = useState<ConversionProgress | null>(null);
|
||||||
const [forceSingleTextElement, setForceSingleTextElement] = useState(true);
|
const [forceSingleTextElement, setForceSingleTextElement] = useState(true);
|
||||||
@@ -234,6 +239,7 @@ const PdfTextEditor = ({ onComplete, onError }: BaseToolProps) => {
|
|||||||
const originalGroupsRef = useRef<TextGroup[][]>([]);
|
const originalGroupsRef = useRef<TextGroup[][]>([]);
|
||||||
const imagesByPageRef = useRef<PdfJsonImageElement[][]>([]);
|
const imagesByPageRef = useRef<PdfJsonImageElement[][]>([]);
|
||||||
const autoLoadKeyRef = useRef<string | null>(null);
|
const autoLoadKeyRef = useRef<string | null>(null);
|
||||||
|
const sourceFileIdRef = useRef<string | null>(null);
|
||||||
const loadRequestIdRef = useRef(0);
|
const loadRequestIdRef = useRef(0);
|
||||||
const latestPdfRequestIdRef = useRef<number | null>(null);
|
const latestPdfRequestIdRef = useRef<number | null>(null);
|
||||||
const loadedDocumentRef = useRef<PdfJsonDocument | null>(null);
|
const loadedDocumentRef = useRef<PdfJsonDocument | null>(null);
|
||||||
@@ -279,6 +285,23 @@ const PdfTextEditor = ({ onComplete, onError }: BaseToolProps) => {
|
|||||||
);
|
);
|
||||||
const hasChanges = useMemo(() => dirtyPages.some(Boolean), [dirtyPages]);
|
const hasChanges = useMemo(() => dirtyPages.some(Boolean), [dirtyPages]);
|
||||||
const hasDocument = loadedDocument !== null;
|
const hasDocument = loadedDocument !== null;
|
||||||
|
|
||||||
|
// Sync hasChanges to navigation context so navigation guards can block
|
||||||
|
useEffect(() => {
|
||||||
|
navigationActions.setHasUnsavedChanges(hasChanges);
|
||||||
|
return () => {
|
||||||
|
navigationActions.setHasUnsavedChanges(false);
|
||||||
|
};
|
||||||
|
}, [hasChanges, navigationActions]);
|
||||||
|
|
||||||
|
// Navigate to files view AFTER the unsaved changes state is properly cleared
|
||||||
|
useEffect(() => {
|
||||||
|
if (shouldNavigateAfterSave && !navigationState.hasUnsavedChanges) {
|
||||||
|
setShouldNavigateAfterSave(false);
|
||||||
|
navigationActions.setToolAndWorkbench(null, getDefaultWorkbench());
|
||||||
|
}
|
||||||
|
}, [shouldNavigateAfterSave, navigationState.hasUnsavedChanges, navigationActions]);
|
||||||
|
|
||||||
const viewLabel = useMemo(() => t('pdfTextEditor.viewLabel', 'PDF Editor'), [t]);
|
const viewLabel = useMemo(() => t('pdfTextEditor.viewLabel', 'PDF Editor'), [t]);
|
||||||
const { selectedFiles } = useFileSelection();
|
const { selectedFiles } = useFileSelection();
|
||||||
|
|
||||||
@@ -720,6 +743,21 @@ const PdfTextEditor = ({ onComplete, onError }: BaseToolProps) => {
|
|||||||
[groupingMode, resetToDocument, t],
|
[groupingMode, resetToDocument, t],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Wrapper for loading files from the dropzone - adds to workbench first
|
||||||
|
const handleLoadFileFromDropzone = useCallback(
|
||||||
|
async (file: File) => {
|
||||||
|
// Add the file to the workbench so it appears in the file list
|
||||||
|
const addedFiles = await addFiles([file]);
|
||||||
|
// Capture the file ID for save-to-workbench functionality
|
||||||
|
if (addedFiles.length > 0 && addedFiles[0].fileId) {
|
||||||
|
sourceFileIdRef.current = addedFiles[0].fileId;
|
||||||
|
}
|
||||||
|
// Then load it into the editor
|
||||||
|
void handleLoadFile(file);
|
||||||
|
},
|
||||||
|
[addFiles, handleLoadFile],
|
||||||
|
);
|
||||||
|
|
||||||
const handleSelectPage = useCallback((pageIndex: number) => {
|
const handleSelectPage = useCallback((pageIndex: number) => {
|
||||||
setSelectedPage(pageIndex);
|
setSelectedPage(pageIndex);
|
||||||
// Trigger lazy loading for images on the selected page
|
// Trigger lazy loading for images on the selected page
|
||||||
@@ -1122,6 +1160,229 @@ const PdfTextEditor = ({ onComplete, onError }: BaseToolProps) => {
|
|||||||
t,
|
t,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// Save changes to workbench (replaces the original file with edited version)
|
||||||
|
const handleSaveToWorkbench = useCallback(async () => {
|
||||||
|
setIsSavingToWorkbench(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!sourceFileIdRef.current) {
|
||||||
|
console.warn('[PdfTextEditor] No source file ID available for save to workbench');
|
||||||
|
// Fall back to generating PDF download if no source file
|
||||||
|
await handleGeneratePdf(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parentStub = selectors.getStirlingFileStub(sourceFileIdRef.current as any);
|
||||||
|
if (!parentStub) {
|
||||||
|
console.warn('[PdfTextEditor] Could not find parent stub for save to workbench');
|
||||||
|
await handleGeneratePdf(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ensureImagesForPages = async (pageIndices: number[]) => {
|
||||||
|
const uniqueIndices = Array.from(new Set(pageIndices)).filter((index) => index >= 0);
|
||||||
|
if (uniqueIndices.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const index of uniqueIndices) {
|
||||||
|
if (!loadedImagePagesRef.current.has(index)) {
|
||||||
|
await loadImagesForPage(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxWaitTime = 15000;
|
||||||
|
const pollInterval = 150;
|
||||||
|
const startWait = Date.now();
|
||||||
|
while (Date.now() - startWait < maxWaitTime) {
|
||||||
|
const allLoaded = uniqueIndices.every(
|
||||||
|
(index) =>
|
||||||
|
loadedImagePagesRef.current.has(index) &&
|
||||||
|
imagesByPageRef.current[index] !== undefined,
|
||||||
|
);
|
||||||
|
const anyLoading = uniqueIndices.some((index) =>
|
||||||
|
loadingImagePagesRef.current.has(index),
|
||||||
|
);
|
||||||
|
if (allLoaded && !anyLoading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, pollInterval));
|
||||||
|
}
|
||||||
|
|
||||||
|
const missing = uniqueIndices.filter(
|
||||||
|
(index) => !loadedImagePagesRef.current.has(index),
|
||||||
|
);
|
||||||
|
if (missing.length > 0) {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to load images for pages ${missing.map((i) => i + 1).join(', ')}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const currentDoc = loadedDocumentRef.current;
|
||||||
|
const totalPages = currentDoc?.pages?.length ?? 0;
|
||||||
|
const currentDirtyPages = getDirtyPages(groupsByPage, imagesByPage, originalGroupsRef.current, originalImagesRef.current);
|
||||||
|
const dirtyPageIndices = currentDirtyPages
|
||||||
|
.map((isDirty, index) => (isDirty ? index : -1))
|
||||||
|
.filter((index) => index >= 0);
|
||||||
|
|
||||||
|
let pdfBlob: Blob;
|
||||||
|
let downloadName: string;
|
||||||
|
|
||||||
|
const canUseIncremental =
|
||||||
|
isLazyMode &&
|
||||||
|
cachedJobId &&
|
||||||
|
dirtyPageIndices.length > 0 &&
|
||||||
|
dirtyPageIndices.length < totalPages;
|
||||||
|
|
||||||
|
if (canUseIncremental) {
|
||||||
|
await ensureImagesForPages(dirtyPageIndices);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const payload = buildPayload();
|
||||||
|
if (!payload) {
|
||||||
|
throw new Error('Failed to build payload');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { document, filename } = payload;
|
||||||
|
const dirtyPageSet = new Set(dirtyPageIndices);
|
||||||
|
const partialPages =
|
||||||
|
document.pages?.filter((_, index) => dirtyPageSet.has(index)) ?? [];
|
||||||
|
|
||||||
|
const partialDocument: PdfJsonDocument = {
|
||||||
|
metadata: document.metadata,
|
||||||
|
xmpMetadata: document.xmpMetadata,
|
||||||
|
fonts: document.fonts,
|
||||||
|
lazyImages: true,
|
||||||
|
pages: partialPages,
|
||||||
|
};
|
||||||
|
|
||||||
|
const baseName = sanitizeBaseName(filename).replace(/-edited$/u, '');
|
||||||
|
const expectedName = `${baseName || 'document'}.pdf`;
|
||||||
|
const response = await apiClient.post(
|
||||||
|
`/api/v1/convert/pdf/text-editor/partial/${cachedJobId}?filename=${encodeURIComponent(expectedName)}`,
|
||||||
|
partialDocument,
|
||||||
|
{
|
||||||
|
responseType: 'blob',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const contentDisposition = response.headers?.['content-disposition'] ?? '';
|
||||||
|
const detectedName = getFilenameFromHeaders(contentDisposition);
|
||||||
|
downloadName = detectedName || expectedName;
|
||||||
|
pdfBlob = response.data;
|
||||||
|
} catch (incrementalError) {
|
||||||
|
console.warn(
|
||||||
|
'[handleSaveToWorkbench] Incremental export failed, falling back to full export',
|
||||||
|
incrementalError,
|
||||||
|
);
|
||||||
|
// Fall through to full export
|
||||||
|
if (isLazyMode && totalPages > 0) {
|
||||||
|
const allPageIndices = Array.from({ length: totalPages }, (_, index) => index);
|
||||||
|
await ensureImagesForPages(allPageIndices);
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = buildPayload();
|
||||||
|
if (!payload) {
|
||||||
|
throw new Error('Failed to build payload');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { document, filename } = payload;
|
||||||
|
const serialized = JSON.stringify(document);
|
||||||
|
const jsonFile = new File([serialized], filename, { type: 'application/json' });
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('fileInput', jsonFile);
|
||||||
|
const response = await apiClient.post(CONVERSION_ENDPOINTS['text-editor-pdf'], formData, {
|
||||||
|
responseType: 'blob',
|
||||||
|
});
|
||||||
|
|
||||||
|
const contentDisposition = response.headers?.['content-disposition'] ?? '';
|
||||||
|
const detectedName = getFilenameFromHeaders(contentDisposition);
|
||||||
|
const baseName = sanitizeBaseName(filename).replace(/-edited$/u, '');
|
||||||
|
downloadName = detectedName || `${baseName || 'document'}.pdf`;
|
||||||
|
pdfBlob = response.data;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (isLazyMode && totalPages > 0) {
|
||||||
|
const allPageIndices = Array.from({ length: totalPages }, (_, index) => index);
|
||||||
|
await ensureImagesForPages(allPageIndices);
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = buildPayload();
|
||||||
|
if (!payload) {
|
||||||
|
throw new Error('Failed to build payload');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { document, filename } = payload;
|
||||||
|
const serialized = JSON.stringify(document);
|
||||||
|
const jsonFile = new File([serialized], filename, { type: 'application/json' });
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('fileInput', jsonFile);
|
||||||
|
const response = await apiClient.post(CONVERSION_ENDPOINTS['text-editor-pdf'], formData, {
|
||||||
|
responseType: 'blob',
|
||||||
|
});
|
||||||
|
|
||||||
|
const contentDisposition = response.headers?.['content-disposition'] ?? '';
|
||||||
|
const detectedName = getFilenameFromHeaders(contentDisposition);
|
||||||
|
const baseName = sanitizeBaseName(filename).replace(/-edited$/u, '');
|
||||||
|
downloadName = detectedName || `${baseName || 'document'}.pdf`;
|
||||||
|
pdfBlob = response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the new PDF file
|
||||||
|
const pdfFile = new File([pdfBlob], downloadName, { type: 'application/pdf' });
|
||||||
|
|
||||||
|
// Create StirlingFile and stub for the output
|
||||||
|
const { stirlingFiles, stubs } = await createStirlingFilesAndStubs(
|
||||||
|
[pdfFile],
|
||||||
|
parentStub,
|
||||||
|
'pdfTextEditor',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Replace the original file with the edited version
|
||||||
|
await consumeFiles([sourceFileIdRef.current as any], stirlingFiles, stubs);
|
||||||
|
|
||||||
|
// Update the source file ID to point to the new file
|
||||||
|
sourceFileIdRef.current = stubs[0].id;
|
||||||
|
|
||||||
|
// Clear the unsaved changes flag - this will trigger the useEffect to navigate
|
||||||
|
// once React has processed the state update
|
||||||
|
navigationActions.setHasUnsavedChanges(false);
|
||||||
|
setErrorMessage(null);
|
||||||
|
|
||||||
|
// Set flag to trigger navigation after state update is processed
|
||||||
|
setShouldNavigateAfterSave(true);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Failed to save to workbench', error);
|
||||||
|
const message =
|
||||||
|
error?.response?.data ||
|
||||||
|
error?.message ||
|
||||||
|
t('pdfTextEditor.errors.pdfConversion', 'Unable to save changes to workbench.');
|
||||||
|
const msgString = typeof message === 'string' ? message : String(message);
|
||||||
|
setErrorMessage(msgString);
|
||||||
|
if (onError) {
|
||||||
|
onError(msgString);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setIsSavingToWorkbench(false);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
buildPayload,
|
||||||
|
cachedJobId,
|
||||||
|
consumeFiles,
|
||||||
|
groupsByPage,
|
||||||
|
handleGeneratePdf,
|
||||||
|
imagesByPage,
|
||||||
|
isLazyMode,
|
||||||
|
loadImagesForPage,
|
||||||
|
navigationActions,
|
||||||
|
onError,
|
||||||
|
selectors,
|
||||||
|
t,
|
||||||
|
]);
|
||||||
|
|
||||||
const requestPagePreview = useCallback(
|
const requestPagePreview = useCallback(
|
||||||
async (pageIndex: number, scale: number) => {
|
async (pageIndex: number, scale: number) => {
|
||||||
if (!hasVectorPreview || !pdfDocumentRef.current) {
|
if (!hasVectorPreview || !pdfDocumentRef.current) {
|
||||||
@@ -1260,6 +1521,7 @@ const PdfTextEditor = ({ onComplete, onError }: BaseToolProps) => {
|
|||||||
fileName,
|
fileName,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
isGeneratingPdf,
|
isGeneratingPdf,
|
||||||
|
isSavingToWorkbench,
|
||||||
isConverting,
|
isConverting,
|
||||||
conversionProgress,
|
conversionProgress,
|
||||||
hasChanges,
|
hasChanges,
|
||||||
@@ -1278,15 +1540,19 @@ const PdfTextEditor = ({ onComplete, onError }: BaseToolProps) => {
|
|||||||
// Generate PDF without triggering tool completion
|
// Generate PDF without triggering tool completion
|
||||||
await handleGeneratePdf(true);
|
await handleGeneratePdf(true);
|
||||||
},
|
},
|
||||||
|
onSaveToWorkbench: handleSaveToWorkbench,
|
||||||
onForceSingleTextElementChange: setForceSingleTextElement,
|
onForceSingleTextElementChange: setForceSingleTextElement,
|
||||||
onGroupingModeChange: setGroupingMode,
|
onGroupingModeChange: setGroupingMode,
|
||||||
onMergeGroups: handleMergeGroups,
|
onMergeGroups: handleMergeGroups,
|
||||||
onUngroupGroup: handleUngroupGroup,
|
onUngroupGroup: handleUngroupGroup,
|
||||||
|
onLoadFile: handleLoadFileFromDropzone,
|
||||||
}), [
|
}), [
|
||||||
handleMergeGroups,
|
handleMergeGroups,
|
||||||
handleUngroupGroup,
|
handleUngroupGroup,
|
||||||
handleImageTransform,
|
handleImageTransform,
|
||||||
|
handleSaveToWorkbench,
|
||||||
imagesByPage,
|
imagesByPage,
|
||||||
|
isSavingToWorkbench,
|
||||||
pagePreviews,
|
pagePreviews,
|
||||||
dirtyPages,
|
dirtyPages,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
@@ -1311,6 +1577,7 @@ const PdfTextEditor = ({ onComplete, onError }: BaseToolProps) => {
|
|||||||
groupingMode,
|
groupingMode,
|
||||||
requestPagePreview,
|
requestPagePreview,
|
||||||
setForceSingleTextElement,
|
setForceSingleTextElement,
|
||||||
|
handleLoadFileFromDropzone,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const latestViewDataRef = useRef<PdfTextEditorViewData>(viewData);
|
const latestViewDataRef = useRef<PdfTextEditorViewData>(viewData);
|
||||||
@@ -1326,6 +1593,7 @@ const PdfTextEditor = ({ onComplete, onError }: BaseToolProps) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedFiles.length === 0) {
|
if (selectedFiles.length === 0) {
|
||||||
autoLoadKeyRef.current = null;
|
autoLoadKeyRef.current = null;
|
||||||
|
sourceFileIdRef.current = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1344,6 +1612,8 @@ const PdfTextEditor = ({ onComplete, onError }: BaseToolProps) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
autoLoadKeyRef.current = fileKey;
|
autoLoadKeyRef.current = fileKey;
|
||||||
|
// Capture the source file ID for save-to-workbench functionality
|
||||||
|
sourceFileIdRef.current = (file as any).fileId ?? null;
|
||||||
void handleLoadFile(file);
|
void handleLoadFile(file);
|
||||||
}, [selectedFiles, navigationState.selectedTool, handleLoadFile]);
|
}, [selectedFiles, navigationState.selectedTool, handleLoadFile]);
|
||||||
|
|
||||||
@@ -1398,27 +1668,6 @@ const PdfTextEditor = ({ onComplete, onError }: BaseToolProps) => {
|
|||||||
// The workbench should be set when the tool is selected via proper channels
|
// The workbench should be set when the tool is selected via proper channels
|
||||||
// (tool registry, tool picker, etc.) - not forced here
|
// (tool registry, tool picker, etc.) - not forced here
|
||||||
|
|
||||||
// Keep hasChanges in a ref for the checker to access
|
|
||||||
const hasChangesRef = useRef(hasChanges);
|
|
||||||
useEffect(() => {
|
|
||||||
hasChangesRef.current = hasChanges;
|
|
||||||
console.log('[PdfTextEditor] hasChanges updated to:', hasChanges);
|
|
||||||
}, [hasChanges]);
|
|
||||||
|
|
||||||
// Register unsaved changes checker for navigation guard
|
|
||||||
useEffect(() => {
|
|
||||||
const checker = () => {
|
|
||||||
console.log('[PdfTextEditor] Checking unsaved changes:', hasChangesRef.current);
|
|
||||||
return hasChangesRef.current;
|
|
||||||
};
|
|
||||||
registerUnsavedChangesChecker(checker);
|
|
||||||
console.log('[PdfTextEditor] Registered unsaved changes checker');
|
|
||||||
return () => {
|
|
||||||
console.log('[PdfTextEditor] Unregistered unsaved changes checker');
|
|
||||||
unregisterUnsavedChangesChecker();
|
|
||||||
};
|
|
||||||
}, [registerUnsavedChangesChecker, unregisterUnsavedChangesChecker]);
|
|
||||||
|
|
||||||
const lastSentViewDataRef = useRef<PdfTextEditorViewData | null>(null);
|
const lastSentViewDataRef = useRef<PdfTextEditorViewData | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -221,8 +221,11 @@ export interface PdfTextEditorViewData {
|
|||||||
onDownloadJson: () => void;
|
onDownloadJson: () => void;
|
||||||
onGeneratePdf: () => void;
|
onGeneratePdf: () => void;
|
||||||
onGeneratePdfForNavigation: () => Promise<void>;
|
onGeneratePdfForNavigation: () => Promise<void>;
|
||||||
|
onSaveToWorkbench: () => Promise<void>;
|
||||||
|
isSavingToWorkbench: boolean;
|
||||||
onForceSingleTextElementChange: (value: boolean) => void;
|
onForceSingleTextElementChange: (value: boolean) => void;
|
||||||
onGroupingModeChange: (value: 'auto' | 'paragraph' | 'singleLine') => void;
|
onGroupingModeChange: (value: 'auto' | 'paragraph' | 'singleLine') => void;
|
||||||
onMergeGroups: (pageIndex: number, groupIds: string[]) => boolean;
|
onMergeGroups: (pageIndex: number, groupIds: string[]) => boolean;
|
||||||
onUngroupGroup: (pageIndex: number, groupId: string) => boolean;
|
onUngroupGroup: (pageIndex: number, groupId: string) => boolean;
|
||||||
|
onLoadFile: (file: File) => void;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user