move save button to left sidebar, allow the view to switch away from the viewer while annotations tool is selected

This commit is contained in:
EthanHealy01 2025-12-18 15:35:54 +00:00
parent d036444af1
commit 786252e6f8
6 changed files with 67 additions and 43 deletions

View File

@ -307,7 +307,19 @@ export const AnnotationAPIBridge = forwardRef<AnnotationAPI>(function Annotation
},
getSelectedAnnotation: () => {
const api = annotationApi as AnnotationApiSurface | undefined;
return api?.getSelectedAnnotation?.() ?? null;
if (!api?.getSelectedAnnotation) {
return null;
}
try {
return api.getSelectedAnnotation();
} catch (error) {
// Some EmbedPDF builds expose getSelectedAnnotation with an internal
// `this`/state dependency (e.g. reading `selectedUid` from undefined).
// If that happens, fail gracefully and treat it as "no selection"
// instead of crashing the entire annotations tool.
console.error('[AnnotationAPIBridge] getSelectedAnnotation failed:', error);
return null;
}
},
deselectAnnotation: () => {
const api = annotationApi as AnnotationApiSurface | undefined;

View File

@ -250,7 +250,7 @@ const EmbedPdfViewerContent = ({
unsubscribe();
}
};
}, [historyApiRef, setHasUnsavedChanges]);
}, [historyApiRef.current, setHasUnsavedChanges]);
// Register checker for unsaved changes (annotations only for now)
useEffect(() => {
@ -303,10 +303,20 @@ const EmbedPdfViewerContent = ({
}
}, [currentFile, activeFileIds, exportActions, actions, selectors, setHasUnsavedChanges]);
// Register viewer right-rail buttons (including optional Save for annotations)
useViewerRightRailButtons({
onSaveAnnotations: applyChanges,
});
// Expose annotation apply via a global event so tools (like Annotate) can
// trigger saves from the left sidebar without tight coupling.
useEffect(() => {
const handler = () => {
void applyChanges();
};
window.addEventListener('stirling-annotations-apply', handler);
return () => {
window.removeEventListener('stirling-annotations-apply', handler);
};
}, [applyChanges]);
// Register viewer right-rail buttons
useViewerRightRailButtons();
const sidebarWidthRem = 15;
const totalRightMargin =

View File

@ -1,4 +1,4 @@
import { useMemo, useState, useEffect, useCallback, useRef } from 'react';
import { useMemo, useState, useEffect, useCallback } from 'react';
import { ActionIcon, Popover } from '@mantine/core';
import { useTranslation } from 'react-i18next';
import { useViewer } from '@app/contexts/ViewerContext';
@ -13,15 +13,7 @@ import { useToolWorkflow } from '@app/contexts/ToolWorkflowContext';
import { useNavigationState } from '@app/contexts/NavigationContext';
import { BASE_PATH, withBasePath } from '@app/constants/app';
interface ViewerRightRailButtonsOptions {
/**
* Optional handler to save annotation changes to a new PDF version.
* When provided, a Save button will be shown in the viewer right rail.
*/
onSaveAnnotations?: () => void | Promise<void>;
}
export function useViewerRightRailButtons(options?: ViewerRightRailButtonsOptions) {
export function useViewerRightRailButtons() {
const { t, i18n } = useTranslation();
const viewer = useViewer();
const [isPanning, setIsPanning] = useState<boolean>(() => viewer.getPanState()?.isPanning ?? false);
@ -30,14 +22,6 @@ export function useViewerRightRailButtons(options?: ViewerRightRailButtonsOption
const { handleToolSelect } = useToolWorkflow();
const { selectedTool } = useNavigationState();
// Keep the latest save handler in a ref to avoid re-registering right-rail
// buttons on every render when the callback identity changes.
const saveAnnotationsRef = useRef<(() => void | Promise<void>) | undefined>(undefined);
useEffect(() => {
saveAnnotationsRef.current = options?.onSaveAnnotations;
}, [options?.onSaveAnnotations]);
const stripBasePath = useCallback((path: string) => {
if (BASE_PATH && path.startsWith(BASE_PATH)) {
return path.slice(BASE_PATH.length) || '/';
@ -239,23 +223,6 @@ export function useViewerRightRailButtons(options?: ViewerRightRailButtonsOption
// Optional: Save button for annotations (always registered when this hook is used
// with a save handler; uses a ref to avoid infinite re-registration loops).
buttons.push({
id: 'viewer-save-annotations',
icon: <LocalIcon icon="save" width="1.5rem" height="1.5rem" />,
tooltip: saveChangesLabel,
ariaLabel: saveChangesLabel,
section: 'top' as const,
order: 59,
disabled: !canExport,
visible: true,
onClick: () => {
const handler = saveAnnotationsRef.current;
if (handler) {
void handler();
}
},
});
return buttons;
}, [
t,

View File

@ -38,7 +38,7 @@ const isKnownAnnotationTool = (toolId: string | undefined | null): toolId is Ann
const Annotate = (_props: BaseToolProps) => {
const { t } = useTranslation();
const { selectedTool, workbench } = useNavigation();
const { selectedTool, workbench, hasUnsavedChanges } = useNavigation();
const { selectedFiles } = useFileSelection();
const {
signatureApiRef,
@ -143,6 +143,10 @@ const Annotate = (_props: BaseToolProps) => {
setTextAlignment,
} = styleActions;
const handleApplyChanges = useCallback(() => {
window.dispatchEvent(new CustomEvent('stirling-annotations-apply'));
}, []);
useEffect(() => {
const isAnnotateActive = workbench === 'viewer' && selectedTool === 'annotate';
if (wasAnnotateActiveRef.current && !isAnnotateActive) {
@ -371,6 +375,8 @@ const Annotate = (_props: BaseToolProps) => {
undo={undo}
redo={redo}
historyAvailability={historyAvailability}
onApplyChanges={handleApplyChanges}
applyDisabled={!hasUnsavedChanges}
/>
),
},

View File

@ -102,6 +102,8 @@ interface AnnotationPanelProps {
undo: () => void;
redo: () => void;
historyAvailability: { canUndo: boolean; canRedo: boolean };
onApplyChanges: () => void;
applyDisabled: boolean;
}
// AnnotationPanel component extracted from Annotate.tsx to keep the main file smaller.
@ -136,6 +138,8 @@ export function AnnotationPanel(props: AnnotationPanelProps) {
undo,
redo,
historyAvailability,
onApplyChanges,
applyDisabled,
} = props;
const {
@ -1291,6 +1295,19 @@ export function AnnotationPanel(props: AnnotationPanelProps) {
{colorPickerComponent}
<Button
fullWidth
size="md"
radius="md"
mt="sm"
variant="filled"
color="blue"
disabled={applyDisabled}
onClick={onApplyChanges}
>
{t('annotation.applyChanges', 'Apply Changes')}
</Button>
<SuggestedToolsSection />
</Stack>
);

View File

@ -261,7 +261,19 @@ export function useAnnotationSelection({
if (!api) return;
const checkSelection = () => {
const ann = api.getSelectedAnnotation?.();
let ann: any = null;
if (typeof api.getSelectedAnnotation === 'function') {
try {
ann = api.getSelectedAnnotation();
} catch (error) {
// Some builds of the annotation plugin can throw when reading
// internal selection state (e.g., accessing `selectedUid` on
// an undefined object). Treat this as "no current selection"
// instead of crashing the annotations tool.
console.error('[useAnnotationSelection] getSelectedAnnotation failed:', error);
ann = null;
}
}
const currentId = ann?.object?.id ?? ann?.id ?? null;
if (currentId !== selectedAnnIdRef.current) {
applySelectionFromAnnotation(ann ?? null);