From bbd658d3b83fa20a2d14ca9ce5e4b9e93d4319d4 Mon Sep 17 00:00:00 2001 From: ConnorYoh <40631091+ConnorYoh@users.noreply.github.com> Date: Tue, 26 Aug 2025 09:25:33 +0100 Subject: [PATCH 1/6] Default View to file editor not page editor (#4258) Co-authored-by: Connor Yoh --- frontend/src/types/navigation.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/types/navigation.ts b/frontend/src/types/navigation.ts index 61aa24cc3..70d108c9a 100644 --- a/frontend/src/types/navigation.ts +++ b/frontend/src/types/navigation.ts @@ -33,7 +33,7 @@ export const isValidMode = (mode: string): mode is ModeType => { return validModes.includes(mode as ModeType); }; -export const getDefaultMode = (): ModeType => 'pageEditor'; +export const getDefaultMode = (): ModeType => 'fileEditor'; // Route parsing result export interface ToolRoute { From 42d7664e25044cefa8a96d4cb2ccea214e5d5ff9 Mon Sep 17 00:00:00 2001 From: ConnorYoh <40631091+ConnorYoh@users.noreply.github.com> Date: Tue, 26 Aug 2025 09:26:26 +0100 Subject: [PATCH 2/6] Preview file (#4260) Preview file works Co-authored-by: Connor Yoh --- frontend/src/contexts/ToolWorkflowContext.tsx | 5 ++++- frontend/src/tools/Automate.tsx | 8 +++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/frontend/src/contexts/ToolWorkflowContext.tsx b/frontend/src/contexts/ToolWorkflowContext.tsx index f784dd49a..2bbb3c9f4 100644 --- a/frontend/src/contexts/ToolWorkflowContext.tsx +++ b/frontend/src/contexts/ToolWorkflowContext.tsx @@ -134,7 +134,10 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) { const setPreviewFile = useCallback((file: File | null) => { dispatch({ type: 'SET_PREVIEW_FILE', payload: file }); - }, []); + if (file) { + actions.setMode('viewer'); + } + }, [actions]); const setPageEditorFunctions = useCallback((functions: PageEditorFunctions | null) => { dispatch({ type: 'SET_PAGE_EDITOR_FUNCTIONS', payload: functions }); diff --git a/frontend/src/tools/Automate.tsx b/frontend/src/tools/Automate.tsx index af6b3d411..7e75f0cbf 100644 --- a/frontend/src/tools/Automate.tsx +++ b/frontend/src/tools/Automate.tsx @@ -2,6 +2,7 @@ import React, { useState } from "react"; import { useTranslation } from "react-i18next"; import { useFileContext } from "../contexts/FileContext"; import { useFileSelection } from "../contexts/FileContext"; +import { useNavigation } from "../contexts/NavigationContext"; import { createToolFlow } from "../components/tools/shared/createToolFlow"; import { createFilesToolStep } from "../components/tools/shared/FilesToolStep"; @@ -19,6 +20,7 @@ import { AUTOMATION_STEPS } from "../constants/automation"; const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { const { t } = useTranslation(); const { selectedFiles } = useFileSelection(); + const { setMode } = useNavigation(); const [currentStep, setCurrentStep] = useState<'selection' | 'creation' | 'run'>(AUTOMATION_STEPS.SELECTION); const [stepData, setStepData] = useState({ step: AUTOMATION_STEPS.SELECTION }); @@ -171,7 +173,11 @@ const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { review: { isVisible: hasResults && currentStep === AUTOMATION_STEPS.RUN, operation: automateOperation, - title: t('automate.reviewTitle', 'Automation Results') + title: t('automate.reviewTitle', 'Automation Results'), + onFileClick: (file: File) => { + onPreviewFile?.(file); + setMode('viewer'); + } } }); }; From fe9d2367d5e086f68453b5e09e72f55c812be551 Mon Sep 17 00:00:00 2001 From: ConnorYoh <40631091+ConnorYoh@users.noreply.github.com> Date: Tue, 26 Aug 2025 09:44:30 +0100 Subject: [PATCH 3/6] AutomateFixes (#4281) can edit automations drop down styles drop down bug fixes --------- Co-authored-by: Connor Yoh --- frontend/src/components/shared/TextInput.tsx | 6 +- .../tools/automate/AutomationCreation.tsx | 27 ++- .../components/tools/automate/ToolList.tsx | 116 +++++++----- .../tools/automate/ToolSelector.tsx | 168 ++++++++++-------- .../tools/toolPicker/ToolButton.tsx | 15 +- .../tools/toolPicker/ToolPicker.css | 2 +- .../tools/toolPicker/ToolSearch.tsx | 78 ++++---- .../hooks/tools/automate/useAutomationForm.ts | 12 +- frontend/src/tools/Automate.tsx | 10 +- frontend/src/types/automation.ts | 4 +- 10 files changed, 264 insertions(+), 174 deletions(-) diff --git a/frontend/src/components/shared/TextInput.tsx b/frontend/src/components/shared/TextInput.tsx index fc3e99015..a5dd90569 100644 --- a/frontend/src/components/shared/TextInput.tsx +++ b/frontend/src/components/shared/TextInput.tsx @@ -31,6 +31,8 @@ export interface TextInputProps { readOnly?: boolean; /** Accessibility label */ 'aria-label'?: string; + /** Focus event handler */ + onFocus?: () => void; } export const TextInput = forwardRef(({ @@ -46,6 +48,7 @@ export const TextInput = forwardRef(({ disabled = false, readOnly = false, 'aria-label': ariaLabel, + onFocus, ...props }, ref) => { const { colorScheme } = useMantineColorScheme(); @@ -63,7 +66,7 @@ export const TextInput = forwardRef(({ return (
{icon && ( - @@ -81,6 +84,7 @@ export const TextInput = forwardRef(({ disabled={disabled} readOnly={readOnly} aria-label={ariaLabel} + onFocus={onFocus} style={{ backgroundColor: colorScheme === 'dark' ? '#4B525A' : '#FFFFFF', color: colorScheme === 'dark' ? '#FFFFFF' : '#6B7382', diff --git a/frontend/src/components/tools/automate/AutomationCreation.tsx b/frontend/src/components/tools/automate/AutomationCreation.tsx index 49b12c396..ee301ae8e 100644 --- a/frontend/src/components/tools/automate/AutomationCreation.tsx +++ b/frontend/src/components/tools/automate/AutomationCreation.tsx @@ -98,7 +98,7 @@ export default function AutomationCreation({ mode, existingAutomation, onBack, o const saveAutomation = async () => { if (!canSaveAutomation()) return; - const automation = { + const automationData = { name: automationName.trim(), description: '', operations: selectedTools.map(tool => ({ @@ -109,7 +109,30 @@ export default function AutomationCreation({ mode, existingAutomation, onBack, o try { const { automationStorage } = await import('../../../services/automationStorage'); - const savedAutomation = await automationStorage.saveAutomation(automation); + let savedAutomation; + + if (mode === AutomationMode.EDIT && existingAutomation) { + // For edit mode, check if name has changed + const nameChanged = automationName.trim() !== existingAutomation.name; + + if (nameChanged) { + // Name changed - create new automation + savedAutomation = await automationStorage.saveAutomation(automationData); + } else { + // Name unchanged - update existing automation + const updatedAutomation = { + ...existingAutomation, + ...automationData, + id: existingAutomation.id, + createdAt: existingAutomation.createdAt + }; + savedAutomation = await automationStorage.updateAutomation(updatedAutomation); + } + } else { + // Create mode - always create new automation + savedAutomation = await automationStorage.saveAutomation(automationData); + } + onComplete(savedAutomation); } catch (error) { console.error('Error saving automation:', error); diff --git a/frontend/src/components/tools/automate/ToolList.tsx b/frontend/src/components/tools/automate/ToolList.tsx index 8b24b5c17..b11140ac5 100644 --- a/frontend/src/components/tools/automate/ToolList.tsx +++ b/frontend/src/components/tools/automate/ToolList.tsx @@ -1,14 +1,13 @@ -import React from 'react'; -import { useTranslation } from 'react-i18next'; -import { Text, Stack, Group, ActionIcon } from '@mantine/core'; -import DeleteIcon from '@mui/icons-material/Delete'; -import SettingsIcon from '@mui/icons-material/Settings'; -import CloseIcon from '@mui/icons-material/Close'; -import AddCircleOutline from '@mui/icons-material/AddCircleOutline'; -import { AutomationTool } from '../../../types/automation'; -import { ToolRegistryEntry } from '../../../data/toolsTaxonomy'; -import ToolSelector from './ToolSelector'; -import AutomationEntry from './AutomationEntry'; +import React from "react"; +import { useTranslation } from "react-i18next"; +import { Text, Stack, Group, ActionIcon } from "@mantine/core"; +import SettingsIcon from "@mui/icons-material/Settings"; +import CloseIcon from "@mui/icons-material/Close"; +import AddCircleOutline from "@mui/icons-material/AddCircleOutline"; +import { AutomationTool } from "../../../types/automation"; +import { ToolRegistryEntry } from "../../../data/toolsTaxonomy"; +import ToolSelector from "./ToolSelector"; +import AutomationEntry from "./AutomationEntry"; interface ToolListProps { tools: AutomationTool[]; @@ -29,35 +28,39 @@ export default function ToolList({ onToolConfigure, onToolAdd, getToolName, - getToolDefaultParameters + getToolDefaultParameters, }: ToolListProps) { const { t } = useTranslation(); const handleToolSelect = (index: number, newOperation: string) => { const defaultParams = getToolDefaultParameters(newOperation); - + onToolUpdate(index, { operation: newOperation, name: getToolName(newOperation), configured: false, - parameters: defaultParams + parameters: defaultParams, }); }; return (
- - {t('automate.creation.tools.selected', 'Selected Tools')} ({tools.length}) + + {t("automate.creation.tools.selected", "Selected Tools")} ({tools.length}) {tools.map((tool, index) => (
{/* Delete X in top right */} @@ -65,26 +68,26 @@ export default function ToolList({ variant="subtle" size="xs" onClick={() => onToolRemove(index)} - title={t('automate.creation.tools.remove', 'Remove tool')} + title={t("automate.creation.tools.remove", "Remove tool")} style={{ - position: 'absolute', - top: '4px', - right: '4px', + position: "absolute", + top: "4px", + right: "4px", zIndex: 1, - color: 'var(--mantine-color-gray-6)' + color: "var(--mantine-color-gray-6)", }} > - + -
+
{/* Tool Selection Dropdown with inline settings cog */}
handleToolSelect(index, newOperation)} - excludeTools={['automate']} + excludeTools={["automate"]} toolRegistry={toolRegistry} selectedValue={tool.operation} placeholder={tool.name} @@ -97,26 +100,37 @@ export default function ToolList({ variant="subtle" size="sm" onClick={() => onToolConfigure(index)} - title={t('automate.creation.tools.configure', 'Configure tool')} - style={{ color: 'var(--mantine-color-gray-6)' }} + title={t("automate.creation.tools.configure", "Configure tool")} + style={{ color: "var(--mantine-color-gray-6)" }} > )} - - {/* Configuration status underneath */} - {tool.operation && !tool.configured && ( - - {t('automate.creation.tools.notConfigured', "! Not Configured")} - - )}
- + {/* Configuration status underneath */} + {tool.operation && !tool.configured && ( +
+ + {t("automate.creation.tools.notConfigured", "! Not Configured")} + +
+ )} {index < tools.length - 1 && ( -
- +
+ + ↓ +
)} @@ -124,19 +138,23 @@ export default function ToolList({ {/* Arrow before Add Tool Button */} {tools.length > 0 && ( -
- +
+ + ↓ +
)} {/* Add Tool Button */} -
+
); -} \ No newline at end of file +} diff --git a/frontend/src/components/tools/automate/ToolSelector.tsx b/frontend/src/components/tools/automate/ToolSelector.tsx index 80b68b0a4..4fb87548f 100644 --- a/frontend/src/components/tools/automate/ToolSelector.tsx +++ b/frontend/src/components/tools/automate/ToolSelector.tsx @@ -1,10 +1,11 @@ -import React, { useState, useMemo, useCallback } from 'react'; +import React, { useState, useMemo, useCallback, useRef, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; -import { Menu, Stack, Text, ScrollArea } from '@mantine/core'; +import { Stack, Text, ScrollArea } from '@mantine/core'; import { ToolRegistryEntry } from '../../../data/toolsTaxonomy'; import { useToolSections } from '../../../hooks/useToolSections'; import { renderToolButtons } from '../shared/renderToolButtons'; import ToolSearch from '../toolPicker/ToolSearch'; +import ToolButton from '../toolPicker/ToolButton'; interface ToolSelectorProps { onSelect: (toolKey: string) => void; @@ -24,6 +25,8 @@ export default function ToolSelector({ const { t } = useTranslation(); const [opened, setOpened] = useState(false); const [searchTerm, setSearchTerm] = useState(''); + const [shouldAutoFocus, setShouldAutoFocus] = useState(false); + const containerRef = useRef(null); // Filter out excluded tools (like 'automate' itself) const baseFilteredTools = useMemo(() => { @@ -66,13 +69,21 @@ export default function ToolSelector({ } if (!sections || sections.length === 0) { + // If no sections, create a simple group from filtered tools + if (baseFilteredTools.length > 0) { + return [{ + name: 'All Tools', + subcategoryId: 'all' as any, + tools: baseFilteredTools.map(([key, tool]) => ({ id: key, tool })) + }]; + } return []; } // Find the "all" section which contains all tools without duplicates const allSection = sections.find(s => (s as any).key === 'all'); return allSection?.subcategories || []; - }, [isSearching, searchGroups, sections]); + }, [isSearching, searchGroups, sections, baseFilteredTools]); const handleToolSelect = useCallback((toolKey: string) => { onSelect(toolKey); @@ -88,8 +99,25 @@ export default function ToolSelector({ const handleSearchFocus = () => { setOpened(true); + setShouldAutoFocus(true); // Request auto-focus for the input }; + // Handle click outside to close dropdown + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (containerRef.current && !containerRef.current.contains(event.target as Node)) { + setOpened(false); + setSearchTerm(''); + } + }; + + if (opened) { + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + } + }, [opened]); + + const handleSearchChange = (value: string) => { setSearchTerm(value); if (!opened) { @@ -97,6 +125,14 @@ export default function ToolSelector({ } }; + const handleInputFocus = () => { + if (!opened) { + setOpened(true); + } + // Clear auto-focus flag since input is now focused + setShouldAutoFocus(false); + }; + // Get display value for selected tool const getDisplayValue = () => { if (selectedValue && toolRegistry[selectedValue]) { @@ -106,77 +142,63 @@ export default function ToolSelector({ }; return ( -
- { - setOpened(isOpen); - // Clear search term when menu closes to show proper display - if (!isOpen) { - setSearchTerm(''); - } - }} - closeOnClickOutside={true} - closeOnEscape={true} - position="bottom-start" - offset={4} - withinPortal={false} - trapFocus={false} - shadow="sm" - transitionProps={{ duration: 0 }} - > - -
- {selectedValue && toolRegistry[selectedValue] && !opened ? ( - // Show selected tool in AutomationEntry style when tool is selected and not searching -
-
-
- {toolRegistry[selectedValue].icon} -
- - {toolRegistry[selectedValue].name} - -
-
- ) : ( - // Show search input when no tool selected or actively searching - - )} -
-
+
+ {/* Always show the target - either selected tool or search input */} - - - - {displayGroups.length === 0 ? ( - - {isSearching - ? t('tools.noSearchResults', 'No tools found') - : t('tools.noTools', 'No tools available') - } - - ) : ( - renderedTools - )} - - - -
+ {selectedValue && toolRegistry[selectedValue] && !opened ? ( + // Show selected tool in AutomationEntry style when tool is selected and dropdown closed +
+ {}} rounded={true}> +
+ ) : ( + // Show search input when no tool selected OR when dropdown is opened + + )} + + {/* Custom dropdown */} + {opened && ( +
+ + + {displayGroups.length === 0 ? ( + + {isSearching + ? t('tools.noSearchResults', 'No tools found') + : t('tools.noTools', 'No tools available') + } + + ) : ( + renderedTools + )} + + +
+ )}
); } diff --git a/frontend/src/components/tools/toolPicker/ToolButton.tsx b/frontend/src/components/tools/toolPicker/ToolButton.tsx index 66bd9489e..185eed5ed 100644 --- a/frontend/src/components/tools/toolPicker/ToolButton.tsx +++ b/frontend/src/components/tools/toolPicker/ToolButton.tsx @@ -9,9 +9,10 @@ interface ToolButtonProps { tool: ToolRegistryEntry; isSelected: boolean; onSelect: (id: string) => void; + rounded?: boolean; } -const ToolButton: React.FC = ({ id, tool, isSelected, onSelect }) => { +const ToolButton: React.FC = ({ id, tool, isSelected, onSelect, rounded = false }) => { const handleClick = (id: string) => { if (tool.link) { // Open external link in new tab @@ -33,7 +34,17 @@ const ToolButton: React.FC = ({ id, tool, isSelected, onSelect fullWidth justify="flex-start" className="tool-button" - styles={{ root: { borderRadius: 0, color: "var(--tools-text-and-icon-color)" } }} + styles={{ + root: { + borderRadius: rounded ? 'var(--mantine-radius-lg)' : 0, + color: "var(--tools-text-and-icon-color)", + ...(rounded && { + '&:hover': { + borderRadius: 'var(--mantine-radius-lg)', + } + }) + } + }} > void; toolRegistry: Readonly>; onToolSelect?: (toolId: string) => void; - mode: 'filter' | 'dropdown'; + mode: "filter" | "dropdown" | "unstyled"; selectedToolKey?: string | null; placeholder?: string; hideIcon?: boolean; onFocus?: () => void; + autoFocus?: boolean; } const ToolSearch = ({ @@ -23,11 +24,12 @@ const ToolSearch = ({ onChange, toolRegistry, onToolSelect, - mode = 'filter', + mode = "filter", selectedToolKey, placeholder, hideIcon = false, - onFocus + onFocus, + autoFocus = false, }: ToolSearchProps) => { const { t } = useTranslation(); const [dropdownOpen, setDropdownOpen] = useState(false); @@ -38,9 +40,10 @@ const ToolSearch = ({ if (!value.trim()) return []; return Object.entries(toolRegistry) .filter(([id, tool]) => { - if (mode === 'dropdown' && id === selectedToolKey) return false; - return tool.name.toLowerCase().includes(value.toLowerCase()) || - tool.description.toLowerCase().includes(value.toLowerCase()); + if (mode === "dropdown" && id === selectedToolKey) return false; + return ( + tool.name.toLowerCase().includes(value.toLowerCase()) || tool.description.toLowerCase().includes(value.toLowerCase()) + ); }) .slice(0, 6) .map(([id, tool]) => ({ id, tool })); @@ -48,7 +51,7 @@ const ToolSearch = ({ const handleSearchChange = (searchValue: string) => { onChange(searchValue); - if (mode === 'dropdown') { + if (mode === "dropdown") { setDropdownOpen(searchValue.trim().length > 0 && filteredTools.length > 0); } }; @@ -64,12 +67,20 @@ const ToolSearch = ({ setDropdownOpen(false); } }; - document.addEventListener('mousedown', handleClickOutside); - return () => document.removeEventListener('mousedown', handleClickOutside); + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); }, []); + // Auto-focus the input when requested + useEffect(() => { + if (autoFocus && searchRef.current) { + setTimeout(() => { + searchRef.current?.focus(); + }, 10); + } + }, [autoFocus]); + const searchInput = ( -
} autoComplete="off" - + onFocus={onFocus} /> -
); - if (mode === 'filter') { + if (mode === "filter") { + return
{searchInput}
; + } + + if (mode === "unstyled") { return searchInput; } return ( -
+
{searchInput} {dropdownOpen && filteredTools.length > 0 && (
- + {filteredTools.map(({ id, tool }) => (
- } + leftSection={
{tool.icon}
} fullWidth justify="flex-start" style={{ - borderRadius: '6px', - color: 'var(--tools-text-and-icon-color)', - padding: '8px 12px' + borderRadius: "6px", + color: "var(--tools-text-and-icon-color)", + padding: "8px 12px", }} > -
+
{tool.name}
- + {tool.description}
diff --git a/frontend/src/hooks/tools/automate/useAutomationForm.ts b/frontend/src/hooks/tools/automate/useAutomationForm.ts index 11464a329..7bbe14d9b 100644 --- a/frontend/src/hooks/tools/automate/useAutomationForm.ts +++ b/frontend/src/hooks/tools/automate/useAutomationForm.ts @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { AutomationTool, AutomationConfig, AutomationMode } from '../../../types/automation'; import { AUTOMATION_CONSTANTS } from '../../../constants/automation'; @@ -16,18 +16,18 @@ export function useAutomationForm({ mode, existingAutomation, toolRegistry }: Us const [automationName, setAutomationName] = useState(''); const [selectedTools, setSelectedTools] = useState([]); - const getToolName = (operation: string) => { + const getToolName = useCallback((operation: string) => { const tool = toolRegistry?.[operation] as any; return tool?.name || t(`tools.${operation}.name`, operation); - }; + }, [toolRegistry, t]); - const getToolDefaultParameters = (operation: string): Record => { + const getToolDefaultParameters = useCallback((operation: string): Record => { const config = toolRegistry[operation]?.operationConfig; if (config?.defaultParameters) { return { ...config.defaultParameters }; } return {}; - }; + }, [toolRegistry]); // Initialize based on mode and existing automation useEffect(() => { @@ -58,7 +58,7 @@ export function useAutomationForm({ mode, existingAutomation, toolRegistry }: Us })); setSelectedTools(defaultTools); } - }, [mode, existingAutomation, selectedTools.length, t, getToolName]); + }, [mode, existingAutomation, t, getToolName]); const addTool = (operation: string) => { const newTool: AutomationTool = { diff --git a/frontend/src/tools/Automate.tsx b/frontend/src/tools/Automate.tsx index 7e75f0cbf..444cdfce9 100644 --- a/frontend/src/tools/Automate.tsx +++ b/frontend/src/tools/Automate.tsx @@ -14,7 +14,7 @@ import { useAutomateOperation } from "../hooks/tools/automate/useAutomateOperati import { BaseToolProps } from "../types/tool"; import { useFlatToolRegistry } from "../data/useTranslatedToolRegistry"; import { useSavedAutomations } from "../hooks/tools/automate/useSavedAutomations"; -import { AutomationConfig, AutomationStepData, AutomationMode } from "../types/automation"; +import { AutomationConfig, AutomationStepData, AutomationMode, AutomationStep } from "../types/automation"; import { AUTOMATION_STEPS } from "../constants/automation"; const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { @@ -22,7 +22,7 @@ const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { const { selectedFiles } = useFileSelection(); const { setMode } = useNavigation(); - const [currentStep, setCurrentStep] = useState<'selection' | 'creation' | 'run'>(AUTOMATION_STEPS.SELECTION); + const [currentStep, setCurrentStep] = useState(AUTOMATION_STEPS.SELECTION); const [stepData, setStepData] = useState({ step: AUTOMATION_STEPS.SELECTION }); const automateOperation = useAutomateOperation(); @@ -64,7 +64,7 @@ const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { const renderCurrentStep = () => { switch (currentStep) { - case 'selection': + case AUTOMATION_STEPS.SELECTION: return ( { /> ); - case 'creation': + case AUTOMATION_STEPS.CREATION: if (!stepData.mode) { console.error('Creation mode is undefined'); return null; @@ -100,7 +100,7 @@ const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { /> ); - case 'run': + case AUTOMATION_STEPS.RUN: if (!stepData.automation) { console.error('Automation config is undefined'); return null; diff --git a/frontend/src/types/automation.ts b/frontend/src/types/automation.ts index ffbfe36b2..8d2cb5ae8 100644 --- a/frontend/src/types/automation.ts +++ b/frontend/src/types/automation.ts @@ -24,8 +24,10 @@ export interface AutomationTool { parameters?: Record; } +export type AutomationStep = typeof import('../constants/automation').AUTOMATION_STEPS[keyof typeof import('../constants/automation').AUTOMATION_STEPS]; + export interface AutomationStepData { - step: 'selection' | 'creation' | 'run'; + step: AutomationStep; mode?: AutomationMode; automation?: AutomationConfig; } From 1cc803545a432009723fb69daa45dabca0d39e35 Mon Sep 17 00:00:00 2001 From: ConnorYoh <40631091+ConnorYoh@users.noreply.github.com> Date: Tue, 26 Aug 2025 10:09:49 +0100 Subject: [PATCH 4/6] Dynamic upload/add files button for toolstep (#4284) Co-authored-by: Connor Yoh --- .../public/locales/en-GB/translation.json | 6 +- .../tools/shared/FileStatusIndicator.tsx | 127 ++++++++++++++---- 2 files changed, 104 insertions(+), 29 deletions(-) diff --git a/frontend/public/locales/en-GB/translation.json b/frontend/public/locales/en-GB/translation.json index 2c9a0a6cd..712441a81 100644 --- a/frontend/public/locales/en-GB/translation.json +++ b/frontend/public/locales/en-GB/translation.json @@ -48,7 +48,11 @@ "filesSelected": "{{count}} files selected", "files": { "title": "Files", - "placeholder": "Select a PDF file in the main view to get started" + "placeholder": "Select a PDF file in the main view to get started", + "upload": "Upload", + "addFiles": "Add files", + "noFiles": "No files uploaded. ", + "selectFromWorkbench": "Select files from the workbench or " }, "noFavourites": "No favourites added", "downloadComplete": "Download Complete", diff --git a/frontend/src/components/tools/shared/FileStatusIndicator.tsx b/frontend/src/components/tools/shared/FileStatusIndicator.tsx index 20e7c4d7a..ca56b3c22 100644 --- a/frontend/src/components/tools/shared/FileStatusIndicator.tsx +++ b/frontend/src/components/tools/shared/FileStatusIndicator.tsx @@ -1,9 +1,11 @@ -import React from "react"; +import React, { useState, useEffect } from "react"; import { Text, Anchor } from "@mantine/core"; import { useTranslation } from "react-i18next"; import FolderIcon from '@mui/icons-material/Folder'; +import UploadIcon from '@mui/icons-material/Upload'; import { useFilesModalContext } from "../../../contexts/FilesModalContext"; import { useAllFiles } from "../../../contexts/FileContext"; +import { useFileManager } from "../../../hooks/useFileManager"; export interface FileStatusIndicatorProps { selectedFiles?: File[]; @@ -15,41 +17,110 @@ const FileStatusIndicator = ({ placeholder, }: FileStatusIndicatorProps) => { const { t } = useTranslation(); - const { openFilesModal } = useFilesModalContext(); + const { openFilesModal, onFilesSelect } = useFilesModalContext(); const { files: workbenchFiles } = useAllFiles(); + const { loadRecentFiles } = useFileManager(); + const [hasRecentFiles, setHasRecentFiles] = useState(null); + + // Check if there are recent files + useEffect(() => { + const checkRecentFiles = async () => { + try { + const recentFiles = await loadRecentFiles(); + setHasRecentFiles(recentFiles.length > 0); + } catch (error) { + setHasRecentFiles(false); + } + }; + checkRecentFiles(); + }, [loadRecentFiles]); + + // Handle native file picker + const handleNativeUpload = () => { + const input = document.createElement('input'); + input.type = 'file'; + input.multiple = true; + input.accept = '.pdf,application/pdf'; + input.onchange = (event) => { + const files = Array.from((event.target as HTMLInputElement).files || []); + if (files.length > 0) { + onFilesSelect(files); + } + }; + input.click(); + }; + + // Don't render until we know if there are recent files + if (hasRecentFiles === null) { + return null; + } // Check if there are no files in the workbench if (workbenchFiles.length === 0) { - return ( - - {t("files.noFiles", "No files uploaded. ")}{" "} - - - {t("files.addFiles", "Add files")} - - - ); + // If no recent files, show upload button + if (!hasRecentFiles) { + return ( + + + + {t("files.upload", "Upload")} + + + ); + } else { + // If there are recent files, show add files button + return ( + + + + {t("files.addFiles", "Add files")} + + + ); + } } // Show selection status when there are files in workbench if (selectedFiles.length === 0) { - return ( - - {t("files.selectFromWorkbench", "Select files from the workbench or ") + " "} - - - {t("files.addFiles", "Add files")} - - - ); + // If no recent files, show upload option + if (!hasRecentFiles) { + return ( + + {t("files.selectFromWorkbench", "Select files from the workbench or ") + " "} + + + {t("files.upload", "Upload")} + + + ); + } else { + // If there are recent files, show add files option + return ( + + {t("files.selectFromWorkbench", "Select files from the workbench or ") + " "} + + + {t("files.addFiles", "Add files")} + + + ); + } } return ( From 4b70ef12985c26c733f1e76ccc8b3af476f93a4e Mon Sep 17 00:00:00 2001 From: ConnorYoh <40631091+ConnorYoh@users.noreply.github.com> Date: Tue, 26 Aug 2025 10:12:15 +0100 Subject: [PATCH 5/6] Moved scroll out to tool panel and works better with zoom (#4282) Co-authored-by: Connor Yoh --- frontend/src/components/tools/ToolPanel.tsx | 19 +++++++++++-------- .../tools/shared/createToolFlow.tsx | 3 ++- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/frontend/src/components/tools/ToolPanel.tsx b/frontend/src/components/tools/ToolPanel.tsx index a846978d6..69a012690 100644 --- a/frontend/src/components/tools/ToolPanel.tsx +++ b/frontend/src/components/tools/ToolPanel.tsx @@ -8,6 +8,7 @@ import ToolRenderer from './ToolRenderer'; import ToolSearch from './toolPicker/ToolSearch'; import { useSidebarContext } from "../../contexts/SidebarContext"; import rainbowStyles from '../../styles/rainbow.module.css'; +import { Stack, ScrollArea } from '@mantine/core'; // No props needed - component uses context @@ -91,15 +92,17 @@ export default function ToolPanel() {
) : ( // Selected Tool Content View -
+
{/* Tool content */} -
- {selectedToolKey && ( - - )} +
+ + {selectedToolKey && ( + + )} +
)} diff --git a/frontend/src/components/tools/shared/createToolFlow.tsx b/frontend/src/components/tools/shared/createToolFlow.tsx index d523ff6f6..da2503b4e 100644 --- a/frontend/src/components/tools/shared/createToolFlow.tsx +++ b/frontend/src/components/tools/shared/createToolFlow.tsx @@ -65,7 +65,8 @@ export function createToolFlow(config: ToolFlowConfig) { const steps = createToolSteps(); return ( - + + {/* */} {config.title && } 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 6/6] 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}`); + } + }} /> );