From 95b3e222296c917896149353412c55eae4568e57 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Tue, 26 Aug 2025 11:19:15 +0100 Subject: [PATCH] Automation tolltip + new operations + copy to saved (#4292) # Description of Changes --- ## 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. --------- Co-authored-by: ConnorYoh <40631091+ConnorYoh@users.noreply.github.com> --- .../public/locales/en-GB/translation.json | 16 ++- .../public/locales/en-US/translation.json | 15 ++ .../tools/automate/AutomationEntry.tsx | 78 ++++++++++- .../tools/automate/AutomationSelection.tsx | 10 +- .../tools/automate/useSavedAutomations.ts | 24 +++- .../tools/automate/useSuggestedAutomations.ts | 132 ++++++++++++------ frontend/src/tools/Automate.tsx | 10 +- 7 files changed, 238 insertions(+), 47 deletions(-) diff --git a/frontend/public/locales/en-GB/translation.json b/frontend/public/locales/en-GB/translation.json index 712441a81..261b8dbc7 100644 --- a/frontend/public/locales/en-GB/translation.json +++ b/frontend/public/locales/en-GB/translation.json @@ -2279,6 +2279,20 @@ "description": "Configure the settings for this tool. These settings will be applied when the automation runs.", "cancel": "Cancel", "save": "Save Configuration" - } + }, + "copyToSaved": "Copy to Saved" } + }, + "automation": { + "suggested": { + "securePdfIngestion": "Secure PDF Ingestion", + "securePdfIngestionDesc": "Comprehensive PDF processing workflow that sanitises documents, applies OCR with cleanup, converts to PDF/A format for long-term archival, and optimises file size.", + "emailPreparation": "Email Preparation", + "emailPreparationDesc": "Optimises PDFs for email distribution by compressing files, splitting large documents into 20MB chunks for email compatibility, and removing metadata for privacy.", + "secureWorkflow": "Security Workflow", + "secureWorkflowDesc": "Secures PDF documents by removing potentially malicious content like JavaScript and embedded files, then adds password protection to prevent unauthorised access.", + "processImages": "Process Images", + "processImagesDesc": "Converts multiple image files into a single PDF document, then applies OCR technology to extract searchable text from the images." + } + } } diff --git a/frontend/public/locales/en-US/translation.json b/frontend/public/locales/en-US/translation.json index 26c2e5b15..ab5b66802 100644 --- a/frontend/public/locales/en-US/translation.json +++ b/frontend/public/locales/en-US/translation.json @@ -2106,5 +2106,20 @@ "results": { "title": "Decrypted PDFs" } + }, + "automation": { + "suggested": { + "securePdfIngestion": "Secure PDF Ingestion", + "securePdfIngestionDesc": "Comprehensive PDF processing workflow that sanitizes documents, applies OCR with cleanup, converts to PDF/A format for long-term archival, and optimizes file size.", + "emailPreparation": "Email Preparation", + "emailPreparationDesc": "Optimizes PDFs for email distribution by compressing files, splitting large documents into 20MB chunks for email compatibility, and removing metadata for privacy.", + "secureWorkflow": "Security Workflow", + "secureWorkflowDesc": "Secures PDF documents by removing potentially malicious content like JavaScript and embedded files, then adds password protection to prevent unauthorized access.", + "processImages": "Process Images", + "processImagesDesc": "Converts multiple image files into a single PDF document, then applies OCR technology to extract searchable text from the images." + } + }, + "automate": { + "copyToSaved": "Copy to Saved" } } diff --git a/frontend/src/components/tools/automate/AutomationEntry.tsx b/frontend/src/components/tools/automate/AutomationEntry.tsx index 3314831be..8c07fb14b 100644 --- a/frontend/src/components/tools/automate/AutomationEntry.tsx +++ b/frontend/src/components/tools/automate/AutomationEntry.tsx @@ -4,10 +4,14 @@ import { Button, Group, Text, ActionIcon, Menu, Box } from '@mantine/core'; import MoreVertIcon from '@mui/icons-material/MoreVert'; import EditIcon from '@mui/icons-material/Edit'; import DeleteIcon from '@mui/icons-material/Delete'; +import ContentCopyIcon from '@mui/icons-material/ContentCopy'; +import { Tooltip } from '../../shared/Tooltip'; interface AutomationEntryProps { /** Optional title for the automation (usually for custom ones) */ title?: string; + /** Optional description for tooltip */ + description?: string; /** MUI Icon component for the badge */ badgeIcon?: React.ComponentType; /** Array of tool operation names in the workflow */ @@ -22,17 +26,21 @@ interface AutomationEntryProps { onEdit?: () => void; /** Delete handler */ onDelete?: () => void; + /** Copy handler (for suggested automations) */ + onCopy?: () => void; } export default function AutomationEntry({ title, + description, badgeIcon: BadgeIcon, operations, onClick, keepIconColor = false, showMenu = false, onEdit, - onDelete + onDelete, + onCopy }: AutomationEntryProps) { const { t } = useTranslation(); const [isHovered, setIsHovered] = useState(false); @@ -41,6 +49,47 @@ export default function AutomationEntry({ // Keep item in hovered state if menu is open const shouldShowHovered = isHovered || isMenuOpen; + // Create tooltip content with description and tool chain + const createTooltipContent = () => { + if (!description) return null; + + const toolChain = operations.map((op, index) => ( + + + {t(`${op}.title`, op)} + + {index < operations.length - 1 && ( + + → + + )} + + )); + + return ( +
+ + {description} + +
+ {toolChain} +
+
+ ); + }; + const renderContent = () => { if (title) { // Custom automation with title @@ -89,7 +138,7 @@ export default function AutomationEntry({ } }; - return ( + const boxContent = ( + {onCopy && ( + } + onClick={(e) => { + e.stopPropagation(); + onCopy(); + }} + > + {t('automate.copyToSaved', 'Copy to Saved')} + + )} {onEdit && ( } @@ -160,4 +220,18 @@ export default function AutomationEntry({ ); + + // Only show tooltip if description exists, otherwise return plain content + return description ? ( + + {boxContent} + + ) : ( + boxContent + ); } diff --git a/frontend/src/components/tools/automate/AutomationSelection.tsx b/frontend/src/components/tools/automate/AutomationSelection.tsx index f55cf4c5d..197a96b3e 100644 --- a/frontend/src/components/tools/automate/AutomationSelection.tsx +++ b/frontend/src/components/tools/automate/AutomationSelection.tsx @@ -5,7 +5,7 @@ import AddCircleOutline from "@mui/icons-material/AddCircleOutline"; import SettingsIcon from "@mui/icons-material/Settings"; import AutomationEntry from "./AutomationEntry"; import { useSuggestedAutomations } from "../../../hooks/tools/automate/useSuggestedAutomations"; -import { AutomationConfig } from "../../../types/automation"; +import { AutomationConfig, SuggestedAutomation } from "../../../types/automation"; interface AutomationSelectionProps { savedAutomations: AutomationConfig[]; @@ -13,6 +13,7 @@ interface AutomationSelectionProps { onRun: (automation: AutomationConfig) => void; onEdit: (automation: AutomationConfig) => void; onDelete: (automation: AutomationConfig) => void; + onCopyFromSuggested: (automation: SuggestedAutomation) => void; } export default function AutomationSelection({ @@ -20,7 +21,8 @@ export default function AutomationSelection({ onCreateNew, onRun, onEdit, - onDelete + onDelete, + onCopyFromSuggested }: AutomationSelectionProps) { const { t } = useTranslation(); const suggestedAutomations = useSuggestedAutomations(); @@ -63,9 +65,13 @@ export default function AutomationSelection({ {suggestedAutomations.map((automation) => ( op.operation)} onClick={() => onRun(automation)} + showMenu={true} + onCopy={() => onCopyFromSuggested(automation)} /> ))} diff --git a/frontend/src/hooks/tools/automate/useSavedAutomations.ts b/frontend/src/hooks/tools/automate/useSavedAutomations.ts index c52e4c784..1f210b432 100644 --- a/frontend/src/hooks/tools/automate/useSavedAutomations.ts +++ b/frontend/src/hooks/tools/automate/useSavedAutomations.ts @@ -1,5 +1,6 @@ import { useState, useEffect, useCallback } from 'react'; import { AutomationConfig } from '../../../services/automationStorage'; +import { SuggestedAutomation } from '../../../types/automation'; export interface SavedAutomation extends AutomationConfig {} @@ -40,6 +41,26 @@ export function useSavedAutomations() { } }, [refreshAutomations]); + const copyFromSuggested = useCallback(async (suggestedAutomation: SuggestedAutomation) => { + try { + const { automationStorage } = await import('../../../services/automationStorage'); + + // Convert suggested automation to saved automation format + const savedAutomation = { + name: suggestedAutomation.name, + description: suggestedAutomation.description, + operations: suggestedAutomation.operations + }; + + await automationStorage.saveAutomation(savedAutomation); + // Refresh the list after saving + refreshAutomations(); + } catch (err) { + console.error('Error copying suggested automation:', err); + throw err; + } + }, [refreshAutomations]); + // Load automations on mount useEffect(() => { loadSavedAutomations(); @@ -50,6 +71,7 @@ export function useSavedAutomations() { loading, error, refreshAutomations, - deleteAutomation + deleteAutomation, + copyFromSuggested }; } \ No newline at end of file diff --git a/frontend/src/hooks/tools/automate/useSuggestedAutomations.ts b/frontend/src/hooks/tools/automate/useSuggestedAutomations.ts index 006c9f179..047f041e4 100644 --- a/frontend/src/hooks/tools/automate/useSuggestedAutomations.ts +++ b/frontend/src/hooks/tools/automate/useSuggestedAutomations.ts @@ -17,9 +17,60 @@ export function useSuggestedAutomations(): SuggestedAutomation[] { const now = new Date().toISOString(); return [ { - id: "compress-and-split", - name: t("automation.suggested.compressAndSplit", "Compress & Split"), - description: t("automation.suggested.compressAndSplitDesc", "Compress PDFs and split them by pages"), + id: "secure-pdf-ingestion", + name: t("automation.suggested.securePdfIngestion", "Secure PDF Ingestion"), + description: t("automation.suggested.securePdfIngestionDesc", "Comprehensive PDF processing workflow that sanitizes documents, applies OCR with cleanup, converts to PDF/A format for long-term archival, and optimizes file size."), + operations: [ + { + operation: "sanitize", + parameters: { + removeJavaScript: true, + removeEmbeddedFiles: true, + removeXMPMetadata: true, + removeMetadata: true, + removeLinks: false, + removeFonts: false, + } + }, + { + operation: "ocr", + parameters: { + languages: ['eng'], + ocrType: 'skip-text', + ocrRenderType: 'hocr', + additionalOptions: ['clean', 'cleanFinal'], + } + }, + { + operation: "convert", + parameters: { + fromExtension: 'pdf', + toExtension: 'pdfa', + pdfaOptions: { + outputFormat: 'pdfa-1', + } + } + }, + { + operation: "compress", + parameters: { + compressionLevel: 5, + grayscale: false, + expectedSize: '', + compressionMethod: 'quality', + fileSizeValue: '', + fileSizeUnit: 'MB', + } + } + ], + createdAt: now, + updatedAt: now, + icon: SecurityIcon, + }, + { + id: "email-preparation", + name: t("automation.suggested.emailPreparation", "Email Preparation"), + description: t("automation.suggested.emailPreparationDesc", "Optimizes PDFs for email distribution by compressing files, splitting large documents into 20MB chunks for email compatibility, and removing metadata for privacy."), operations: [ { operation: "compress", @@ -36,45 +87,37 @@ export function useSuggestedAutomations(): SuggestedAutomation[] { operation: "splitPdf", parameters: { mode: 'bySizeOrCount', - pages: '1', - hDiv: '2', - vDiv: '2', + pages: '', + hDiv: '1', + vDiv: '1', merge: false, - splitType: 'pages', - splitValue: '1', + splitType: 'size', + splitValue: '20MB', bookmarkLevel: '1', includeMetadata: false, allowDuplicates: false, } + }, + { + operation: "sanitize", + parameters: { + removeJavaScript: false, + removeEmbeddedFiles: false, + removeXMPMetadata: true, + removeMetadata: true, + removeLinks: false, + removeFonts: false, + } } ], createdAt: now, updatedAt: now, icon: CompressIcon, }, - { - 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: { - languages: ['eng'], - ocrType: 'skip-text', - ocrRenderType: 'hocr', - additionalOptions: [], - } - } - ], - createdAt: now, - updatedAt: now, - icon: TextFieldsIcon, - }, { id: "secure-workflow", name: t("automation.suggested.secureWorkflow", "Security Workflow"), - description: t("automation.suggested.secureWorkflowDesc", "Sanitize PDFs and add password protection"), + description: t("automation.suggested.secureWorkflowDesc", "Secures PDF documents by removing potentially malicious content like JavaScript and embedded files, then adds password protection to prevent unauthorized access."), operations: [ { operation: "sanitize", @@ -111,23 +154,32 @@ export function useSuggestedAutomations(): SuggestedAutomation[] { icon: SecurityIcon, }, { - id: "optimization-workflow", - name: t("automation.suggested.optimizationWorkflow", "Optimization Workflow"), - description: t("automation.suggested.optimizationWorkflowDesc", "Repair and compress PDFs for better performance"), + id: "process-images", + name: t("automation.suggested.processImages", "Process Images"), + description: t("automation.suggested.processImagesDesc", "Converts multiple image files into a single PDF document, then applies OCR technology to extract searchable text from the images."), operations: [ { - operation: "repair", - parameters: {} + operation: "convert", + parameters: { + fromExtension: 'image', + toExtension: 'pdf', + imageOptions: { + colorType: 'color', + dpi: 300, + singleOrMultiple: 'multiple', + fitOption: 'maintainAspectRatio', + autoRotate: true, + combineImages: true, + } + } }, { - operation: "compress", + operation: "ocr", parameters: { - compressionLevel: 7, - grayscale: false, - expectedSize: '', - compressionMethod: 'quality', - fileSizeValue: '', - fileSizeUnit: 'MB', + languages: ['eng'], + ocrType: 'skip-text', + ocrRenderType: 'hocr', + additionalOptions: [], } } ], diff --git a/frontend/src/tools/Automate.tsx b/frontend/src/tools/Automate.tsx index 444cdfce9..e31d1abe3 100644 --- a/frontend/src/tools/Automate.tsx +++ b/frontend/src/tools/Automate.tsx @@ -28,7 +28,7 @@ const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { const automateOperation = useAutomateOperation(); const toolRegistry = useFlatToolRegistry(); const hasResults = automateOperation.files.length > 0 || automateOperation.downloadUrl !== null; - const { savedAutomations, deleteAutomation, refreshAutomations } = useSavedAutomations(); + const { savedAutomations, deleteAutomation, refreshAutomations, copyFromSuggested } = useSavedAutomations(); const handleStepChange = (data: AutomationStepData) => { // If navigating away from run step, reset automation results @@ -79,6 +79,14 @@ const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { onError?.(`Failed to delete automation: ${automation.name}`); } }} + onCopyFromSuggested={async (suggestedAutomation) => { + try { + await copyFromSuggested(suggestedAutomation); + } catch (error) { + console.error('Failed to copy suggested automation:', error); + onError?.(`Failed to copy automation: ${suggestedAutomation.name}`); + } + }} /> );