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;