Selection and creation

This commit is contained in:
Connor Yoh 2025-08-20 18:30:33 +01:00
parent 0ac5b20488
commit 775723f560
8 changed files with 311 additions and 146 deletions

View File

@ -22,8 +22,14 @@ import ToolConfigurationModal from './ToolConfigurationModal';
import ToolSelector from './ToolSelector'; import ToolSelector from './ToolSelector';
import AutomationEntry from './AutomationEntry'; import AutomationEntry from './AutomationEntry';
export enum AutomationMode {
CREATE = 'create',
EDIT = 'edit',
SUGGESTED = 'suggested'
}
interface AutomationCreationProps { interface AutomationCreationProps {
mode: 'custom' | 'suggested' | 'create'; mode: AutomationMode;
existingAutomation?: any; existingAutomation?: any;
onBack: () => void; onBack: () => void;
onComplete: (automation: any) => void; onComplete: (automation: any) => void;
@ -49,19 +55,24 @@ export default function AutomationCreation({ mode, existingAutomation, onBack, o
// Initialize based on mode and existing automation // Initialize based on mode and existing automation
useEffect(() => { useEffect(() => {
if (mode === 'suggested' && existingAutomation) { if ((mode === AutomationMode.SUGGESTED || mode === AutomationMode.EDIT) && existingAutomation) {
setAutomationName(existingAutomation.name); setAutomationName(existingAutomation.name || '');
const tools = existingAutomation.operations.map((op: string) => ({ // Handle both string array (suggested) and object array (custom) operations
id: `${op}-${Date.now()}`, const operations = existingAutomation.operations || [];
operation: op, const tools = operations.map((op: any, index: number) => {
name: getToolName(op), const operation = typeof op === 'string' ? op : op.operation;
configured: false, return {
parameters: {} id: `${operation}-${Date.now()}-${index}`,
})); operation: operation,
name: getToolName(operation),
configured: mode === AutomationMode.EDIT ? true : (typeof op === 'object' ? op.configured || false : false),
parameters: typeof op === 'object' ? op.parameters || {} : {}
};
});
setSelectedTools(tools); setSelectedTools(tools);
} else if (mode === 'create' && selectedTools.length === 0) { } else if (mode === AutomationMode.CREATE && selectedTools.length === 0) {
// Initialize with 2 empty tools for new automation // Initialize with 2 empty tools for new automation
const defaultTools = [ const defaultTools = [
{ {
@ -217,60 +228,72 @@ export default function AutomationCreation({ mode, existingAutomation, onBack, o
style={{ style={{
border: '1px solid var(--mantine-color-gray-2)', border: '1px solid var(--mantine-color-gray-2)',
borderRadius: 'var(--mantine-radius-sm)', borderRadius: 'var(--mantine-radius-sm)',
backgroundColor: 'white' position: 'relative',
padding: 'var(--mantine-spacing-xs)'
}} }}
> >
<Group gap="xs" align="center" wrap="nowrap" style={{ width: '100%' }}> {/* Delete X in top right */}
<ActionIcon
variant="subtle"
size="xs"
onClick={() => removeTool(index)}
title={t('automate.creation.tools.remove', 'Remove tool')}
style={{
position: 'absolute',
top: '4px',
right: '4px',
zIndex: 1,
color: 'var(--mantine-color-gray-6)'
}}
>
<CloseIcon style={{ fontSize: 12 }} />
</ActionIcon>
<div style={{ flex: 1, minWidth: 0, overflow: 'hidden' }}> <div style={{ paddingRight: '1.25rem' }}>
{/* Tool Selection Dropdown */} {/* Tool Selection Dropdown with inline settings cog */}
<ToolSelector <Group gap="xs" align="center" wrap="nowrap">
key={`tool-selector-${tool.id}`} <div style={{ flex: 1, minWidth: 0 }}>
onSelect={(newOperation) => { <ToolSelector
const updatedTools = [...selectedTools]; key={`tool-selector-${tool.id}`}
updatedTools[index] = { onSelect={(newOperation) => {
...updatedTools[index], const updatedTools = [...selectedTools];
operation: newOperation, updatedTools[index] = {
name: getToolName(newOperation), ...updatedTools[index],
configured: false, operation: newOperation,
parameters: {} name: getToolName(newOperation),
}; configured: false,
setSelectedTools(updatedTools); parameters: {}
}} };
excludeTools={['automate']} setSelectedTools(updatedTools);
toolRegistry={toolRegistry} }}
selectedValue={tool.operation} excludeTools={['automate']}
placeholder={tool.name} toolRegistry={toolRegistry}
/> selectedValue={tool.operation}
</div> placeholder={tool.name}
/>
</div>
<Group gap="xs" style={{ flexShrink: 0 }}> {/* Settings cog - only show if tool is selected, aligned right */}
{tool.configured ? ( {tool.operation && (
<CheckIcon style={{ fontSize: 14, color: 'green' }} /> <ActionIcon
) : ( variant="subtle"
<CloseIcon style={{ fontSize: 14, color: 'orange' }} /> size="sm"
onClick={() => configureTool(index)}
title={t('automate.creation.tools.configure', 'Configure tool')}
style={{ color: 'var(--mantine-color-gray-6)' }}
>
<SettingsIcon style={{ fontSize: 16 }} />
</ActionIcon>
)} )}
<ActionIcon
variant="subtle"
size="sm"
onClick={() => configureTool(index)}
title={t('automate.creation.tools.configure', 'Configure tool')}
>
<SettingsIcon style={{ fontSize: 16 }} />
</ActionIcon>
<ActionIcon
variant="subtle"
size="sm"
color="red"
onClick={() => removeTool(index)}
title={t('automate.creation.tools.remove', 'Remove tool')}
>
<DeleteIcon style={{ fontSize: 16 }} />
</ActionIcon>
</Group> </Group>
</Group>
{/* Configuration status underneath */}
{tool.operation && !tool.configured && (
<Text pl="md" size="xs" c="dimmed" mt="xs">
{t('automate.creation.tools.notConfigured', "! Not Configured")}
</Text>
)}
</div>
</div> </div>
{index < selectedTools.length - 1 && ( {index < selectedTools.length - 1 && (

View File

@ -1,6 +1,9 @@
import React from 'react'; import React, { useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Button, Group, Text, Badge } from '@mantine/core'; 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';
interface AutomationEntryProps { interface AutomationEntryProps {
/** Optional title for the automation (usually for custom ones) */ /** Optional title for the automation (usually for custom ones) */
@ -13,6 +16,12 @@ interface AutomationEntryProps {
onClick: () => void; onClick: () => void;
/** Whether to keep the icon at normal color (for special cases like "Add New") */ /** Whether to keep the icon at normal color (for special cases like "Add New") */
keepIconColor?: boolean; keepIconColor?: boolean;
/** Show menu for saved/suggested automations */
showMenu?: boolean;
/** Edit handler */
onEdit?: () => void;
/** Delete handler */
onDelete?: () => void;
} }
export default function AutomationEntry({ export default function AutomationEntry({
@ -20,9 +29,17 @@ export default function AutomationEntry({
badgeIcon: BadgeIcon, badgeIcon: BadgeIcon,
operations, operations,
onClick, onClick,
keepIconColor = false keepIconColor = false,
showMenu = false,
onEdit,
onDelete
}: AutomationEntryProps) { }: AutomationEntryProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const [isHovered, setIsHovered] = useState(false);
const [isMenuOpen, setIsMenuOpen] = useState(false);
// Keep item in hovered state if menu is open
const shouldShowHovered = isHovered || isMenuOpen;
const renderContent = () => { const renderContent = () => {
if (title) { if (title) {
@ -32,11 +49,11 @@ export default function AutomationEntry({
{BadgeIcon && ( {BadgeIcon && (
<BadgeIcon <BadgeIcon
style={{ style={{
color: keepIconColor ? 'inherit' : 'var(--mantine-color-text)' color: keepIconColor ? 'var(--mantine-primary-color-filled)' : 'var(--mantine-color-text)'
}} }}
/> />
)} )}
<Text size="sm" style={{ flex: 1, textAlign: 'left', color: 'var(--mantine-color-text)' }}> <Text size="xs" style={{ flex: 1, textAlign: 'left', color: 'var(--mantine-color-text)' }}>
{title} {title}
</Text> </Text>
</Group> </Group>
@ -48,14 +65,14 @@ export default function AutomationEntry({
{BadgeIcon && ( {BadgeIcon && (
<BadgeIcon <BadgeIcon
style={{ style={{
color: keepIconColor ? 'inherit' : 'var(--mantine-color-text)' color: keepIconColor ? 'var(--mantine-primary-color-filled)' : 'var(--mantine-color-text)'
}} }}
/> />
)} )}
<Group gap="xs" justify="flex-start" style={{ flex: 1 }}> <Group gap="xs" justify="flex-start" style={{ flex: 1 }}>
{operations.map((op, index) => ( {operations.map((op, index) => (
<React.Fragment key={`${op}-${index}`}> <React.Fragment key={`${op}-${index}`}>
<Text size="sm" style={{ color: 'var(--mantine-color-text)' }}> <Text size="xs" style={{ color: 'var(--mantine-color-text)' }}>
{t(`${op}.title`, op)} {t(`${op}.title`, op)}
</Text> </Text>
@ -73,20 +90,74 @@ export default function AutomationEntry({
}; };
return ( return (
<Button <Box
variant="subtle"
fullWidth
onClick={onClick}
style={{ style={{
height: 'auto', backgroundColor: shouldShowHovered ? 'var(--mantine-color-gray-1)' : 'transparent',
borderRadius: 'var(--mantine-radius-md)',
transition: 'background-color 0.15s ease',
padding: '0.75rem 1rem', padding: '0.75rem 1rem',
justifyContent: 'flex-start', cursor: 'pointer'
display: 'flex'
}} }}
onClick={onClick}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
> >
<div style={{ width: '100%', display: 'flex', justifyContent: 'flex-start' }}> <Group gap="md" align="center" justify="space-between" style={{ width: '100%' }}>
{renderContent()} <div style={{ flex: 1, display: 'flex', justifyContent: 'flex-start' }}>
</div> {renderContent()}
</Button> </div>
{showMenu && (
<Menu
position="bottom-end"
withinPortal
onOpen={() => setIsMenuOpen(true)}
onClose={() => setIsMenuOpen(false)}
>
<Menu.Target>
<ActionIcon
variant="subtle"
c="dimmed"
size="md"
onClick={(e) => e.stopPropagation()}
style={{
opacity: shouldShowHovered ? 1 : 0,
transform: shouldShowHovered ? 'scale(1)' : 'scale(0.8)',
transition: 'opacity 0.3s ease, transform 0.3s ease',
pointerEvents: shouldShowHovered ? 'auto' : 'none'
}}
>
<MoreVertIcon style={{ fontSize: 20 }} />
</ActionIcon>
</Menu.Target>
<Menu.Dropdown>
{onEdit && (
<Menu.Item
leftSection={<EditIcon style={{ fontSize: 16 }} />}
onClick={(e) => {
e.stopPropagation();
onEdit();
}}
>
{t('common.edit', 'Edit')}
</Menu.Item>
)}
{onDelete && (
<Menu.Item
leftSection={<DeleteIcon style={{ fontSize: 16 }} />}
onClick={(e) => {
e.stopPropagation();
onDelete();
}}
>
{t('common.delete', 'Delete')}
</Menu.Item>
)}
</Menu.Dropdown>
</Menu>
)}
</Group>
</Box>
); );
} }

View File

@ -5,17 +5,23 @@ import AddCircleOutline from "@mui/icons-material/AddCircleOutline";
import SettingsIcon from "@mui/icons-material/Settings"; import SettingsIcon from "@mui/icons-material/Settings";
import AutomationEntry from "./AutomationEntry"; import AutomationEntry from "./AutomationEntry";
import { useSuggestedAutomations } from "../../../hooks/tools/automate/useSuggestedAutomations"; import { useSuggestedAutomations } from "../../../hooks/tools/automate/useSuggestedAutomations";
import { useSavedAutomations } from "../../../hooks/tools/automate/useSavedAutomations";
interface AutomationSelectionProps { interface AutomationSelectionProps {
onSelectCustom: () => void; savedAutomations: any[];
onSelectSuggested: (automation: any) => void;
onCreateNew: () => void; onCreateNew: () => void;
onRun: (automation: any) => void;
onEdit: (automation: any) => void;
onDelete: (automation: any) => void;
} }
export default function AutomationSelection({ onSelectCustom, onSelectSuggested, onCreateNew }: AutomationSelectionProps) { export default function AutomationSelection({
savedAutomations,
onCreateNew,
onRun,
onEdit,
onDelete
}: AutomationSelectionProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const { savedAutomations } = useSavedAutomations();
const suggestedAutomations = useSuggestedAutomations(); const suggestedAutomations = useSuggestedAutomations();
return ( return (
@ -39,7 +45,10 @@ export default function AutomationSelection({ onSelectCustom, onSelectSuggested,
title={automation.name} title={automation.name}
badgeIcon={SettingsIcon} badgeIcon={SettingsIcon}
operations={automation.operations.map(op => typeof op === 'string' ? op : op.operation)} operations={automation.operations.map(op => typeof op === 'string' ? op : op.operation)}
onClick={() => onSelectCustom()} onClick={() => onRun(automation)}
showMenu={true}
onEdit={() => onEdit(automation)}
onDelete={() => onDelete(automation)}
/> />
))} ))}
<Divider pb='sm' /> <Divider pb='sm' />
@ -55,7 +64,7 @@ export default function AutomationSelection({ onSelectCustom, onSelectSuggested,
key={automation.id} key={automation.id}
badgeIcon={automation.icon} badgeIcon={automation.icon}
operations={automation.operations} operations={automation.operations}
onClick={() => onSelectSuggested(automation)} onClick={() => onRun(automation)}
/> />
))} ))}
</Stack> </Stack>

View File

@ -128,10 +128,10 @@ export default function ToolConfigurationModal({ opened, tool, onSave, onCancel
if (tool.parameters) { if (tool.parameters) {
setParameters(tool.parameters); setParameters(tool.parameters);
} else if (parameterHook) { } else if (parameterHook) {
// If we have a parameter hook, use it to get default values // If we have a parameter module, use its default parameters
try { try {
const defaultParams = parameterHook(); const defaultParams = parameterHook.defaultParameters || {};
setParameters(defaultParams.parameters || {}); setParameters(defaultParams);
} catch (error) { } catch (error) {
console.warn(`Error getting default parameters for ${tool.operation}:`, error); console.warn(`Error getting default parameters for ${tool.operation}:`, error);
setParameters({}); setParameters({});

View File

@ -1,4 +1,4 @@
import React, { useState, useMemo } from 'react'; import React, { useState, useMemo, useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Menu, Stack, Text, ScrollArea } from '@mantine/core'; import { Menu, Stack, Text, ScrollArea } from '@mantine/core';
import { ToolRegistryEntry } from '../../../data/toolsTaxonomy'; import { ToolRegistryEntry } from '../../../data/toolsTaxonomy';
@ -14,10 +14,10 @@ interface ToolSelectorProps {
placeholder?: string; // Custom placeholder text placeholder?: string; // Custom placeholder text
} }
export default function ToolSelector({ export default function ToolSelector({
onSelect, onSelect,
excludeTools = [], excludeTools = [],
toolRegistry, toolRegistry,
selectedValue, selectedValue,
placeholder placeholder
}: ToolSelectorProps) { }: ToolSelectorProps) {
@ -35,7 +35,7 @@ export default function ToolSelector({
if (!searchTerm.trim()) { if (!searchTerm.trim()) {
return baseFilteredTools; return baseFilteredTools;
} }
const lowercaseSearch = searchTerm.toLowerCase(); const lowercaseSearch = searchTerm.toLowerCase();
return baseFilteredTools.filter(([key, tool]) => { return baseFilteredTools.filter(([key, tool]) => {
return ( return (
@ -57,28 +57,34 @@ export default function ToolSelector({
// Use the same tool sections logic as the main ToolPicker // Use the same tool sections logic as the main ToolPicker
const { sections, searchGroups } = useToolSections(filteredTools); const { sections, searchGroups } = useToolSections(filteredTools);
// Determine what to display: search results or organized sections // Determine what to display: search results or organized sections
const isSearching = searchTerm.trim().length > 0; const isSearching = searchTerm.trim().length > 0;
const displayGroups = useMemo(() => { const displayGroups = useMemo(() => {
if (isSearching) { if (isSearching) {
return searchGroups || []; return searchGroups || [];
} }
if (!sections || sections.length === 0) { if (!sections || sections.length === 0) {
return []; return [];
} }
// Find the "all" section which contains all tools without duplicates // Find the "all" section which contains all tools without duplicates
const allSection = sections.find(s => (s as any).key === 'all'); const allSection = sections.find(s => (s as any).key === 'all');
return allSection?.subcategories || []; return allSection?.subcategories || [];
}, [isSearching, searchGroups, sections]); }, [isSearching, searchGroups, sections]);
const handleToolSelect = (toolKey: string) => { const handleToolSelect = useCallback((toolKey: string) => {
onSelect(toolKey); onSelect(toolKey);
setOpened(false); setOpened(false);
setSearchTerm(''); setSearchTerm(''); // Clear search to show the selected tool display
}; }, [onSelect]);
const renderedTools = useMemo(() =>
displayGroups.map((subcategory) =>
renderToolButtons(subcategory, null, handleToolSelect, !isSearching)
), [displayGroups, handleToolSelect, isSearching]
);
const handleSearchFocus = () => { const handleSearchFocus = () => {
setOpened(true); setOpened(true);
@ -100,44 +106,77 @@ export default function ToolSelector({
}; };
return ( return (
<Menu <div style={{ position: 'relative', width: '100%' }}>
opened={opened} <Menu
onChange={setOpened} opened={opened}
width="300px" onChange={(isOpen) => {
position="bottom-start" setOpened(isOpen);
withinPortal // Clear search term when menu closes to show proper display
> if (!isOpen) {
<Menu.Target> setSearchTerm('');
<div onClick={handleSearchFocus} style={{ cursor: 'text' }}> }
<ToolSearch }}
value={searchTerm} closeOnClickOutside={true}
onChange={handleSearchChange} closeOnEscape={true}
toolRegistry={filteredToolRegistry} position="bottom-start"
mode="filter" offset={4}
placeholder={getDisplayValue()} withinPortal={false}
hideIcon={true} trapFocus={false}
/> shadow="sm"
</div> transitionProps={{ duration: 0 }}
</Menu.Target> >
<Menu.Target>
<div style={{ width: '100%' }}>
{selectedValue && toolRegistry[selectedValue] && !opened ? (
// Show selected tool in AutomationEntry style when tool is selected and not searching
<div onClick={handleSearchFocus} style={{ cursor: 'pointer' }}>
<div style={{
display: 'flex',
alignItems: 'center',
gap: 'var(--mantine-spacing-sm)',
padding: '0 0.5rem',
borderRadius: 'var(--mantine-radius-sm)',
}}>
<div style={{ color: 'var(--mantine-color-text)', fontSize: '1.2rem' }}>
{toolRegistry[selectedValue].icon}
</div>
<Text size="sm" style={{ flex: 1, color: 'var(--mantine-color-text)' }}>
{toolRegistry[selectedValue].name}
</Text>
</div>
</div>
) : (
// Show search input when no tool selected or actively searching
<ToolSearch
value={searchTerm}
onChange={handleSearchChange}
toolRegistry={filteredToolRegistry}
mode="filter"
placeholder={getDisplayValue()}
hideIcon={true}
onFocus={handleSearchFocus}
/>
)}
</div>
</Menu.Target>
<Menu.Dropdown p={0}> <Menu.Dropdown p={0} style={{ minWidth: '16rem' }}>
<ScrollArea h={350}> <ScrollArea h={350}>
<Stack gap="sm" p="sm"> <Stack gap="sm" p="sm">
{displayGroups.length === 0 ? ( {displayGroups.length === 0 ? (
<Text size="sm" c="dimmed" ta="center" p="md"> <Text size="sm" c="dimmed" ta="center" p="md">
{isSearching {isSearching
? t('tools.noSearchResults', 'No tools found') ? t('tools.noSearchResults', 'No tools found')
: t('tools.noTools', 'No tools available') : t('tools.noTools', 'No tools available')
} }
</Text> </Text>
) : ( ) : (
displayGroups.map((subcategory) => renderedTools
renderToolButtons(subcategory, null, handleToolSelect, !isSearching)
)
)} )}
</Stack> </Stack>
</ScrollArea> </ScrollArea>
</Menu.Dropdown> </Menu.Dropdown>
</Menu> </Menu>
</div>
); );
} }

View File

@ -14,21 +14,24 @@ interface ToolSearchProps {
selectedToolKey?: string | null; selectedToolKey?: string | null;
placeholder?: string; placeholder?: string;
hideIcon?: boolean; hideIcon?: boolean;
onFocus?: () => void;
} }
const ToolSearch = ({ const ToolSearch = ({
value, value,
onChange, onChange,
toolRegistry, toolRegistry,
onToolSelect, onToolSelect,
mode = 'filter', mode = 'filter',
selectedToolKey, selectedToolKey,
placeholder, placeholder,
hideIcon = false hideIcon = false,
onFocus
}: ToolSearchProps) => { }: ToolSearchProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [dropdownOpen, setDropdownOpen] = useState(false); const [dropdownOpen, setDropdownOpen] = useState(false);
const searchRef = useRef<HTMLInputElement>(null); const searchRef = useRef<HTMLInputElement>(null);
const dropdownRef = useRef<HTMLDivElement>(null);
const filteredTools = useMemo(() => { const filteredTools = useMemo(() => {
if (!value.trim()) return []; if (!value.trim()) return [];
@ -51,7 +54,12 @@ const ToolSearch = ({
useEffect(() => { useEffect(() => {
const handleClickOutside = (event: MouseEvent) => { const handleClickOutside = (event: MouseEvent) => {
if (searchRef.current && !searchRef.current.contains(event.target as Node)) { if (
searchRef.current &&
dropdownRef.current &&
!searchRef.current.contains(event.target as Node) &&
!dropdownRef.current.contains(event.target as Node)
) {
setDropdownOpen(false); setDropdownOpen(false);
} }
}; };
@ -68,6 +76,7 @@ const ToolSearch = ({
placeholder={placeholder || t("toolPicker.searchPlaceholder", "Search tools...")} placeholder={placeholder || t("toolPicker.searchPlaceholder", "Search tools...")}
icon={hideIcon ? undefined : <span className="material-symbols-rounded">search</span>} icon={hideIcon ? undefined : <span className="material-symbols-rounded">search</span>}
autoComplete="off" autoComplete="off"
/> />
</div> </div>
); );
@ -81,19 +90,19 @@ const ToolSearch = ({
{searchInput} {searchInput}
{dropdownOpen && filteredTools.length > 0 && ( {dropdownOpen && filteredTools.length > 0 && (
<div <div
ref={dropdownRef}
style={{ style={{
position: 'absolute', position: 'absolute',
top: '100%', top: '100%',
left: 0, left: 0,
right: 0, right: 0,
zIndex: 1000, zIndex: 1000,
backgroundColor: 'var(--bg-toolbar)', backgroundColor: 'var(--mantine-color-body)',
border: '1px solid var(--border-default)', border: '1px solid var(--mantine-color-gray-3)',
borderRadius: '8px', borderRadius: '6px',
marginTop: '4px', boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)',
maxHeight: '300px', maxHeight: '300px',
overflowY: 'auto', overflowY: 'auto'
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)'
}} }}
> >
<Stack gap="xs" style={{ padding: '8px' }}> <Stack gap="xs" style={{ padding: '8px' }}>
@ -101,7 +110,10 @@ const ToolSearch = ({
<Button <Button
key={id} key={id}
variant="subtle" variant="subtle"
onClick={() => onToolSelect && onToolSelect(id)} onClick={() => {
onToolSelect && onToolSelect(id);
setDropdownOpen(false);
}}
leftSection={ leftSection={
<div style={{ color: 'var(--tools-text-and-icon-color)' }}> <div style={{ color: 'var(--tools-text-and-icon-color)' }}>
{tool.icon} {tool.icon}
@ -130,4 +142,4 @@ const ToolSearch = ({
); );
}; };
export default ToolSearch; export default ToolSearch;

View File

@ -1,4 +1,4 @@
import React from 'react'; import React, { useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import SplitPdfPanel from "../tools/Split"; import SplitPdfPanel from "../tools/Split";
import CompressPdfPanel from "../tools/Compress"; import CompressPdfPanel from "../tools/Compress";
@ -21,7 +21,7 @@ import RemoveCertificateSign from '../tools/RemoveCertificateSign';
export function useFlatToolRegistry(): ToolRegistry { export function useFlatToolRegistry(): ToolRegistry {
const { t } = useTranslation(); const { t } = useTranslation();
return { return useMemo(() => ({
// Signing // Signing
"certSign": { "certSign": {
@ -618,5 +618,5 @@ export function useFlatToolRegistry(): ToolRegistry {
category: ToolCategory.RECOMMENDED_TOOLS, category: ToolCategory.RECOMMENDED_TOOLS,
subcategory: SubcategoryId.GENERAL subcategory: SubcategoryId.GENERAL
}, },
}; }), [t]);
} }

View File

@ -5,12 +5,13 @@ import { useToolFileSelection } from "../contexts/FileSelectionContext";
import { createToolFlow } from "../components/tools/shared/createToolFlow"; import { createToolFlow } from "../components/tools/shared/createToolFlow";
import AutomationSelection from "../components/tools/automate/AutomationSelection"; import AutomationSelection from "../components/tools/automate/AutomationSelection";
import AutomationCreation from "../components/tools/automate/AutomationCreation"; import AutomationCreation, { AutomationMode } from "../components/tools/automate/AutomationCreation";
import ToolSequence from "../components/tools/automate/ToolSequence"; import ToolSequence from "../components/tools/automate/ToolSequence";
import { useAutomateOperation } from "../hooks/tools/automate/useAutomateOperation"; import { useAutomateOperation } from "../hooks/tools/automate/useAutomateOperation";
import { BaseToolProps } from "../types/tool"; import { BaseToolProps } from "../types/tool";
import { useFlatToolRegistry } from "../data/useTranslatedToolRegistry"; import { useFlatToolRegistry } from "../data/useTranslatedToolRegistry";
import { useSavedAutomations } from "../hooks/tools/automate/useSavedAutomations";
const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
@ -22,6 +23,7 @@ const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
const automateOperation = useAutomateOperation(); const automateOperation = useAutomateOperation();
const toolRegistry = useFlatToolRegistry(); const toolRegistry = useFlatToolRegistry();
const { savedAutomations, deleteAutomation } = useSavedAutomations();
const handleStepChange = (data: any) => { const handleStepChange = (data: any) => {
setStepData(data); setStepData(data);
@ -40,9 +42,18 @@ const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
case 'selection': case 'selection':
return ( return (
<AutomationSelection <AutomationSelection
onSelectCustom={() => handleStepChange({ step: 'creation', mode: 'custom' })} savedAutomations={savedAutomations}
onSelectSuggested={(automation: any) => handleStepChange({ step: 'creation', mode: 'suggested', automation })} onCreateNew={() => handleStepChange({ step: 'creation', mode: AutomationMode.CREATE })}
onCreateNew={() => handleStepChange({ step: 'creation', mode: 'create' })} onRun={(automation: any) => handleStepChange({ step: 'sequence', automation })}
onEdit={(automation: any) => handleStepChange({ step: 'creation', mode: AutomationMode.EDIT, automation })}
onDelete={async (automation: any) => {
try {
await deleteAutomation(automation.id);
} catch (error) {
console.error('Failed to delete automation:', error);
onError?.(`Failed to delete automation: ${automation.name}`);
}
}}
/> />
); );