Feature/v2/navigate save prompt (#4586)

# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## 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.
This commit is contained in:
Reece Browne
2025-10-02 20:14:35 +01:00
committed by GitHub
parent 8aa6aff53a
commit f9ac1bd62e
15 changed files with 396 additions and 169 deletions
+42 -5
View File
@@ -74,6 +74,8 @@ export interface NavigationContextActions {
setSelectedTool: (toolId: ToolId | null) => void;
setToolAndWorkbench: (toolId: ToolId | null, workbench: WorkbenchType) => void;
setHasUnsavedChanges: (hasChanges: boolean) => void;
registerUnsavedChangesChecker: (checker: () => boolean) => void;
unregisterUnsavedChangesChecker: () => void;
showNavigationWarning: (show: boolean) => void;
requestNavigation: (navigationFn: () => void) => void;
confirmNavigation: () => void;
@@ -106,11 +108,29 @@ export const NavigationProvider: React.FC<{
}> = ({ children }) => {
const [state, dispatch] = useReducer(navigationReducer, initialState);
const toolRegistry = useFlatToolRegistry();
const unsavedChangesCheckerRef = React.useRef<(() => boolean) | null>(null);
const actions: NavigationContextActions = {
setWorkbench: useCallback((workbench: WorkbenchType) => {
// If we're leaving pageEditor workbench and have unsaved changes, request navigation
if (state.workbench === 'pageEditor' && workbench !== 'pageEditor' && state.hasUnsavedChanges) {
// Check for unsaved changes using registered checker or state
const hasUnsavedChanges = unsavedChangesCheckerRef.current?.() || state.hasUnsavedChanges;
console.log('[NavigationContext] setWorkbench:', {
from: state.workbench,
to: workbench,
hasChecker: !!unsavedChangesCheckerRef.current,
hasUnsavedChanges
});
// If we're leaving pageEditor or viewer workbench and have unsaved changes, request navigation
const leavingWorkbenchWithChanges =
(state.workbench === 'pageEditor' && workbench !== 'pageEditor' && hasUnsavedChanges) ||
(state.workbench === 'viewer' && workbench !== 'viewer' && hasUnsavedChanges);
if (leavingWorkbenchWithChanges) {
// Update state to reflect unsaved changes so modal knows
if (!state.hasUnsavedChanges) {
dispatch({ type: 'SET_UNSAVED_CHANGES', payload: { hasChanges: true } });
}
const performWorkbenchChange = () => {
dispatch({ type: 'SET_WORKBENCH', payload: { workbench } });
};
@@ -126,8 +146,15 @@ export const NavigationProvider: React.FC<{
}, []),
setToolAndWorkbench: useCallback((toolId: ToolId | null, workbench: WorkbenchType) => {
// If we're leaving pageEditor workbench and have unsaved changes, request navigation
if (state.workbench === 'pageEditor' && workbench !== 'pageEditor' && state.hasUnsavedChanges) {
// 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
const leavingWorkbenchWithChanges =
(state.workbench === 'pageEditor' && workbench !== 'pageEditor' && hasUnsavedChanges) ||
(state.workbench === 'viewer' && workbench !== 'viewer' && hasUnsavedChanges);
if (leavingWorkbenchWithChanges) {
const performWorkbenchChange = () => {
dispatch({ type: 'SET_TOOL_AND_WORKBENCH', payload: { toolId, workbench } });
};
@@ -142,6 +169,14 @@ export const NavigationProvider: React.FC<{
dispatch({ type: 'SET_UNSAVED_CHANGES', payload: { hasChanges } });
}, []),
registerUnsavedChangesChecker: useCallback((checker: () => boolean) => {
unsavedChangesCheckerRef.current = checker;
}, []),
unregisterUnsavedChangesChecker: useCallback(() => {
unsavedChangesCheckerRef.current = null;
}, []),
showNavigationWarning: useCallback((show: boolean) => {
dispatch({ type: 'SHOW_NAVIGATION_WARNING', payload: { show } });
}, []),
@@ -254,6 +289,8 @@ export const useNavigationGuard = () => {
confirmNavigation: actions.confirmNavigation,
cancelNavigation: actions.cancelNavigation,
setHasUnsavedChanges: actions.setHasUnsavedChanges,
setShowNavigationWarning: actions.showNavigationWarning
setShowNavigationWarning: actions.showNavigationWarning,
registerUnsavedChangesChecker: actions.registerUnsavedChangesChecker,
unregisterUnsavedChangesChecker: actions.unregisterUnsavedChangesChecker
};
};
+18 -4
View File
@@ -57,13 +57,13 @@ const addFilesMutex = new SimpleMutex();
/**
* Helper to create ProcessedFile metadata structure
*/
export function createProcessedFile(pageCount: number, thumbnail?: string) {
export function createProcessedFile(pageCount: number, thumbnail?: string, pageRotations?: number[]) {
return {
totalPages: pageCount,
pages: Array.from({ length: pageCount }, (_, index) => ({
pageNumber: index + 1,
thumbnail: index === 0 ? thumbnail : undefined, // Only first page gets thumbnail initially
rotation: 0,
rotation: pageRotations?.[index] ?? 0,
splitBefore: false
})),
thumbnailUrl: thumbnail,
@@ -82,8 +82,22 @@ export async function generateProcessedFileMetadata(file: File): Promise<Process
}
try {
const result = await generateThumbnailWithMetadata(file);
return createProcessedFile(result.pageCount, result.thumbnail);
// Generate unrotated thumbnails for PageEditor (rotation applied via CSS)
const unrotatedResult = await generateThumbnailWithMetadata(file, false);
// Generate rotated thumbnail for file manager display
const rotatedResult = await generateThumbnailWithMetadata(file, true);
const processedFile = createProcessedFile(
unrotatedResult.pageCount,
unrotatedResult.thumbnail, // Page thumbnails (unrotated)
unrotatedResult.pageRotations
);
// Use rotated thumbnail for file manager
processedFile.thumbnailUrl = rotatedResult.thumbnail;
return processedFile;
} catch (error) {
if (DEBUG) console.warn(`📄 Failed to generate processedFileMetadata for ${file.name}:`, error);
}