diff --git a/.github/aur/stirling-pdf-desktop/PKGBUILD b/.github/aur/stirling-pdf-desktop/PKGBUILD index 1afd3bba3..8498b05d2 100644 --- a/.github/aur/stirling-pdf-desktop/PKGBUILD +++ b/.github/aur/stirling-pdf-desktop/PKGBUILD @@ -1,6 +1,6 @@ # Maintainer: Stirling PDF Inc pkgname=stirling-pdf-desktop -pkgver=2.10.0 +pkgver=2.10.1 pkgrel=1 pkgdesc="Locally hosted, web-based PDF manipulation tool (Tauri desktop app, official Stirling PDF Inc build)" arch=('x86_64') diff --git a/.github/aur/stirling-pdf-server-bin/PKGBUILD b/.github/aur/stirling-pdf-server-bin/PKGBUILD index 568b86b83..3a06d652a 100644 --- a/.github/aur/stirling-pdf-server-bin/PKGBUILD +++ b/.github/aur/stirling-pdf-server-bin/PKGBUILD @@ -1,6 +1,6 @@ # Maintainer: Stirling PDF Inc pkgname=stirling-pdf-server-bin -pkgver=2.10.0 +pkgver=2.10.1 pkgrel=1 pkgdesc="Locally hosted, web-based PDF manipulation tool (server JAR, prebuilt)" arch=('any') diff --git a/build.gradle b/build.gradle index f200c5775..5c7617bfa 100644 --- a/build.gradle +++ b/build.gradle @@ -78,7 +78,7 @@ springBoot { allprojects { group = 'stirling.software' - version = '2.10.0' + version = '2.10.1' configurations.configureEach { exclude group: "org.springframework.boot", module: "spring-boot-starter-tomcat" diff --git a/frontend/public/locales/en-GB/translation.toml b/frontend/public/locales/en-GB/translation.toml index 9868563ba..05be4b505 100644 --- a/frontend/public/locales/en-GB/translation.toml +++ b/frontend/public/locales/en-GB/translation.toml @@ -1791,26 +1791,14 @@ bullet3 = "Keeps the original name if no suitable title is found" text = "Automatically finds the title from your PDF content and uses it as the filename." title = "Smart Renaming" -[pdfCommentAgent] -submit = "Generate comments" -prompt.label = "What should the AI comment on?" -prompt.placeholder = "e.g. Flag any ambiguous dates and suggest clarifications" - -[pdfCommentAgent.settings] -title = "Comment instructions" - -[pdfCommentAgent.results] -title = "Commented PDF" - -[pdfCommentAgent.error] -failed = "Failed to generate comments" -emptyPrompt = "Please describe what the AI should comment on" -tooLong = "Prompt is too long (maximum {{max}} characters)" - [automate] copyToSaved = "Copy to Saved" desc = "Build multi-step workflows by chaining together PDF actions. Ideal for recurring tasks." +export = "Export" exportForFolderScanning = "Export for Folder Scanning" +import = "Import" +importPartialSuccess = "Imported with {{count}} unmapped operation(s): {{ops}}" +importSuccess = "Imported automation: {{name}}" invalidStep = "Invalid step" reviewTitle = "Automation Results" title = "Automate" @@ -1826,6 +1814,7 @@ title = "Configure {{toolName}}" [automate.creation] createTitle = "Create Automation" editTitle = "Edit Automation" +export = "Export" exportForFolderScanning = "Export for Folder Scanning" intro = "Automations run tools sequentially. To get started, add tools in the order you want them to run." save = "Save Automation" @@ -1856,9 +1845,28 @@ confirm = "Go Back" message = "You have unsaved changes. Are you sure you want to go back? All changes will be lost." title = "Unsaved Changes" +[automate.entryMenu] +label = "Open menu for {{title}}" + [automate.files] placeholder = "Select files to process with this automation" +[automate.importModal] +cancel = "Cancel" +confirm = "Import" +detectedAutomation = "Automate JSON" +detectedFolderScan = "Folder Scanning JSON" +dropHint = "Drop JSON here or click to choose a file" +dropSubhint = "Both Automate and Folder Scanning configs are accepted" +dropzoneAriaLabel = "Drop an automation JSON file here" +intro = "Drop a JSON file or paste its contents below. The format (Automate or Folder Scanning) is detected automatically." +opCount = "{{count}} operation(s)" +parseError = "Could not parse: {{message}}" +pasteLabel = "Or paste JSON" +pastePlaceholder = "Paste your automation JSON here…" +title = "Import automation" +unresolved = "Unmapped: {{ops}}" + [automate.run] title = "Run Automation" @@ -4068,11 +4076,6 @@ desc = "Auto renames a PDF file based on its detected header" tags = "auto-detect,header-based,organize,relabel,auto rename,automatic rename,smart rename,rename by content,filename,file naming,detect title" title = "Auto Rename PDF File" -[home.pdfCommentAgent] -desc = "Ask AI to annotate a PDF with sticky-note comments based on your prompt" -tags = "AI,agent,comment,annotate,sticky note,review,feedback,notes" -title = "Add AI comments" - [home.autoSizeSplitPDF] desc = "Automatically split PDFs by file size or page count" tags = "auto,split,size" @@ -4212,6 +4215,11 @@ desc = "Merge multiple pages of a PDF document into a single page" tags = "layout,arrange,combine,N-up,2-up,4-up,multiple per page,pages per sheet,layout pages,tile,grid layout,multi-page layout,combine on page,handout" title = "Multi-Page Layout" +[home.pdfCommentAgent] +desc = "Ask AI to annotate a PDF with sticky-note comments based on your prompt" +tags = "AI,agent,comment,annotate,sticky note,review,feedback,notes" +title = "Add AI comments" + [home.pdfOrganiser] desc = "Remove/Rearrange pages in any order" tags = "organize,rearrange,reorder,organise,arrange pages,sort,move pages,delete pages,remove pages,page management,page organizer,page organiser,resequence" @@ -5288,6 +5296,24 @@ selectYearly = "Select Yearly" title = "Choose Your Billing Period" totalForSeats = "Total ({{count}} seats)" +[pdfCommentAgent] +submit = "Generate comments" + +[pdfCommentAgent.error] +emptyPrompt = "Please describe what the AI should comment on" +failed = "Failed to generate comments" +tooLong = "Prompt is too long (maximum {{max}} characters)" + +[pdfCommentAgent.prompt] +label = "What should the AI comment on?" +placeholder = "e.g. Flag any ambiguous dates and suggest clarifications" + +[pdfCommentAgent.results] +title = "Commented PDF" + +[pdfCommentAgent.settings] +title = "Comment instructions" + [pdfOrganiser] header = "PDF Page Organiser" placeholder = "(e.g. 1,3,2 or 4-8,2,10-12 or 2n-1)" diff --git a/frontend/src-tauri/tauri.conf.json b/frontend/src-tauri/tauri.conf.json index 6e643e50d..65070180e 100644 --- a/frontend/src-tauri/tauri.conf.json +++ b/frontend/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "../node_modules/@tauri-apps/cli/config.schema.json", "productName": "Stirling-PDF", - "version": "2.10.0", + "version": "2.10.1", "identifier": "stirling.pdf.dev", "build": { "frontendDist": "../dist", diff --git a/frontend/src/core/components/tools/automate/AutomationCreation.tsx b/frontend/src/core/components/tools/automate/AutomationCreation.tsx index f39c30036..007936739 100644 --- a/frontend/src/core/components/tools/automate/AutomationCreation.tsx +++ b/frontend/src/core/components/tools/automate/AutomationCreation.tsx @@ -23,7 +23,10 @@ import { AutomationTool, } from "@app/types/automation"; import { useAutomationForm } from "@app/hooks/tools/automate/useAutomationForm"; -import { downloadFolderScanningConfig } from "@app/utils/automationConverter"; +import { + downloadAutomationConfig, + downloadFolderScanningConfig, +} from "@app/utils/automationConverter"; interface AutomationCreationProps { mode: AutomationMode; @@ -154,6 +157,24 @@ export default function AutomationCreation({ const currentConfigTool = configuraingToolIndex >= 0 ? selectedTools[configuraingToolIndex] : null; + /** + * Build a temporary AutomationConfig from in-progress form state so the + * export buttons can run without requiring the user to save first. The + * `id` and timestamps are placeholders — the export utilities strip them. + */ + const buildExportableAutomation = (): AutomationConfig => ({ + id: existingAutomation?.id || "temp", + name: automationName.trim(), + description: automationDescription.trim(), + icon: automationIcon, + operations: selectedTools.map((tool) => ({ + operation: tool.operation, + parameters: tool.parameters || {}, + })), + createdAt: existingAutomation?.createdAt || new Date().toISOString(), + updatedAt: new Date().toISOString(), + }); + return (
- + + + + diff --git a/frontend/src/core/components/tools/automate/AutomationEntry.tsx b/frontend/src/core/components/tools/automate/AutomationEntry.tsx index e9116951d..d7fdc43b6 100644 --- a/frontend/src/core/components/tools/automate/AutomationEntry.tsx +++ b/frontend/src/core/components/tools/automate/AutomationEntry.tsx @@ -6,6 +6,7 @@ import EditIcon from "@mui/icons-material/Edit"; import DeleteIcon from "@mui/icons-material/Delete"; import ContentCopyIcon from "@mui/icons-material/ContentCopy"; import DownloadIcon from "@mui/icons-material/Download"; +import UploadFileIcon from "@mui/icons-material/UploadFile"; import { Tooltip } from "@app/components/shared/Tooltip"; import { ToolIcon } from "@app/components/shared/ToolIcon"; import { ToolRegistry } from "@app/data/toolsTaxonomy"; @@ -32,8 +33,12 @@ interface AutomationEntryProps { onDelete?: () => void; /** Copy handler (for suggested automations) */ onCopy?: () => void; - /** Export handler (for folder scanning) */ - onExport?: () => void; + /** Export handler — folder scanning JSON */ + onExportFolderScan?: () => void; + /** Export handler — native Automate JSON (renders as just "Export") */ + onExportAutomation?: () => void; + /** Import handler — opens the import modal (used on "Create New" entry) */ + onImport?: () => void; /** Tool registry to resolve operation names */ toolRegistry?: Partial; } @@ -49,7 +54,9 @@ export default function AutomationEntry({ onEdit, onDelete, onCopy, - onExport, + onExportFolderScan, + onExportAutomation, + onImport, toolRegistry, }: AutomationEntryProps) { const { t } = useTranslation(); @@ -211,6 +218,11 @@ export default function AutomationEntry({ variant="subtle" c="dimmed" size="md" + aria-label={t( + "automate.entryMenu.label", + "Open menu for {{title}}", + { title: title || operations.join(" → ") }, + )} onClick={(e) => e.stopPropagation()} style={{ position: "absolute", @@ -228,6 +240,17 @@ export default function AutomationEntry({ + {onImport && ( + } + onClick={(e) => { + e.stopPropagation(); + onImport(); + }} + > + {t("automate.import", "Import")} + + )} {onCopy && ( } @@ -250,12 +273,23 @@ export default function AutomationEntry({ {t("edit", "Edit")} )} - {onExport && ( + {onExportAutomation && ( } onClick={(e) => { e.stopPropagation(); - onExport(); + onExportAutomation(); + }} + > + {t("automate.export", "Export")} + + )} + {onExportFolderScan && ( + } + onClick={(e) => { + e.stopPropagation(); + onExportFolderScan(); }} > {t( diff --git a/frontend/src/core/components/tools/automate/AutomationImportModal.tsx b/frontend/src/core/components/tools/automate/AutomationImportModal.tsx new file mode 100644 index 000000000..95e85e802 --- /dev/null +++ b/frontend/src/core/components/tools/automate/AutomationImportModal.tsx @@ -0,0 +1,235 @@ +import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { + Alert, + Badge, + Button, + Group, + Modal, + Stack, + Text, + Textarea, +} from "@mantine/core"; +import { Dropzone } from "@mantine/dropzone"; +import UploadFileIcon from "@mui/icons-material/UploadFile"; +import { Z_INDEX_AUTOMATE_MODAL } from "@app/styles/zIndex"; +import { + ParsedAutomationImport, + parseAutomationFile, +} from "@app/utils/automationConverter"; +import { ToolRegistry } from "@app/data/toolsTaxonomy"; +import type { ImportableAutomation } from "@app/hooks/tools/automate/useSavedAutomations"; + +interface AutomationImportModalProps { + opened: boolean; + toolRegistry: Partial; + onCancel: () => void; + onImport: ( + automation: ImportableAutomation, + meta: { format: ParsedAutomationImport["format"]; unresolved: string[] }, + ) => void | Promise; +} + +/** + * Single import surface for both supported automation JSON shapes. + * + * Accepts a file drop or pasted text, auto-detects whether the JSON is the + * native Automate config or the backend folder-scanning config, and shows + * the resolved name + format before the user commits the import. + */ +export default function AutomationImportModal({ + opened, + toolRegistry, + onCancel, + onImport, +}: AutomationImportModalProps) { + const { t } = useTranslation(); + + const [pastedText, setPastedText] = useState(""); + const [parsed, setParsed] = useState(null); + const [parseError, setParseError] = useState(null); + const [submitting, setSubmitting] = useState(false); + + // Reset modal state every time it's reopened so a previous import doesn't + // leak into the next one. + useEffect(() => { + if (opened) { + setPastedText(""); + setParsed(null); + setParseError(null); + setSubmitting(false); + } + }, [opened]); + + // Re-parse whenever the textarea changes — gives the user immediate feedback + // without requiring a click. + useEffect(() => { + const trimmed = pastedText.trim(); + if (!trimmed) { + setParsed(null); + setParseError(null); + return; + } + try { + const result = parseAutomationFile(trimmed, toolRegistry); + setParsed(result); + setParseError(null); + } catch (err) { + setParsed(null); + setParseError(err instanceof Error ? err.message : String(err)); + } + }, [pastedText, toolRegistry]); + + const handleFileDrop = async (files: File[]) => { + const file = files[0]; + if (!file) return; + try { + const text = await file.text(); + setPastedText(text); + } catch (err) { + setParseError(err instanceof Error ? err.message : String(err)); + } + }; + + const handleSubmit = async () => { + if (!parsed) return; + setSubmitting(true); + try { + await onImport(parsed.automation, { + format: parsed.format, + unresolved: parsed.unresolvedOperations, + }); + } finally { + setSubmitting(false); + } + }; + + const formatLabel = + parsed?.format === "automate" + ? t("automate.importModal.detectedAutomation", "Automate JSON") + : parsed?.format === "folderScanning" + ? t("automate.importModal.detectedFolderScan", "Folder Scanning JSON") + : null; + + return ( + + + + {t( + "automate.importModal.intro", + "Drop a JSON file or paste its contents below. The format (Automate or Folder Scanning) is detected automatically.", + )} + + + void handleFileDrop(files)} + accept={["application/json", "text/plain"]} + multiple={false} + maxSize={10 * 1024 * 1024} + aria-label={t( + "automate.importModal.dropzoneAriaLabel", + "Drop an automation JSON file here", + )} + > + + +
+ + {t( + "automate.importModal.dropHint", + "Drop JSON here or click to choose a file", + )} + + + {t( + "automate.importModal.dropSubhint", + "Both Automate and Folder Scanning configs are accepted", + )} + +
+
+
+ +