From 195b1472e41a736d6be17071662d17aa7eae8afb Mon Sep 17 00:00:00 2001 From: Reece Browne <74901996+reecebrowne@users.noreply.github.com> Date: Tue, 16 Dec 2025 15:52:47 +0000 Subject: [PATCH] Bug/v2/viewer annotations (#5245) Show uneditable annotations on viewer show editable annotations layer when in annotation tools (sign, add image, add text) Remove draw tool from viewer (this is replaced wholesale in an upcoming PR so it wasn't worth doing the work to ensure it worked with the new annotation layer set up_) refactoring work, mostly renaming variables we can use for all annotation based tools that had sign specific names. remove "tools" tooltip --------- Co-authored-by: Claude Sonnet 4.5 --- .../components/shared/AllToolsNavButton.tsx | 35 ++-- .../rightRail/ViewerAnnotationControls.tsx | 188 +----------------- .../core/components/viewer/EmbedPdfViewer.tsx | 19 +- .../core/components/viewer/LocalEmbedPDF.tsx | 7 +- frontend/src/core/contexts/ViewerContext.tsx | 6 - 5 files changed, 31 insertions(+), 224 deletions(-) diff --git a/frontend/src/core/components/shared/AllToolsNavButton.tsx b/frontend/src/core/components/shared/AllToolsNavButton.tsx index 1608d5bd3..3ba3bb70b 100644 --- a/frontend/src/core/components/shared/AllToolsNavButton.tsx +++ b/frontend/src/core/components/shared/AllToolsNavButton.tsx @@ -1,6 +1,5 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; -import { Tooltip } from '@app/components/shared/Tooltip'; import AppsIcon from '@mui/icons-material/AppsRounded'; import { useToolWorkflow } from '@app/contexts/ToolWorkflowContext'; import { useNavigationState, useNavigationActions } from '@app/contexts/NavigationContext'; @@ -11,13 +10,11 @@ import QuickAccessButton from '@app/components/shared/quickAccessBar/QuickAccess interface AllToolsNavButtonProps { activeButton: string; setActiveButton: (id: string) => void; - tooltipPosition?: 'left' | 'right' | 'top' | 'bottom'; } const AllToolsNavButton: React.FC = ({ activeButton, setActiveButton, - tooltipPosition = 'right' }) => { const { t } = useTranslation(); const { handleReaderToggle, handleBackToTools, selectedToolKey, leftPanelView } = useToolWorkflow(); @@ -55,26 +52,18 @@ const AllToolsNavButton: React.FC = ({ }; return ( - -
- } - label={t("quickAccess.allTools", "Tools")} - isActive={isActive} - onClick={handleNavClick} - href={navProps.href} - ariaLabel={t("quickAccess.allTools", "Tools")} - textClassName="all-tools-text" - component="a" - /> -
-
+
+ } + label={t("quickAccess.allTools", "Tools")} + isActive={isActive} + onClick={handleNavClick} + href={navProps.href} + ariaLabel={t("quickAccess.allTools", "Tools")} + textClassName="all-tools-text" + component="a" + /> +
); }; diff --git a/frontend/src/core/components/shared/rightRail/ViewerAnnotationControls.tsx b/frontend/src/core/components/shared/rightRail/ViewerAnnotationControls.tsx index cd95b90b9..0c9cfc3dd 100644 --- a/frontend/src/core/components/shared/rightRail/ViewerAnnotationControls.tsx +++ b/frontend/src/core/components/shared/rightRail/ViewerAnnotationControls.tsx @@ -1,15 +1,9 @@ -import React, { useState, useEffect } from 'react'; -import { ActionIcon, Popover } from '@mantine/core'; +import React from 'react'; +import { ActionIcon } from '@mantine/core'; import { useTranslation } from 'react-i18next'; import LocalIcon from '@app/components/shared/LocalIcon'; import { Tooltip } from '@app/components/shared/Tooltip'; import { ViewerContext } from '@app/contexts/ViewerContext'; -import { useSignature } from '@app/contexts/SignatureContext'; -import { ColorSwatchButton, ColorPicker } from '@app/components/annotation/shared/ColorPicker'; -import { useFileState, useFileContext } from '@app/contexts/FileContext'; -import { generateThumbnailWithMetadata } from '@app/utils/thumbnailUtils'; -import { createProcessedFile } from '@app/contexts/file/fileActions'; -import { createStirlingFile, createNewStirlingFileStub } from '@app/types/fileContext'; import { useNavigationState } from '@app/contexts/NavigationContext'; import { useSidebarContext } from '@app/contexts/SidebarContext'; import { useRightRailTooltipSide } from '@app/hooks/useRightRailTooltipSide'; @@ -23,32 +17,14 @@ export default function ViewerAnnotationControls({ currentView, disabled = false const { t } = useTranslation(); const { sidebarRefs } = useSidebarContext(); const { position: tooltipPosition, offset: tooltipOffset } = useRightRailTooltipSide(sidebarRefs); - const [selectedColor, setSelectedColor] = useState('#000000'); - const [isColorPickerOpen, setIsColorPickerOpen] = useState(false); - const [isHoverColorPickerOpen, setIsHoverColorPickerOpen] = useState(false); // Viewer context for PDF controls - safely handle when not available const viewerContext = React.useContext(ViewerContext); - // Signature context for accessing drawing API - const { signatureApiRef, isPlacementMode } = useSignature(); - - // File state for save functionality - const { state, selectors } = useFileState(); - const { actions: fileActions } = useFileContext(); - const activeFiles = selectors.getFiles(); - // Check if we're in sign mode const { selectedTool } = useNavigationState(); const isSignMode = selectedTool === 'sign'; - // Turn off annotation mode when switching away from viewer - useEffect(() => { - if (currentView !== 'viewer' && viewerContext?.isAnnotationMode) { - viewerContext.setAnnotationMode(false); - } - }, [currentView, viewerContext]); - // Don't show any annotation controls in sign mode if (isSignMode) { return null; @@ -65,7 +41,7 @@ export default function ViewerAnnotationControls({ currentView, disabled = false onClick={() => { viewerContext?.toggleAnnotationsVisibility(); }} - disabled={disabled || currentView !== 'viewer' || viewerContext?.isAnnotationMode || isPlacementMode} + disabled={disabled || currentView !== 'viewer'} > - - {/* Annotation Mode Toggle with Drawing Controls */} - {viewerContext?.isAnnotationMode ? ( - // When active: Show color picker on hover -
setIsHoverColorPickerOpen(true)} - onMouseLeave={() => setIsHoverColorPickerOpen(false)} - style={{ display: 'inline-flex' }} - > - setIsHoverColorPickerOpen(false)} - position="left" - withArrow - shadow="md" - offset={8} - > - - { - viewerContext?.toggleAnnotationMode(); - setIsHoverColorPickerOpen(false); // Close hover color picker when toggling off - // Deactivate drawing tool when exiting annotation mode - if (signatureApiRef?.current) { - try { - signatureApiRef.current.deactivateTools(); - } catch (error) { - console.log('Signature API not ready:', error); - } - } - }} - disabled={disabled} - aria-label="Drawing mode active" - > - - - - -
-
-
Drawing Color
- { - setIsHoverColorPickerOpen(false); // Close hover picker - setIsColorPickerOpen(true); // Open main color picker modal - }} - /> -
-
-
-
-
- ) : ( - // When inactive: Show "Draw" tooltip - - { - viewerContext?.toggleAnnotationMode(); - // Activate ink drawing tool when entering annotation mode - if (signatureApiRef?.current && currentView === 'viewer') { - try { - signatureApiRef.current.activateDrawMode(); - signatureApiRef.current.updateDrawSettings(selectedColor, 2); - } catch (error) { - console.log('Signature API not ready:', error); - } - } - }} - disabled={disabled} - aria-label={typeof t === 'function' ? t('rightRail.draw', 'Draw') : 'Draw'} - > - - - - )} - - {/* Save PDF with Annotations */} - - { - if (viewerContext?.exportActions?.saveAsCopy && currentView === 'viewer') { - try { - const pdfArrayBuffer = await viewerContext.exportActions.saveAsCopy(); - if (pdfArrayBuffer) { - // Create new File object with flattened annotations - const blob = new Blob([pdfArrayBuffer], { type: 'application/pdf' }); - - // Get the original file name or use a default - const originalFileName = activeFiles.length > 0 ? activeFiles[0].name : 'document.pdf'; - const newFile = new File([blob], originalFileName, { type: 'application/pdf' }); - - // Replace the current file in context with the saved version (exact same logic as Sign tool) - if (activeFiles.length > 0) { - // Generate thumbnail and metadata for the saved file - const thumbnailResult = await generateThumbnailWithMetadata(newFile); - const processedFileMetadata = createProcessedFile(thumbnailResult.pageCount, thumbnailResult.thumbnail); - - // Get current file info - const currentFileIds = state.files.ids; - if (currentFileIds.length > 0) { - const currentFileId = currentFileIds[0]; - const currentRecord = selectors.getStirlingFileStub(currentFileId); - - if (!currentRecord) { - console.error('No file record found for:', currentFileId); - return; - } - - // Create output stub and file (exact same as Sign tool) - const outputStub = createNewStirlingFileStub(newFile, undefined, thumbnailResult.thumbnail, processedFileMetadata); - const outputStirlingFile = createStirlingFile(newFile, outputStub.id); - - // Replace the original file with the saved version - await fileActions.consumeFiles([currentFileId], [outputStirlingFile], [outputStub]); - } - } - } - } catch (error) { - console.error('Error saving PDF:', error); - } - } - }} - disabled={disabled} - > - - - - - {/* Color Picker Modal */} - setIsColorPickerOpen(false)} - selectedColor={selectedColor} - onColorChange={(color) => { - setSelectedColor(color); - // Update drawing tool color if annotation mode is active - if (viewerContext?.isAnnotationMode && signatureApiRef?.current && currentView === 'viewer') { - try { - signatureApiRef.current.updateDrawSettings(color, 2); - } catch (error) { - console.log('Unable to update drawing settings:', error); - } - } - }} - title="Choose Drawing Color" - /> ); } diff --git a/frontend/src/core/components/viewer/EmbedPdfViewer.tsx b/frontend/src/core/components/viewer/EmbedPdfViewer.tsx index 94bff5a35..8e66f0b5c 100644 --- a/frontend/src/core/components/viewer/EmbedPdfViewer.tsx +++ b/frontend/src/core/components/viewer/EmbedPdfViewer.tsx @@ -51,6 +51,7 @@ const EmbedPdfViewerContent = ({ getScrollState, getRotationState, isAnnotationMode, + setAnnotationMode, isAnnotationsVisible, exportActions, } = useViewer(); @@ -82,15 +83,18 @@ const EmbedPdfViewerContent = ({ // Navigation guard for unsaved changes const { setHasUnsavedChanges, registerUnsavedChangesChecker, unregisterUnsavedChangesChecker } = useNavigationGuard(); - // Check if we're in signature mode OR viewer annotation mode + // Check if we're in an annotation tool const { selectedTool } = useNavigationState(); - // Tools that use the stamp/signature placement system with hover preview - const isSignatureMode = selectedTool === 'sign' || selectedTool === 'addText' || selectedTool === 'addImage'; + // Tools that require the annotation layer (Sign, Add Text, Add Image) + const isInAnnotationTool = selectedTool === 'sign' || selectedTool === 'addText' || selectedTool === 'addImage'; + + // Sync isAnnotationMode in ViewerContext with current tool + useEffect(() => { + setAnnotationMode(isInAnnotationTool); + }, [isInAnnotationTool, setAnnotationMode]); - // Enable annotations when: in sign mode, OR annotation mode is active, OR we want to show existing annotations - const shouldEnableAnnotations = isSignatureMode || isAnnotationMode || isAnnotationsVisible; const isPlacementOverlayActive = Boolean( - isSignatureMode && shouldEnableAnnotations && isPlacementMode && signatureConfig + isInAnnotationTool && isPlacementMode && signatureConfig ); // Track which file tab is active @@ -333,7 +337,8 @@ const EmbedPdfViewerContent = ({ key={currentFile && isStirlingFile(currentFile) ? currentFile.fileId : (effectiveFile.file instanceof File ? effectiveFile.file.name : effectiveFile.url)} file={effectiveFile.file} url={effectiveFile.url} - enableAnnotations={shouldEnableAnnotations} + enableAnnotations={isAnnotationMode} + showBakedAnnotations={isAnnotationsVisible} signatureApiRef={signatureApiRef as React.RefObject} historyApiRef={historyApiRef as React.RefObject} onSignatureAdded={() => { diff --git a/frontend/src/core/components/viewer/LocalEmbedPDF.tsx b/frontend/src/core/components/viewer/LocalEmbedPDF.tsx index 0b79604fd..8f20cd96f 100644 --- a/frontend/src/core/components/viewer/LocalEmbedPDF.tsx +++ b/frontend/src/core/components/viewer/LocalEmbedPDF.tsx @@ -52,12 +52,13 @@ interface LocalEmbedPDFProps { file?: File | Blob; url?: string | null; enableAnnotations?: boolean; + showBakedAnnotations?: boolean; onSignatureAdded?: (annotation: any) => void; signatureApiRef?: React.RefObject; historyApiRef?: React.RefObject; } -export function LocalEmbedPDF({ file, url, enableAnnotations = false, onSignatureAdded, signatureApiRef, historyApiRef }: LocalEmbedPDFProps) { +export function LocalEmbedPDF({ file, url, enableAnnotations = false, showBakedAnnotations = true, onSignatureAdded, signatureApiRef, historyApiRef }: LocalEmbedPDFProps) { const { t } = useTranslation(); const [pdfUrl, setPdfUrl] = useState(null); const [, setAnnotations] = useState>([]); @@ -100,7 +101,7 @@ export function LocalEmbedPDF({ file, url, enableAnnotations = false, onSignatur }), createPluginRegistration(RenderPluginPackage, { withForms: true, - withAnnotations: true, + withAnnotations: showBakedAnnotations && !enableAnnotations, // Show baked annotations only when: visibility is ON and annotation layer is OFF }), // Register interaction manager (required for zoom and selection features) @@ -166,7 +167,7 @@ export function LocalEmbedPDF({ file, url, enableAnnotations = false, onSignatur // Register print plugin for printing PDFs createPluginRegistration(PrintPluginPackage), ]; - }, [pdfUrl]); + }, [pdfUrl, enableAnnotations, showBakedAnnotations]); // Initialize the engine with the React hook - use local WASM for offline support const { engine, isLoading, error } = usePdfiumEngine({ diff --git a/frontend/src/core/contexts/ViewerContext.tsx b/frontend/src/core/contexts/ViewerContext.tsx index 9217511ef..aa2dc497b 100644 --- a/frontend/src/core/contexts/ViewerContext.tsx +++ b/frontend/src/core/contexts/ViewerContext.tsx @@ -95,7 +95,6 @@ interface ViewerContextType { // Annotation/drawing mode for viewer isAnnotationMode: boolean; setAnnotationMode: (enabled: boolean) => void; - toggleAnnotationMode: () => void; // Active file index for multi-file viewing activeFileIndex: number; @@ -230,10 +229,6 @@ export const ViewerProvider: React.FC = ({ children }) => { setIsAnnotationModeState(enabled); }; - const toggleAnnotationMode = () => { - setIsAnnotationModeState(prev => !prev); - }; - // State getters - read from bridge refs const getScrollState = (): ScrollState => { return bridgeRefs.current.scroll?.state || { currentPage: 1, totalPages: 0 }; @@ -318,7 +313,6 @@ export const ViewerProvider: React.FC = ({ children }) => { toggleAnnotationsVisibility, isAnnotationMode, setAnnotationMode, - toggleAnnotationMode, // Active file index activeFileIndex,