This commit is contained in:
Anthony Stirling 2025-11-14 15:33:22 +00:00
parent 458cb7fab4
commit bd383fb1e7
6 changed files with 222 additions and 22 deletions

View File

@ -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": {

View File

@ -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 } });

View File

@ -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;
}
}

View File

@ -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(() => {
)}
</Group>
<Modal
opened={showWelcomeBanner}
onClose={handleDismissWelcomeBanner}
title={
<Group gap="xs">
<InfoOutlinedIcon fontSize="small" />
<Text fw={600}>{t('pdfTextEditor.welcomeBanner.title', 'Welcome to PDF Text Editor (Early Access)')}</Text>
</Group>
}
centered
size="lg"
>
<ScrollArea style={{ maxHeight: '70vh' }} offsetScrollbars>
<Stack gap="sm">
<Alert color="orange" variant="light" radius="md">
<Text size="sm" fw={500}>
{t('pdfTextEditor.welcomeBanner.experimental', 'This is an experimental feature in active development. Expect some instability and issues during use.')}
</Text>
</Alert>
<Text size="sm">
{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.')}
</Text>
<Divider />
<Text size="sm" fw={500} c="green.7">
{t('pdfTextEditor.welcomeBanner.bestFor', 'Works Best With:')}
</Text>
<Text size="sm" component="ul" style={{ marginLeft: '1rem', marginTop: '0.25rem' }}>
<li>{t('pdfTextEditor.welcomeBanner.bestFor1', 'Simple PDFs containing primarily text and images')}</li>
<li>{t('pdfTextEditor.welcomeBanner.bestFor2', 'Documents with standard paragraph formatting')}</li>
<li>{t('pdfTextEditor.welcomeBanner.bestFor3', 'Letters, essays, reports, and basic documents')}</li>
</Text>
<Divider />
<Text size="sm" fw={500} c="orange.7">
{t('pdfTextEditor.welcomeBanner.notIdealFor', 'Not Ideal For:')}
</Text>
<Text size="sm" component="ul" style={{ marginLeft: '1rem', marginTop: '0.25rem' }}>
<li>{t('pdfTextEditor.welcomeBanner.notIdealFor1', 'PDFs with special formatting like bullet points, tables, or multi-column layouts')}</li>
<li>{t('pdfTextEditor.welcomeBanner.notIdealFor2', 'Magazines, brochures, or heavily designed documents')}</li>
<li>{t('pdfTextEditor.welcomeBanner.notIdealFor3', 'Instruction manuals with complex layouts')}</li>
</Text>
<Divider />
<Text size="sm" fw={500}>
{t('pdfTextEditor.welcomeBanner.limitations', 'Current Limitations:')}
</Text>
<Text size="sm" component="ul" style={{ marginLeft: '1rem', marginTop: '0.25rem' }}>
<li>{t('pdfTextEditor.welcomeBanner.limitation1', 'Font rendering may differ slightly from the original PDF')}</li>
<li>{t('pdfTextEditor.welcomeBanner.limitation2', 'Complex graphics, form fields, and annotations are preserved but not editable')}</li>
<li>{t('pdfTextEditor.welcomeBanner.limitation3', 'Large files may take time to convert and process')}</li>
</Text>
<Divider />
<Text size="sm" fw={500}>
{t('pdfTextEditor.welcomeBanner.knownIssues', 'Known Issues (Being Fixed):')}
</Text>
<Text size="sm" component="ul" style={{ marginLeft: '1rem', marginTop: '0.25rem' }}>
<li>{t('pdfTextEditor.welcomeBanner.issue1', 'Text colour is not currently preserved (will be added soon)')}</li>
<li>{t('pdfTextEditor.welcomeBanner.issue2', 'Paragraph mode has more alignment and spacing issues - Single Line mode recommended')}</li>
<li>{t('pdfTextEditor.welcomeBanner.issue3', 'The preview display differs from the exported PDF - exported PDFs are closer to the original')}</li>
<li>{t('pdfTextEditor.welcomeBanner.issue4', 'Rotated text alignment may need manual adjustment')}</li>
<li>{t('pdfTextEditor.welcomeBanner.issue5', 'Transparency and layering effects may vary from original')}</li>
</Text>
<Divider />
<Text size="xs" c="dimmed">
{t('pdfTextEditor.welcomeBanner.feedback', 'This is an early access feature. Please report any issues you encounter to help us improve!')}
</Text>
<Group justify="flex-end" gap="sm" mt="md">
<Button variant="default" onClick={handleDismissWelcomeBanner}>
{t('pdfTextEditor.welcomeBanner.gotIt', 'Got it')}
</Button>
<Button onClick={handleDontShowAgain}>
{t('pdfTextEditor.welcomeBanner.dontShowAgain', "Don't show again")}
</Button>
</Group>
</Stack>
</ScrollArea>
</Modal>
<Card
withBorder
padding="md"
@ -2169,7 +2271,7 @@ const selectionToolbarPosition = useMemo(() => {
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(() => {
</Group>
</Stack>
</Modal>
{/* Navigation Warning Modal */}
<NavigationWarningModal
onApplyAndContinue={onGeneratePdfForNavigation}
/>
</Stack>
);
};

View File

@ -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<PdfJsonDocument | null>(null);
const [groupsByPage, setGroupsByPage] = useState<TextGroup[][]>([]);
@ -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<PdfTextEditorViewData | null>(null);

View File

@ -218,6 +218,7 @@ export interface PdfTextEditorViewData {
onReset: () => void;
onDownloadJson: () => void;
onGeneratePdf: () => void;
onGeneratePdfForNavigation: () => Promise<void>;
onForceSingleTextElementChange: (value: boolean) => void;
onGroupingModeChange: (value: 'auto' | 'paragraph' | 'singleLine') => void;
onMergeGroups: (pageIndex: number, groupIds: string[]) => boolean;