From bd383fb1e75ecd2cdbdc37edbe9eb835a519b160 Mon Sep 17 00:00:00 2001
From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
Date: Fri, 14 Nov 2025 15:33:22 +0000
Subject: [PATCH] tabs
---
.../public/locales/en-GB/translation.json | 41 +++++++
.../src/core/contexts/NavigationContext.tsx | 32 ++++-
.../src/core/contexts/ToolWorkflowContext.tsx | 18 ++-
.../tools/pdfTextEditor/PdfTextEditorView.tsx | 111 +++++++++++++++++-
.../tools/pdfTextEditor/PdfTextEditor.tsx | 41 +++++--
.../tools/pdfTextEditor/pdfTextEditorTypes.ts | 1 +
6 files changed, 222 insertions(+), 22 deletions(-)
diff --git a/frontend/public/locales/en-GB/translation.json b/frontend/public/locales/en-GB/translation.json
index c972fd919..c85de2119 100644
--- a/frontend/public/locales/en-GB/translation.json
+++ b/frontend/public/locales/en-GB/translation.json
@@ -4533,6 +4533,32 @@
"cancel": "Cancel",
"confirm": "Reset and Change Mode"
},
+ "welcomeBanner": {
+ "title": "Welcome to PDF Text Editor (Early Access)",
+ "experimental": "This is an experimental feature in active development. Expect some instability and issues during use.",
+ "howItWorks": "This tool converts your PDF to an editable format where you can modify text content and reposition images. Changes are saved back as a new PDF.",
+ "bestFor": "Works Best With:",
+ "bestFor1": "Simple PDFs containing primarily text and images",
+ "bestFor2": "Documents with standard paragraph formatting",
+ "bestFor3": "Letters, essays, reports, and basic documents",
+ "notIdealFor": "Not Ideal For:",
+ "notIdealFor1": "PDFs with special formatting like bullet points, tables, or multi-column layouts",
+ "notIdealFor2": "Magazines, brochures, or heavily designed documents",
+ "notIdealFor3": "Instruction manuals with complex layouts",
+ "limitations": "Current Limitations:",
+ "limitation1": "Font rendering may differ slightly from the original PDF",
+ "limitation2": "Complex graphics, form fields, and annotations are preserved but not editable",
+ "limitation3": "Large files may take time to convert and process",
+ "knownIssues": "Known Issues (Being Fixed):",
+ "issue1": "Text colour is not currently preserved (will be added soon)",
+ "issue2": "Paragraph mode has more alignment and spacing issues - Single Line mode recommended",
+ "issue3": "The preview display differs from the exported PDF - exported PDFs are closer to the original",
+ "issue4": "Rotated text alignment may need manual adjustment",
+ "issue5": "Transparency and layering effects may vary from original",
+ "feedback": "This is an early access feature. Please report any issues you encounter to help us improve!",
+ "gotIt": "Got it",
+ "dontShowAgain": "Don't show again"
+ },
"disclaimer": {
"heading": "Preview limitations",
"textFocus": "This workspace focuses on editing text and repositioning embedded images. Complex page artwork, form widgets, and layered graphics are preserved for export but are not fully editable here.",
@@ -4579,6 +4605,21 @@
"standard14": "Standard PDF Font",
"warnings": "Warnings",
"suggestions": "Notes"
+ },
+ "manual": {
+ "mergeTooltip": "Merge selected boxes into a single paragraph",
+ "merge": "Merge selection",
+ "ungroupTooltip": "Split paragraph back into separate lines",
+ "ungroup": "Ungroup selection",
+ "widthMenu": "Width options",
+ "expandWidth": "Expand to page edge",
+ "resetWidth": "Reset width",
+ "resizeHandle": "Adjust text width"
+ },
+ "options": {
+ "manualGrouping": {
+ "descriptionInline": "Tip: Hold Ctrl (Cmd) or Shift to multi-select text boxes. A floating toolbar will appear above the selection so you can merge, ungroup, or adjust widths."
+ }
}
},
"workspace": {
diff --git a/frontend/src/core/contexts/NavigationContext.tsx b/frontend/src/core/contexts/NavigationContext.tsx
index 377ee43e6..500a6db5e 100644
--- a/frontend/src/core/contexts/NavigationContext.tsx
+++ b/frontend/src/core/contexts/NavigationContext.tsx
@@ -121,10 +121,11 @@ export const NavigationProvider: React.FC<{
hasUnsavedChanges
});
- // If we're leaving pageEditor or viewer workbench and have unsaved changes, request navigation
+ // If we're leaving pageEditor, viewer, or custom workbench and have unsaved changes, request navigation
const leavingWorkbenchWithChanges =
(state.workbench === 'pageEditor' && workbench !== 'pageEditor' && hasUnsavedChanges) ||
- (state.workbench === 'viewer' && workbench !== 'viewer' && hasUnsavedChanges);
+ (state.workbench === 'viewer' && workbench !== 'viewer' && hasUnsavedChanges) ||
+ (state.workbench.startsWith('custom:') && workbench !== state.workbench && hasUnsavedChanges);
if (leavingWorkbenchWithChanges) {
// Update state to reflect unsaved changes so modal knows
@@ -132,7 +133,19 @@ export const NavigationProvider: React.FC<{
dispatch({ type: 'SET_UNSAVED_CHANGES', payload: { hasChanges: true } });
}
const performWorkbenchChange = () => {
- dispatch({ type: 'SET_WORKBENCH', payload: { workbench } });
+ // When leaving a custom workbench, clear the selected tool
+ console.log('[NavigationContext] performWorkbenchChange executing', {
+ from: state.workbench,
+ to: workbench,
+ isCustom: state.workbench.startsWith('custom:')
+ });
+ if (state.workbench.startsWith('custom:')) {
+ console.log('[NavigationContext] Clearing tool and changing workbench to:', workbench);
+ dispatch({ type: 'SET_TOOL_AND_WORKBENCH', payload: { toolId: null, workbench } });
+ } else {
+ console.log('[NavigationContext] Just changing workbench to:', workbench);
+ dispatch({ type: 'SET_WORKBENCH', payload: { workbench } });
+ }
};
dispatch({ type: 'SET_PENDING_NAVIGATION', payload: { navigationFn: performWorkbenchChange } });
dispatch({ type: 'SHOW_NAVIGATION_WARNING', payload: { show: true } });
@@ -149,10 +162,11 @@ export const NavigationProvider: React.FC<{
// Check for unsaved changes using registered checker or state
const hasUnsavedChanges = unsavedChangesCheckerRef.current?.() || state.hasUnsavedChanges;
- // If we're leaving pageEditor or viewer workbench and have unsaved changes, request navigation
+ // If we're leaving pageEditor, viewer, or custom workbench and have unsaved changes, request navigation
const leavingWorkbenchWithChanges =
(state.workbench === 'pageEditor' && workbench !== 'pageEditor' && hasUnsavedChanges) ||
- (state.workbench === 'viewer' && workbench !== 'viewer' && hasUnsavedChanges);
+ (state.workbench === 'viewer' && workbench !== 'viewer' && hasUnsavedChanges) ||
+ (state.workbench.startsWith('custom:') && workbench !== state.workbench && hasUnsavedChanges);
if (leavingWorkbenchWithChanges) {
const performWorkbenchChange = () => {
@@ -192,13 +206,19 @@ export const NavigationProvider: React.FC<{
}, [state.hasUnsavedChanges]),
confirmNavigation: useCallback(() => {
+ console.log('[NavigationContext] confirmNavigation called', {
+ hasPendingNav: !!state.pendingNavigation,
+ currentWorkbench: state.workbench,
+ currentTool: state.selectedTool
+ });
if (state.pendingNavigation) {
state.pendingNavigation();
}
dispatch({ type: 'SET_PENDING_NAVIGATION', payload: { navigationFn: null } });
dispatch({ type: 'SHOW_NAVIGATION_WARNING', payload: { show: false } });
- }, [state.pendingNavigation]),
+ console.log('[NavigationContext] confirmNavigation completed');
+ }, [state.pendingNavigation, state.workbench, state.selectedTool]),
cancelNavigation: useCallback(() => {
dispatch({ type: 'SET_PENDING_NAVIGATION', payload: { navigationFn: null } });
diff --git a/frontend/src/core/contexts/ToolWorkflowContext.tsx b/frontend/src/core/contexts/ToolWorkflowContext.tsx
index 7c657506e..8608d460b 100644
--- a/frontend/src/core/contexts/ToolWorkflowContext.tsx
+++ b/frontend/src/core/contexts/ToolWorkflowContext.tsx
@@ -218,15 +218,25 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
}, [customViewRegistry, customViewData]);
useEffect(() => {
- if (isBaseWorkbench(navigationState.workbench)) {
+ const { workbench } = navigationState;
+ if (isBaseWorkbench(workbench)) {
return;
}
- const currentCustomView = customWorkbenchViews.find(view => view.workbenchId === navigationState.workbench);
+ const currentCustomView = customWorkbenchViews.find(view => view.workbenchId === workbench);
+ const expectedWorkbench = selectedTool?.workbench;
+ const workbenchOwnedBySelectedTool = expectedWorkbench === workbench;
+
if (!currentCustomView || currentCustomView.data == null) {
+ // If the currently selected tool expects this custom workbench, allow it
+ // some time to register/populate the view instead of immediately bouncing
+ // the user back to Active Files.
+ if (workbenchOwnedBySelectedTool) {
+ return;
+ }
actions.setWorkbench(getDefaultWorkbench());
}
- }, [actions, customWorkbenchViews, navigationState.workbench]);
+ }, [actions, customWorkbenchViews, navigationState.workbench, selectedTool]);
// Persisted via PreferencesContext; no direct localStorage writes needed here
@@ -421,4 +431,4 @@ export function useToolWorkflow(): ToolWorkflowContextValue {
throw new Error('useToolWorkflow must be used within a ToolWorkflowProvider');
}
return context;
-}
\ No newline at end of file
+}
diff --git a/frontend/src/proprietary/components/tools/pdfTextEditor/PdfTextEditorView.tsx b/frontend/src/proprietary/components/tools/pdfTextEditor/PdfTextEditorView.tsx
index 72a2488fe..734a0f090 100644
--- a/frontend/src/proprietary/components/tools/pdfTextEditor/PdfTextEditorView.tsx
+++ b/frontend/src/proprietary/components/tools/pdfTextEditor/PdfTextEditorView.tsx
@@ -33,6 +33,7 @@ import MergeTypeIcon from '@mui/icons-material/MergeType';
import CallSplitIcon from '@mui/icons-material/CallSplit';
import MoreVertIcon from '@mui/icons-material/MoreVert';
import { Rnd } from 'react-rnd';
+import NavigationWarningModal from '@core/components/shared/NavigationWarningModal';
import {
PdfTextEditorViewData,
@@ -347,6 +348,30 @@ const PdfTextEditorView = ({ data }: PdfTextEditorViewProps) => {
maxWidth: number;
} | null>(null);
+ // First-time banner state
+ const [showWelcomeBanner, setShowWelcomeBanner] = useState(() => {
+ try {
+ return localStorage.getItem('pdfTextEditor.welcomeBannerDismissed') !== 'true';
+ } catch {
+ return true;
+ }
+ });
+
+ const handleDismissWelcomeBanner = useCallback(() => {
+ // Just dismiss for this session, don't save to localStorage
+ setShowWelcomeBanner(false);
+ }, []);
+
+ const handleDontShowAgain = useCallback(() => {
+ // Save to localStorage to never show again
+ try {
+ localStorage.setItem('pdfTextEditor.welcomeBannerDismissed', 'true');
+ } catch {
+ // Ignore localStorage errors
+ }
+ setShowWelcomeBanner(false);
+ }, []);
+
const {
document: pdfDocument,
groupsByPage,
@@ -373,6 +398,7 @@ const PdfTextEditorView = ({ data }: PdfTextEditorViewProps) => {
onReset,
onDownloadJson,
onGeneratePdf,
+ onGeneratePdfForNavigation,
onForceSingleTextElementChange,
onGroupingModeChange,
onMergeGroups,
@@ -1705,6 +1731,82 @@ const selectionToolbarPosition = useMemo(() => {
)}
+
+
+ {t('pdfTextEditor.welcomeBanner.title', 'Welcome to PDF Text Editor (Early Access)')}
+
+ }
+ centered
+ size="lg"
+ >
+
+
+
+
+ {t('pdfTextEditor.welcomeBanner.experimental', 'This is an experimental feature in active development. Expect some instability and issues during use.')}
+
+
+
+ {t('pdfTextEditor.welcomeBanner.howItWorks', 'This tool converts your PDF to an editable format where you can modify text content and reposition images. Changes are saved back as a new PDF.')}
+
+
+
+ {t('pdfTextEditor.welcomeBanner.bestFor', 'Works Best With:')}
+
+
+ {t('pdfTextEditor.welcomeBanner.bestFor1', 'Simple PDFs containing primarily text and images')}
+ {t('pdfTextEditor.welcomeBanner.bestFor2', 'Documents with standard paragraph formatting')}
+ {t('pdfTextEditor.welcomeBanner.bestFor3', 'Letters, essays, reports, and basic documents')}
+
+
+
+ {t('pdfTextEditor.welcomeBanner.notIdealFor', 'Not Ideal For:')}
+
+
+ {t('pdfTextEditor.welcomeBanner.notIdealFor1', 'PDFs with special formatting like bullet points, tables, or multi-column layouts')}
+ {t('pdfTextEditor.welcomeBanner.notIdealFor2', 'Magazines, brochures, or heavily designed documents')}
+ {t('pdfTextEditor.welcomeBanner.notIdealFor3', 'Instruction manuals with complex layouts')}
+
+
+
+ {t('pdfTextEditor.welcomeBanner.limitations', 'Current Limitations:')}
+
+
+ {t('pdfTextEditor.welcomeBanner.limitation1', 'Font rendering may differ slightly from the original PDF')}
+ {t('pdfTextEditor.welcomeBanner.limitation2', 'Complex graphics, form fields, and annotations are preserved but not editable')}
+ {t('pdfTextEditor.welcomeBanner.limitation3', 'Large files may take time to convert and process')}
+
+
+
+ {t('pdfTextEditor.welcomeBanner.knownIssues', 'Known Issues (Being Fixed):')}
+
+
+ {t('pdfTextEditor.welcomeBanner.issue1', 'Text colour is not currently preserved (will be added soon)')}
+ {t('pdfTextEditor.welcomeBanner.issue2', 'Paragraph mode has more alignment and spacing issues - Single Line mode recommended')}
+ {t('pdfTextEditor.welcomeBanner.issue3', 'The preview display differs from the exported PDF - exported PDFs are closer to the original')}
+ {t('pdfTextEditor.welcomeBanner.issue4', 'Rotated text alignment may need manual adjustment')}
+ {t('pdfTextEditor.welcomeBanner.issue5', 'Transparency and layering effects may vary from original')}
+
+
+
+ {t('pdfTextEditor.welcomeBanner.feedback', 'This is an early access feature. Please report any issues you encounter to help us improve!')}
+
+
+
+
+
+
+
+
+
{
width: '100%',
minHeight: '100%',
height: 'auto',
- padding: 0,
+ padding: '2px',
backgroundColor: 'rgba(255,255,255,0.95)',
color: textColor,
fontSize: `${fontSizePx}px`,
@@ -2212,7 +2314,7 @@ const selectionToolbarPosition = useMemo(() => {
style={{
width: '100%',
minHeight: '100%',
- padding: 0,
+ padding: '2px',
whiteSpace,
wordBreak,
overflowWrap,
@@ -2342,6 +2444,11 @@ const selectionToolbarPosition = useMemo(() => {
+
+ {/* Navigation Warning Modal */}
+
);
};
diff --git a/frontend/src/proprietary/tools/pdfTextEditor/PdfTextEditor.tsx b/frontend/src/proprietary/tools/pdfTextEditor/PdfTextEditor.tsx
index 4fded970e..d04d8e246 100644
--- a/frontend/src/proprietary/tools/pdfTextEditor/PdfTextEditor.tsx
+++ b/frontend/src/proprietary/tools/pdfTextEditor/PdfTextEditor.tsx
@@ -207,6 +207,7 @@ const PdfTextEditor = ({ onComplete, onError }: BaseToolProps) => {
} = useToolWorkflow();
const { actions: navigationActions } = useNavigationActions();
const navigationState = useNavigationState();
+ const { registerUnsavedChangesChecker, unregisterUnsavedChangesChecker } = navigationActions;
const [loadedDocument, setLoadedDocument] = useState(null);
const [groupsByPage, setGroupsByPage] = useState([]);
@@ -959,7 +960,7 @@ const PdfTextEditor = ({ onComplete, onError }: BaseToolProps) => {
}
}, [buildPayload, onComplete]);
- const handleGeneratePdf = useCallback(async () => {
+ const handleGeneratePdf = useCallback(async (skipComplete = false) => {
try {
setIsGeneratingPdf(true);
@@ -1053,7 +1054,7 @@ const PdfTextEditor = ({ onComplete, onError }: BaseToolProps) => {
downloadBlob(response.data, downloadName);
- if (onComplete) {
+ if (onComplete && !skipComplete) {
const pdfFile = new File([response.data], downloadName, { type: 'application/pdf' });
onComplete([pdfFile]);
}
@@ -1094,7 +1095,7 @@ const PdfTextEditor = ({ onComplete, onError }: BaseToolProps) => {
downloadBlob(response.data, downloadName);
- if (onComplete) {
+ if (onComplete && !skipComplete) {
const pdfFile = new File([response.data], downloadName, { type: 'application/pdf' });
onComplete([pdfFile]);
}
@@ -1273,6 +1274,10 @@ const PdfTextEditor = ({ onComplete, onError }: BaseToolProps) => {
onReset: handleResetEdits,
onDownloadJson: handleDownloadJson,
onGeneratePdf: handleGeneratePdf,
+ onGeneratePdfForNavigation: async () => {
+ // Generate PDF without triggering tool completion
+ await handleGeneratePdf(true);
+ },
onForceSingleTextElementChange: setForceSingleTextElement,
onGroupingModeChange: setGroupingMode,
onMergeGroups: handleMergeGroups,
@@ -1370,14 +1375,30 @@ const PdfTextEditor = ({ onComplete, onError }: BaseToolProps) => {
unregisterCustomWorkbenchView,
]);
+ // Note: Compare tool doesn't auto-force workbench, and neither should we
+ // The workbench should be set when the tool is selected via proper channels
+ // (tool registry, tool picker, etc.) - not forced here
+
+ // Keep hasChanges in a ref for the checker to access
+ const hasChangesRef = useRef(hasChanges);
useEffect(() => {
- if (
- navigationState.selectedTool === 'pdfTextEditor' &&
- navigationState.workbench !== WORKBENCH_ID
- ) {
- navigationActions.setWorkbench(WORKBENCH_ID);
- }
- }, [navigationActions, navigationState.selectedTool, navigationState.workbench]);
+ 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(null);
diff --git a/frontend/src/proprietary/tools/pdfTextEditor/pdfTextEditorTypes.ts b/frontend/src/proprietary/tools/pdfTextEditor/pdfTextEditorTypes.ts
index 46639f11a..249553716 100644
--- a/frontend/src/proprietary/tools/pdfTextEditor/pdfTextEditorTypes.ts
+++ b/frontend/src/proprietary/tools/pdfTextEditor/pdfTextEditorTypes.ts
@@ -218,6 +218,7 @@ export interface PdfTextEditorViewData {
onReset: () => void;
onDownloadJson: () => void;
onGeneratePdf: () => void;
+ onGeneratePdfForNavigation: () => Promise;
onForceSingleTextElementChange: (value: boolean) => void;
onGroupingModeChange: (value: 'auto' | 'paragraph' | 'singleLine') => void;
onMergeGroups: (pageIndex: number, groupIds: string[]) => boolean;