+
diff --git a/frontend/src/components/shared/rightRail/RightRail.README.md b/frontend/src/components/shared/rightRail/RightRail.README.md
new file mode 100644
index 000000000..7506e927c
--- /dev/null
+++ b/frontend/src/components/shared/rightRail/RightRail.README.md
@@ -0,0 +1,108 @@
+# RightRail Component
+
+A dynamic vertical toolbar on the right side of the application that supports both static buttons (Undo/Redo, Save, Print, Share) and dynamic buttons registered by tools.
+
+## Structure
+
+- **Top Section**: Dynamic buttons from tools (empty when none)
+- **Middle Section**: Grid, Cut, Undo, Redo
+- **Bottom Section**: Save, Print, Share
+
+## Usage
+
+### For Tools (Recommended)
+
+```tsx
+import { useRightRailButtons } from '../hooks/useRightRailButtons';
+import PlayArrowIcon from '@mui/icons-material/PlayArrow';
+
+function MyTool() {
+ const handleAction = useCallback(() => {
+ // Your action here
+ }, []);
+
+ useRightRailButtons([
+ {
+ id: 'my-action',
+ icon:
,
+ tooltip: 'Execute Action',
+ onClick: handleAction,
+ },
+ ]);
+
+ return
My Tool
;
+}
+```
+
+### Multiple Buttons
+
+```tsx
+useRightRailButtons([
+ {
+ id: 'primary',
+ icon:
,
+ tooltip: 'Primary Action',
+ order: 1,
+ onClick: handlePrimary,
+ },
+ {
+ id: 'secondary',
+ icon:
,
+ tooltip: 'Secondary Action',
+ order: 2,
+ onClick: handleSecondary,
+ },
+]);
+```
+
+### Conditional Buttons
+
+```tsx
+useRightRailButtons([
+ // Always show
+ {
+ id: 'process',
+ icon:
,
+ tooltip: 'Process',
+ disabled: isProcessing,
+ onClick: handleProcess,
+ },
+ // Only show when condition met
+ ...(hasResults ? [{
+ id: 'export',
+ icon:
,
+ tooltip: 'Export',
+ onClick: handleExport,
+ }] : []),
+]);
+```
+
+## API
+
+### Button Config
+
+```typescript
+interface RightRailButtonWithAction {
+ id: string; // Unique identifier
+ icon: React.ReactNode; // Icon component
+ tooltip: string; // Hover tooltip
+ section?: 'top' | 'middle' | 'bottom'; // Section (default: 'top')
+ order?: number; // Sort order (default: 0)
+ disabled?: boolean; // Disabled state (default: false)
+ visible?: boolean; // Visibility (default: true)
+ onClick: () => void; // Click handler
+}
+```
+
+## Built-in Features
+
+- **Undo/Redo**: Automatically integrates with Page Editor
+- **Theme Support**: Light/dark mode with CSS variables
+- **Auto Cleanup**: Buttons unregister when tool unmounts
+
+## Best Practices
+
+- Use descriptive IDs: `'compress-optimize'`, `'ocr-process'`
+- Choose appropriate Material-UI icons
+- Keep tooltips concise: `'Compress PDF'`, `'Process with OCR'`
+- Use `useCallback` for click handlers to prevent re-registration
diff --git a/frontend/src/components/shared/rightRail/RightRail.css b/frontend/src/components/shared/rightRail/RightRail.css
new file mode 100644
index 000000000..8d01052a9
--- /dev/null
+++ b/frontend/src/components/shared/rightRail/RightRail.css
@@ -0,0 +1,127 @@
+.right-rail {
+ background-color: var(--right-rail-bg);
+ width: 3.5rem;
+ min-width: 3.5rem;
+ max-width: 3.5rem;
+ position: relative;
+ z-index: 10;
+ display: flex;
+ flex-direction: column;
+ height: 100vh;
+ border-left: 1px solid var(--border-subtle);
+}
+
+.right-rail-inner {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 0.75rem;
+ padding: 1rem 0.5rem;
+}
+
+.right-rail-section {
+ background-color: var(--right-rail-foreground);
+ border-radius: 12px;
+ padding: 0.5rem;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 0.75rem;
+}
+
+.right-rail-divider {
+ width: 2.75rem;
+ border: none;
+ border-top: 1px solid var(--tool-subcategory-rule-color);
+ margin: 0.25rem 0;
+}
+
+.right-rail-icon {
+ color: var(--right-rail-icon);
+}
+
+.right-rail-icon[aria-disabled="true"],
+.right-rail-icon[disabled] {
+ color: var(--right-rail-icon-disabled) !important;
+ background-color: transparent !important;
+}
+
+.right-rail-spacer {
+ flex: 1;
+}
+
+/* Animated grow-down slot for buttons (mirrors current-tool-slot behavior) */
+.right-rail-slot {
+ overflow: hidden;
+ max-height: 0;
+ opacity: 0;
+ transition: max-height 450ms ease-out, opacity 300ms ease-out;
+}
+
+.right-rail-enter {
+ animation: rightRailGrowDown 450ms ease-out;
+}
+
+.right-rail-exit {
+ animation: rightRailShrinkUp 450ms ease-out;
+}
+
+.right-rail-slot.visible {
+ max-height: 18rem; /* increased to fit additional controls + divider */
+ opacity: 1;
+}
+
+@keyframes rightRailGrowDown {
+ 0% {
+ max-height: 0;
+ opacity: 0;
+ }
+ 100% {
+ max-height: 18rem;
+ opacity: 1;
+ }
+}
+
+@keyframes rightRailShrinkUp {
+ 0% {
+ max-height: 18rem;
+ opacity: 1;
+ }
+ 100% {
+ max-height: 0;
+ opacity: 0;
+ }
+}
+
+/* Remove bottom margin from close icon */
+.right-rail-slot .right-rail-icon {
+ margin-bottom: 0;
+}
+
+/* Inline appear/disappear animation for page-number selector button */
+.right-rail-fade {
+ transition-property: opacity, transform, max-height, visibility;
+ transition-duration: 220ms, 220ms, 220ms, 0s;
+ transition-timing-function: ease, ease, ease, linear;
+ transition-delay: 0s, 0s, 0s, 0s;
+ transform-origin: top center;
+ overflow: hidden;
+}
+
+.right-rail-fade.enter {
+ opacity: 1;
+ transform: scale(1);
+ max-height: 3rem;
+ visibility: visible;
+}
+
+.right-rail-fade.exit {
+ opacity: 0;
+ transform: scale(0.85);
+ max-height: 0;
+ visibility: hidden;
+ /* delay visibility change so opacity/max-height can finish */
+ transition-delay: 0s, 0s, 0s, 220ms;
+ pointer-events: none;
+}
+
diff --git a/frontend/src/components/shared/tooltip/Tooltip.module.css b/frontend/src/components/shared/tooltip/Tooltip.module.css
index 46902c04b..50c242812 100644
--- a/frontend/src/components/shared/tooltip/Tooltip.module.css
+++ b/frontend/src/components/shared/tooltip/Tooltip.module.css
@@ -160,7 +160,7 @@
.tooltip-arrow-top {
top: -0.25rem;
left: 50%;
- transform: translateX(-50%) rotate(45deg);
+ transform: translateX(-50%) rotate(-135deg);
border-top: none;
border-left: none;
}
diff --git a/frontend/src/components/tools/ToolPicker.tsx b/frontend/src/components/tools/ToolPicker.tsx
index a8dbd7993..d81bf5ef0 100644
--- a/frontend/src/components/tools/ToolPicker.tsx
+++ b/frontend/src/components/tools/ToolPicker.tsx
@@ -85,7 +85,8 @@ const ToolPicker = ({ selectedToolKey, onSelect, filteredTools, isSearching = fa
overflowY: "auto",
overflowX: "hidden",
minHeight: 0,
- height: "100%"
+ height: "100%",
+ marginTop: -2
}}
className="tool-picker-scrollable"
>
@@ -109,7 +110,6 @@ const ToolPicker = ({ selectedToolKey, onSelect, filteredTools, isSearching = fa
zIndex: 2,
borderTop: `0.0625rem solid var(--tool-header-border)`,
borderBottom: `0.0625rem solid var(--tool-header-border)`,
- marginBottom: -1,
padding: "0.5rem 1rem",
fontWeight: 700,
background: "var(--tool-header-bg)",
@@ -117,7 +117,7 @@ const ToolPicker = ({ selectedToolKey, onSelect, filteredTools, isSearching = fa
cursor: "pointer",
display: "flex",
alignItems: "center",
- justifyContent: "space-between"
+ justifyContent: "space-between",
}}
onClick={() => scrollTo(quickAccessRef)}
>
diff --git a/frontend/src/contexts/RightRailContext.tsx b/frontend/src/contexts/RightRailContext.tsx
new file mode 100644
index 000000000..be3b7276c
--- /dev/null
+++ b/frontend/src/contexts/RightRailContext.tsx
@@ -0,0 +1,64 @@
+import React, { createContext, useCallback, useContext, useMemo, useState } from 'react';
+import { RightRailAction, RightRailButtonConfig } from '../types/rightRail';
+
+interface RightRailContextValue {
+ buttons: RightRailButtonConfig[];
+ actions: Record
;
+ registerButtons: (buttons: RightRailButtonConfig[]) => void;
+ unregisterButtons: (ids: string[]) => void;
+ setAction: (id: string, action: RightRailAction) => void;
+ clear: () => void;
+}
+
+const RightRailContext = createContext(undefined);
+
+export function RightRailProvider({ children }: { children: React.ReactNode }) {
+ const [buttons, setButtons] = useState([]);
+ const [actions, setActions] = useState>({});
+
+ const registerButtons = useCallback((newButtons: RightRailButtonConfig[]) => {
+ setButtons(prev => {
+ const byId = new Map(prev.map(b => [b.id, b] as const));
+ newButtons.forEach(nb => {
+ const existing = byId.get(nb.id) || ({} as RightRailButtonConfig);
+ byId.set(nb.id, { ...existing, ...nb });
+ });
+ const merged = Array.from(byId.values());
+ merged.sort((a, b) => (a.order ?? 0) - (b.order ?? 0) || a.id.localeCompare(b.id));
+ if (process.env.NODE_ENV === 'development') {
+ const ids = newButtons.map(b => b.id);
+ const dupes = ids.filter((id, idx) => ids.indexOf(id) !== idx);
+ if (dupes.length) console.warn('[RightRail] Duplicate ids in registerButtons:', dupes);
+ }
+ return merged;
+ });
+ }, []);
+
+ const unregisterButtons = useCallback((ids: string[]) => {
+ setButtons(prev => prev.filter(b => !ids.includes(b.id)));
+ setActions(prev => Object.fromEntries(Object.entries(prev).filter(([id]) => !ids.includes(id))));
+ }, []);
+
+ const setAction = useCallback((id: string, action: RightRailAction) => {
+ setActions(prev => ({ ...prev, [id]: action }));
+ }, []);
+
+ const clear = useCallback(() => {
+ setButtons([]);
+ setActions({});
+ }, []);
+
+ const value = useMemo(() => ({ buttons, actions, registerButtons, unregisterButtons, setAction, clear }), [buttons, actions, registerButtons, unregisterButtons, setAction, clear]);
+
+ return (
+
+ {children}
+
+ );
+}
+
+export function useRightRail() {
+ const ctx = useContext(RightRailContext);
+ if (!ctx) throw new Error('useRightRail must be used within RightRailProvider');
+ return ctx;
+}
diff --git a/frontend/src/hooks/useRightRailButtons.ts b/frontend/src/hooks/useRightRailButtons.ts
new file mode 100644
index 000000000..82a4e8cd5
--- /dev/null
+++ b/frontend/src/hooks/useRightRailButtons.ts
@@ -0,0 +1,46 @@
+import { useEffect, useMemo } from 'react';
+import { useRightRail } from '../contexts/RightRailContext';
+import { RightRailAction, RightRailButtonConfig } from '../types/rightRail';
+
+export interface RightRailButtonWithAction extends RightRailButtonConfig {
+ onClick: RightRailAction;
+}
+
+/**
+ * Registers one or more RightRail buttons and their actions.
+ * - Automatically registers on mount and unregisters on unmount
+ * - Updates registration when the input array reference changes
+ */
+export function useRightRailButtons(buttons: readonly RightRailButtonWithAction[]) {
+ const { registerButtons, unregisterButtons, setAction } = useRightRail();
+
+ // Memoize configs and ids to reduce churn
+ const configs: RightRailButtonConfig[] = useMemo(
+ () => buttons.map(({ onClick, ...cfg }) => cfg),
+ [buttons]
+ );
+ const ids: string[] = useMemo(() => buttons.map(b => b.id), [buttons]);
+
+ useEffect(() => {
+ if (!buttons || buttons.length === 0) return;
+
+ // DEV warnings for duplicate ids or missing handlers
+ if (process.env.NODE_ENV === 'development') {
+ const idSet = new Set();
+ buttons.forEach(b => {
+ if (!b.onClick) console.warn('[RightRail] Missing onClick for id:', b.id);
+ if (idSet.has(b.id)) console.warn('[RightRail] Duplicate id in buttons array:', b.id);
+ idSet.add(b.id);
+ });
+ }
+
+ // Register visual button configs (idempotent merge by id)
+ registerButtons(configs);
+
+ // Bind/update actions independent of registration
+ buttons.forEach(({ id, onClick }) => setAction(id, onClick));
+
+ // Cleanup unregisters by ids present in this call
+ return () => unregisterButtons(ids);
+ }, [registerButtons, unregisterButtons, setAction, configs, ids, buttons]);
+}
diff --git a/frontend/src/pages/HomePage.tsx b/frontend/src/pages/HomePage.tsx
index eeb23e83f..12c1f4d7f 100644
--- a/frontend/src/pages/HomePage.tsx
+++ b/frontend/src/pages/HomePage.tsx
@@ -9,6 +9,7 @@ import { getBaseUrl } from "../constants/app";
import ToolPanel from "../components/tools/ToolPanel";
import Workbench from "../components/layout/Workbench";
import QuickAccessBar from "../components/shared/QuickAccessBar";
+import RightRail from "../components/shared/RightRail";
import FileManager from "../components/FileManager";
@@ -46,7 +47,8 @@ export default function HomePage() {
ref={quickAccessRef} />
+
);
-}
+}
\ No newline at end of file
diff --git a/frontend/src/services/pdfExportService.ts b/frontend/src/services/pdfExportService.ts
index b0662437e..9345133b8 100644
--- a/frontend/src/services/pdfExportService.ts
+++ b/frontend/src/services/pdfExportService.ts
@@ -5,6 +5,7 @@ export interface ExportOptions {
selectedOnly?: boolean;
filename?: string;
splitDocuments?: boolean;
+ appendSuffix?: boolean; // when false, do not append _edited/_selected
}
export class PDFExportService {
@@ -16,7 +17,7 @@ export class PDFExportService {
selectedPageIds: string[] = [],
options: ExportOptions = {}
): Promise<{ blob: Blob; filename: string } | { blobs: Blob[]; filenames: string[] }> {
- const { selectedOnly = false, filename, splitDocuments = false } = options;
+ const { selectedOnly = false, filename, splitDocuments = false, appendSuffix = true } = options;
try {
// Determine which pages to export
@@ -36,7 +37,7 @@ export class PDFExportService {
return await this.createSplitDocuments(sourceDoc, pagesToExport, filename || pdfDocument.name);
} else {
const blob = await this.createSingleDocument(sourceDoc, pagesToExport);
- const exportFilename = this.generateFilename(filename || pdfDocument.name, selectedOnly);
+ const exportFilename = this.generateFilename(filename || pdfDocument.name, selectedOnly, appendSuffix);
return { blob, filename: exportFilename };
}
} catch (error) {
@@ -56,7 +57,7 @@ export class PDFExportService {
for (const page of pages) {
// Get the original page from source document
- const sourcePageIndex = page.pageNumber - 1;
+ const sourcePageIndex = this.getOriginalSourceIndex(page);
if (sourcePageIndex >= 0 && sourcePageIndex < sourceDoc.getPageCount()) {
// Copy the page
@@ -113,7 +114,7 @@ export class PDFExportService {
const newDoc = await PDFLibDocument.create();
for (const page of segmentPages) {
- const sourcePageIndex = page.pageNumber - 1;
+ const sourcePageIndex = this.getOriginalSourceIndex(page);
if (sourcePageIndex >= 0 && sourcePageIndex < sourceDoc.getPageCount()) {
const [copiedPage] = await newDoc.copyPages(sourceDoc, [sourcePageIndex]);
@@ -146,11 +147,28 @@ export class PDFExportService {
return { blobs, filenames };
}
+ /**
+ * Derive the original page index from a page's stable id.
+ * Falls back to the current pageNumber if parsing fails.
+ */
+ private getOriginalSourceIndex(page: PDFPage): number {
+ const match = page.id.match(/-page-(\d+)$/);
+ if (match) {
+ const originalNumber = parseInt(match[1], 10);
+ if (!Number.isNaN(originalNumber)) {
+ return originalNumber - 1; // zero-based index for pdf-lib
+ }
+ }
+ // Fallback to the visible page number
+ return Math.max(0, page.pageNumber - 1);
+ }
+
/**
* Generate appropriate filename for export
*/
- private generateFilename(originalName: string, selectedOnly: boolean): string {
+ private generateFilename(originalName: string, selectedOnly: boolean, appendSuffix: boolean): string {
const baseName = originalName.replace(/\.pdf$/i, '');
+ if (!appendSuffix) return `${baseName}.pdf`;
const suffix = selectedOnly ? '_selected' : '_edited';
return `${baseName}${suffix}.pdf`;
}
diff --git a/frontend/src/styles/theme.css b/frontend/src/styles/theme.css
index 634cae91c..a8efa179e 100644
--- a/frontend/src/styles/theme.css
+++ b/frontend/src/styles/theme.css
@@ -106,6 +106,12 @@
--icon-config-bg: #9CA3AF;
--icon-config-color: #FFFFFF;
+ /* RightRail (light) */
+ --right-rail-bg: #F5F6F8; /* light background */
+ --right-rail-foreground: #CDD4E1; /* panel behind custom tool icons */
+ --right-rail-icon: #4B5563; /* icon color */
+ --right-rail-icon-disabled: #CECECE;/* disabled icon */
+
/* Colors for tooltips */
--tooltip-title-bg: #DBEFFF;
--tooltip-title-color: #31528E;
@@ -234,6 +240,12 @@
--icon-inactive-bg: #2A2F36;
--icon-inactive-color: #6E7581;
+ /* RightRail (dark) */
+ --right-rail-bg: #1F2329; /* dark background */
+ --right-rail-foreground: #2A2F36; /* panel behind custom tool icons */
+ --right-rail-icon: #BCBEBF; /* icon color */
+ --right-rail-icon-disabled: #43464B;/* disabled icon */
+
/* Dark mode tooltip colors */
--tooltip-title-bg: #4B525A;
--tooltip-title-color: #fff;
diff --git a/frontend/src/types/rightRail.ts b/frontend/src/types/rightRail.ts
new file mode 100644
index 000000000..1897a7170
--- /dev/null
+++ b/frontend/src/types/rightRail.ts
@@ -0,0 +1,26 @@
+import React from 'react';
+
+export type RightRailSection = 'top' | 'middle' | 'bottom';
+
+export interface RightRailButtonConfig {
+ /** Unique id for the button, also used to bind action callbacks */
+ id: string;
+ /** Icon element to render */
+ icon: React.ReactNode;
+ /** Tooltip content (can be localized node) */
+ tooltip: React.ReactNode;
+ /** Optional ARIA label for a11y (separate from visual tooltip) */
+ ariaLabel?: string;
+ /** Optional i18n key carried by config */
+ templateKey?: string;
+ /** Visual grouping lane */
+ section?: RightRailSection;
+ /** Sorting within a section (lower first); ties broken by id */
+ order?: number;
+ /** Initial disabled state */
+ disabled?: boolean;
+ /** Initial visibility */
+ visible?: boolean;
+}
+
+export type RightRailAction = () => void;
From e6f4cfb3188f3be73dacfbec656623258db5bc05 Mon Sep 17 00:00:00 2001
From: ConnorYoh <40631091+ConnorYoh@users.noreply.github.com>
Date: Mon, 25 Aug 2025 13:10:13 +0100
Subject: [PATCH 3/3] Automate/v2/suggested (#4257)
Suggested pipelines now work
---------
Co-authored-by: Connor Yoh
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
---
.../tools/automate/useSuggestedAutomations.ts | 119 +++++++++++++++---
frontend/src/tools/Automate.tsx | 23 +++-
2 files changed, 119 insertions(+), 23 deletions(-)
diff --git a/frontend/src/hooks/tools/automate/useSuggestedAutomations.ts b/frontend/src/hooks/tools/automate/useSuggestedAutomations.ts
index bb1ed5916..9ddce1e0b 100644
--- a/frontend/src/hooks/tools/automate/useSuggestedAutomations.ts
+++ b/frontend/src/hooks/tools/automate/useSuggestedAutomations.ts
@@ -1,6 +1,9 @@
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import StarIcon from '@mui/icons-material/Star';
+import CompressIcon from '@mui/icons-material/Compress';
+import SecurityIcon from '@mui/icons-material/Security';
+import TextFieldsIcon from '@mui/icons-material/TextFields';
import { SuggestedAutomation } from '../../../types/automation';
export function useSuggestedAutomations(): SuggestedAutomation[] {
@@ -10,37 +13,119 @@ export function useSuggestedAutomations(): SuggestedAutomation[] {
const now = new Date().toISOString();
return [
{
- id: "compress-and-merge",
- name: t("automation.suggested.compressAndMerge", "Compress & Merge"),
- description: t("automation.suggested.compressAndMergeDesc", "Compress PDFs and merge them into one file"),
+ id: "compress-and-split",
+ name: t("automation.suggested.compressAndSplit", "Compress & Split"),
+ description: t("automation.suggested.compressAndSplitDesc", "Compress PDFs and split them by pages"),
operations: [
- { operation: "compress", parameters: {} },
- { operation: "merge", parameters: {} }
+ {
+ operation: "compress",
+ parameters: {
+ compressionLevel: 5,
+ grayscale: false,
+ expectedSize: '',
+ compressionMethod: 'quality',
+ fileSizeValue: '',
+ fileSizeUnit: 'MB',
+ }
+ },
+ {
+ operation: "splitPdf",
+ parameters: {
+ mode: 'bySizeOrCount',
+ pages: '1',
+ hDiv: '2',
+ vDiv: '2',
+ merge: false,
+ splitType: 'pages',
+ splitValue: '1',
+ bookmarkLevel: '1',
+ includeMetadata: false,
+ allowDuplicates: false,
+ }
+ }
],
createdAt: now,
updatedAt: now,
- icon: StarIcon,
+ icon: CompressIcon,
},
{
- id: "ocr-and-convert",
- name: t("automation.suggested.ocrAndConvert", "OCR & Convert"),
- description: t("automation.suggested.ocrAndConvertDesc", "Extract text via OCR and convert to different format"),
+ id: "ocr-workflow",
+ name: t("automation.suggested.ocrWorkflow", "OCR Processing"),
+ description: t("automation.suggested.ocrWorkflowDesc", "Extract text from PDFs using OCR technology"),
operations: [
- { operation: "ocr", parameters: {} },
- { operation: "convert", parameters: {} }
+ {
+ operation: "ocr",
+ parameters: {
+ languages: ['eng'],
+ ocrType: 'skip-text',
+ ocrRenderType: 'hocr',
+ additionalOptions: [],
+ }
+ }
],
createdAt: now,
updatedAt: now,
- icon: StarIcon,
+ icon: TextFieldsIcon,
},
{
id: "secure-workflow",
- name: t("automation.suggested.secureWorkflow", "Secure Workflow"),
- description: t("automation.suggested.secureWorkflowDesc", "Sanitize, add password, and set permissions"),
+ name: t("automation.suggested.secureWorkflow", "Security Workflow"),
+ description: t("automation.suggested.secureWorkflowDesc", "Sanitize PDFs and add password protection"),
operations: [
- { operation: "sanitize", parameters: {} },
- { operation: "addPassword", parameters: {} },
- { operation: "changePermissions", parameters: {} }
+ {
+ operation: "sanitize",
+ parameters: {
+ removeJavaScript: true,
+ removeEmbeddedFiles: true,
+ removeXMPMetadata: false,
+ removeMetadata: false,
+ removeLinks: false,
+ removeFonts: false,
+ }
+ },
+ {
+ operation: "addPassword",
+ parameters: {
+ password: 'password',
+ ownerPassword: '',
+ keyLength: 128,
+ permissions: {
+ preventAssembly: false,
+ preventExtractContent: false,
+ preventExtractForAccessibility: false,
+ preventFillInForm: false,
+ preventModify: false,
+ preventModifyAnnotations: false,
+ preventPrinting: false,
+ preventPrintingFaithful: false,
+ }
+ }
+ }
+ ],
+ createdAt: now,
+ updatedAt: now,
+ icon: SecurityIcon,
+ },
+ {
+ id: "optimization-workflow",
+ name: t("automation.suggested.optimizationWorkflow", "Optimization Workflow"),
+ description: t("automation.suggested.optimizationWorkflowDesc", "Repair and compress PDFs for better performance"),
+ operations: [
+ {
+ operation: "repair",
+ parameters: {}
+ },
+ {
+ operation: "compress",
+ parameters: {
+ compressionLevel: 7,
+ grayscale: false,
+ expectedSize: '',
+ compressionMethod: 'quality',
+ fileSizeValue: '',
+ fileSizeUnit: 'MB',
+ }
+ }
],
createdAt: now,
updatedAt: now,
diff --git a/frontend/src/tools/Automate.tsx b/frontend/src/tools/Automate.tsx
index 54538781b..af6b3d411 100644
--- a/frontend/src/tools/Automate.tsx
+++ b/frontend/src/tools/Automate.tsx
@@ -33,13 +33,19 @@ const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
if (currentStep === AUTOMATION_STEPS.RUN && data.step !== AUTOMATION_STEPS.RUN) {
automateOperation.resetResults();
}
-
+
+ // If navigating to selection step, always clear results
+ if (data.step === AUTOMATION_STEPS.SELECTION) {
+ automateOperation.resetResults();
+ automateOperation.clearError();
+ }
+
// If navigating to run step with a different automation, reset results
- if (data.step === AUTOMATION_STEPS.RUN && data.automation &&
+ if (data.step === AUTOMATION_STEPS.RUN && data.automation &&
stepData.automation && data.automation.id !== stepData.automation.id) {
automateOperation.resetResults();
}
-
+
setStepData(data);
setCurrentStep(data.step);
};
@@ -47,7 +53,7 @@ const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
const handleComplete = () => {
// Reset automation results when completing
automateOperation.resetResults();
-
+
// Reset to selection step
setCurrentStep(AUTOMATION_STEPS.SELECTION);
setStepData({ step: AUTOMATION_STEPS.SELECTION });
@@ -127,7 +133,12 @@ const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
createStep(t('automate.selection.title', 'Automation Selection'), {
isVisible: true,
isCollapsed: currentStep !== AUTOMATION_STEPS.SELECTION,
- onCollapsedClick: () => setCurrentStep(AUTOMATION_STEPS.SELECTION)
+ onCollapsedClick: () => {
+ // Clear results when clicking back to selection
+ automateOperation.resetResults();
+ setCurrentStep(AUTOMATION_STEPS.SELECTION);
+ setStepData({ step: AUTOMATION_STEPS.SELECTION });
+ }
}, currentStep === AUTOMATION_STEPS.SELECTION ? renderCurrentStep() : null),
createStep(stepData.mode === AutomationMode.EDIT
@@ -158,7 +169,7 @@ const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
},
steps: automationSteps,
review: {
- isVisible: hasResults,
+ isVisible: hasResults && currentStep === AUTOMATION_STEPS.RUN,
operation: automateOperation,
title: t('automate.reviewTitle', 'Automation Results')
}