mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-08-26 06:09:23 +00:00
Selection and creation
This commit is contained in:
parent
0ac5b20488
commit
775723f560
@ -22,8 +22,14 @@ import ToolConfigurationModal from './ToolConfigurationModal';
|
||||
import ToolSelector from './ToolSelector';
|
||||
import AutomationEntry from './AutomationEntry';
|
||||
|
||||
export enum AutomationMode {
|
||||
CREATE = 'create',
|
||||
EDIT = 'edit',
|
||||
SUGGESTED = 'suggested'
|
||||
}
|
||||
|
||||
interface AutomationCreationProps {
|
||||
mode: 'custom' | 'suggested' | 'create';
|
||||
mode: AutomationMode;
|
||||
existingAutomation?: any;
|
||||
onBack: () => void;
|
||||
onComplete: (automation: any) => void;
|
||||
@ -49,19 +55,24 @@ export default function AutomationCreation({ mode, existingAutomation, onBack, o
|
||||
|
||||
// Initialize based on mode and existing automation
|
||||
useEffect(() => {
|
||||
if (mode === 'suggested' && existingAutomation) {
|
||||
setAutomationName(existingAutomation.name);
|
||||
if ((mode === AutomationMode.SUGGESTED || mode === AutomationMode.EDIT) && existingAutomation) {
|
||||
setAutomationName(existingAutomation.name || '');
|
||||
|
||||
const tools = existingAutomation.operations.map((op: string) => ({
|
||||
id: `${op}-${Date.now()}`,
|
||||
operation: op,
|
||||
name: getToolName(op),
|
||||
configured: false,
|
||||
parameters: {}
|
||||
}));
|
||||
// Handle both string array (suggested) and object array (custom) operations
|
||||
const operations = existingAutomation.operations || [];
|
||||
const tools = operations.map((op: any, index: number) => {
|
||||
const operation = typeof op === 'string' ? op : op.operation;
|
||||
return {
|
||||
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);
|
||||
} else if (mode === 'create' && selectedTools.length === 0) {
|
||||
} else if (mode === AutomationMode.CREATE && selectedTools.length === 0) {
|
||||
// Initialize with 2 empty tools for new automation
|
||||
const defaultTools = [
|
||||
{
|
||||
@ -217,60 +228,72 @@ export default function AutomationCreation({ mode, existingAutomation, onBack, o
|
||||
style={{
|
||||
border: '1px solid var(--mantine-color-gray-2)',
|
||||
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' }}>
|
||||
{/* Tool Selection Dropdown */}
|
||||
<ToolSelector
|
||||
key={`tool-selector-${tool.id}`}
|
||||
onSelect={(newOperation) => {
|
||||
const updatedTools = [...selectedTools];
|
||||
updatedTools[index] = {
|
||||
...updatedTools[index],
|
||||
operation: newOperation,
|
||||
name: getToolName(newOperation),
|
||||
configured: false,
|
||||
parameters: {}
|
||||
};
|
||||
setSelectedTools(updatedTools);
|
||||
}}
|
||||
excludeTools={['automate']}
|
||||
toolRegistry={toolRegistry}
|
||||
selectedValue={tool.operation}
|
||||
placeholder={tool.name}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ paddingRight: '1.25rem' }}>
|
||||
{/* Tool Selection Dropdown with inline settings cog */}
|
||||
<Group gap="xs" align="center" wrap="nowrap">
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<ToolSelector
|
||||
key={`tool-selector-${tool.id}`}
|
||||
onSelect={(newOperation) => {
|
||||
const updatedTools = [...selectedTools];
|
||||
updatedTools[index] = {
|
||||
...updatedTools[index],
|
||||
operation: newOperation,
|
||||
name: getToolName(newOperation),
|
||||
configured: false,
|
||||
parameters: {}
|
||||
};
|
||||
setSelectedTools(updatedTools);
|
||||
}}
|
||||
excludeTools={['automate']}
|
||||
toolRegistry={toolRegistry}
|
||||
selectedValue={tool.operation}
|
||||
placeholder={tool.name}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Group gap="xs" style={{ flexShrink: 0 }}>
|
||||
{tool.configured ? (
|
||||
<CheckIcon style={{ fontSize: 14, color: 'green' }} />
|
||||
) : (
|
||||
<CloseIcon style={{ fontSize: 14, color: 'orange' }} />
|
||||
{/* Settings cog - only show if tool is selected, aligned right */}
|
||||
{tool.operation && (
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
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>
|
||||
|
||||
{/* 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>
|
||||
|
||||
{index < selectedTools.length - 1 && (
|
||||
|
@ -1,6 +1,9 @@
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
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 {
|
||||
/** Optional title for the automation (usually for custom ones) */
|
||||
@ -13,6 +16,12 @@ interface AutomationEntryProps {
|
||||
onClick: () => void;
|
||||
/** Whether to keep the icon at normal color (for special cases like "Add New") */
|
||||
keepIconColor?: boolean;
|
||||
/** Show menu for saved/suggested automations */
|
||||
showMenu?: boolean;
|
||||
/** Edit handler */
|
||||
onEdit?: () => void;
|
||||
/** Delete handler */
|
||||
onDelete?: () => void;
|
||||
}
|
||||
|
||||
export default function AutomationEntry({
|
||||
@ -20,9 +29,17 @@ export default function AutomationEntry({
|
||||
badgeIcon: BadgeIcon,
|
||||
operations,
|
||||
onClick,
|
||||
keepIconColor = false
|
||||
keepIconColor = false,
|
||||
showMenu = false,
|
||||
onEdit,
|
||||
onDelete
|
||||
}: AutomationEntryProps) {
|
||||
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 = () => {
|
||||
if (title) {
|
||||
@ -32,11 +49,11 @@ export default function AutomationEntry({
|
||||
{BadgeIcon && (
|
||||
<BadgeIcon
|
||||
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}
|
||||
</Text>
|
||||
</Group>
|
||||
@ -48,14 +65,14 @@ export default function AutomationEntry({
|
||||
{BadgeIcon && (
|
||||
<BadgeIcon
|
||||
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 }}>
|
||||
{operations.map((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)}
|
||||
</Text>
|
||||
|
||||
@ -73,20 +90,74 @@ export default function AutomationEntry({
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="subtle"
|
||||
fullWidth
|
||||
onClick={onClick}
|
||||
<Box
|
||||
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',
|
||||
justifyContent: 'flex-start',
|
||||
display: 'flex'
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
onClick={onClick}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
>
|
||||
<div style={{ width: '100%', display: 'flex', justifyContent: 'flex-start' }}>
|
||||
{renderContent()}
|
||||
</div>
|
||||
</Button>
|
||||
<Group gap="md" align="center" justify="space-between" style={{ width: '100%' }}>
|
||||
<div style={{ flex: 1, display: 'flex', justifyContent: 'flex-start' }}>
|
||||
{renderContent()}
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
@ -5,17 +5,23 @@ 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 { useSavedAutomations } from "../../../hooks/tools/automate/useSavedAutomations";
|
||||
|
||||
interface AutomationSelectionProps {
|
||||
onSelectCustom: () => void;
|
||||
onSelectSuggested: (automation: any) => void;
|
||||
savedAutomations: any[];
|
||||
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 { savedAutomations } = useSavedAutomations();
|
||||
const suggestedAutomations = useSuggestedAutomations();
|
||||
|
||||
return (
|
||||
@ -39,7 +45,10 @@ export default function AutomationSelection({ onSelectCustom, onSelectSuggested,
|
||||
title={automation.name}
|
||||
badgeIcon={SettingsIcon}
|
||||
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' />
|
||||
@ -55,7 +64,7 @@ export default function AutomationSelection({ onSelectCustom, onSelectSuggested,
|
||||
key={automation.id}
|
||||
badgeIcon={automation.icon}
|
||||
operations={automation.operations}
|
||||
onClick={() => onSelectSuggested(automation)}
|
||||
onClick={() => onRun(automation)}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
|
@ -128,10 +128,10 @@ export default function ToolConfigurationModal({ opened, tool, onSave, onCancel
|
||||
if (tool.parameters) {
|
||||
setParameters(tool.parameters);
|
||||
} 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 {
|
||||
const defaultParams = parameterHook();
|
||||
setParameters(defaultParams.parameters || {});
|
||||
const defaultParams = parameterHook.defaultParameters || {};
|
||||
setParameters(defaultParams);
|
||||
} catch (error) {
|
||||
console.warn(`Error getting default parameters for ${tool.operation}:`, error);
|
||||
setParameters({});
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import React, { useState, useMemo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Menu, Stack, Text, ScrollArea } from '@mantine/core';
|
||||
import { ToolRegistryEntry } from '../../../data/toolsTaxonomy';
|
||||
@ -14,10 +14,10 @@ interface ToolSelectorProps {
|
||||
placeholder?: string; // Custom placeholder text
|
||||
}
|
||||
|
||||
export default function ToolSelector({
|
||||
onSelect,
|
||||
excludeTools = [],
|
||||
toolRegistry,
|
||||
export default function ToolSelector({
|
||||
onSelect,
|
||||
excludeTools = [],
|
||||
toolRegistry,
|
||||
selectedValue,
|
||||
placeholder
|
||||
}: ToolSelectorProps) {
|
||||
@ -35,7 +35,7 @@ export default function ToolSelector({
|
||||
if (!searchTerm.trim()) {
|
||||
return baseFilteredTools;
|
||||
}
|
||||
|
||||
|
||||
const lowercaseSearch = searchTerm.toLowerCase();
|
||||
return baseFilteredTools.filter(([key, tool]) => {
|
||||
return (
|
||||
@ -57,28 +57,34 @@ export default function ToolSelector({
|
||||
|
||||
// Use the same tool sections logic as the main ToolPicker
|
||||
const { sections, searchGroups } = useToolSections(filteredTools);
|
||||
|
||||
|
||||
// Determine what to display: search results or organized sections
|
||||
const isSearching = searchTerm.trim().length > 0;
|
||||
const displayGroups = useMemo(() => {
|
||||
if (isSearching) {
|
||||
return searchGroups || [];
|
||||
}
|
||||
|
||||
|
||||
if (!sections || sections.length === 0) {
|
||||
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]);
|
||||
|
||||
const handleToolSelect = (toolKey: string) => {
|
||||
const handleToolSelect = useCallback((toolKey: string) => {
|
||||
onSelect(toolKey);
|
||||
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 = () => {
|
||||
setOpened(true);
|
||||
@ -100,44 +106,77 @@ export default function ToolSelector({
|
||||
};
|
||||
|
||||
return (
|
||||
<Menu
|
||||
opened={opened}
|
||||
onChange={setOpened}
|
||||
width="300px"
|
||||
position="bottom-start"
|
||||
withinPortal
|
||||
>
|
||||
<Menu.Target>
|
||||
<div onClick={handleSearchFocus} style={{ cursor: 'text' }}>
|
||||
<ToolSearch
|
||||
value={searchTerm}
|
||||
onChange={handleSearchChange}
|
||||
toolRegistry={filteredToolRegistry}
|
||||
mode="filter"
|
||||
placeholder={getDisplayValue()}
|
||||
hideIcon={true}
|
||||
/>
|
||||
</div>
|
||||
</Menu.Target>
|
||||
<div style={{ position: 'relative', width: '100%' }}>
|
||||
<Menu
|
||||
opened={opened}
|
||||
onChange={(isOpen) => {
|
||||
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 }}
|
||||
>
|
||||
<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}>
|
||||
<Stack gap="sm" p="sm">
|
||||
{displayGroups.length === 0 ? (
|
||||
<Text size="sm" c="dimmed" ta="center" p="md">
|
||||
{isSearching
|
||||
{isSearching
|
||||
? t('tools.noSearchResults', 'No tools found')
|
||||
: t('tools.noTools', 'No tools available')
|
||||
}
|
||||
</Text>
|
||||
) : (
|
||||
displayGroups.map((subcategory) =>
|
||||
renderToolButtons(subcategory, null, handleToolSelect, !isSearching)
|
||||
)
|
||||
renderedTools
|
||||
)}
|
||||
</Stack>
|
||||
</ScrollArea>
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -14,21 +14,24 @@ interface ToolSearchProps {
|
||||
selectedToolKey?: string | null;
|
||||
placeholder?: string;
|
||||
hideIcon?: boolean;
|
||||
onFocus?: () => void;
|
||||
}
|
||||
|
||||
const ToolSearch = ({
|
||||
value,
|
||||
onChange,
|
||||
toolRegistry,
|
||||
onToolSelect,
|
||||
const ToolSearch = ({
|
||||
value,
|
||||
onChange,
|
||||
toolRegistry,
|
||||
onToolSelect,
|
||||
mode = 'filter',
|
||||
selectedToolKey,
|
||||
placeholder,
|
||||
hideIcon = false
|
||||
hideIcon = false,
|
||||
onFocus
|
||||
}: ToolSearchProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||
const searchRef = useRef<HTMLInputElement>(null);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const filteredTools = useMemo(() => {
|
||||
if (!value.trim()) return [];
|
||||
@ -51,7 +54,12 @@ const ToolSearch = ({
|
||||
|
||||
useEffect(() => {
|
||||
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);
|
||||
}
|
||||
};
|
||||
@ -68,6 +76,7 @@ const ToolSearch = ({
|
||||
placeholder={placeholder || t("toolPicker.searchPlaceholder", "Search tools...")}
|
||||
icon={hideIcon ? undefined : <span className="material-symbols-rounded">search</span>}
|
||||
autoComplete="off"
|
||||
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@ -81,19 +90,19 @@ const ToolSearch = ({
|
||||
{searchInput}
|
||||
{dropdownOpen && filteredTools.length > 0 && (
|
||||
<div
|
||||
ref={dropdownRef}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '100%',
|
||||
left: 0,
|
||||
right: 0,
|
||||
zIndex: 1000,
|
||||
backgroundColor: 'var(--bg-toolbar)',
|
||||
border: '1px solid var(--border-default)',
|
||||
borderRadius: '8px',
|
||||
marginTop: '4px',
|
||||
backgroundColor: 'var(--mantine-color-body)',
|
||||
border: '1px solid var(--mantine-color-gray-3)',
|
||||
borderRadius: '6px',
|
||||
boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)',
|
||||
maxHeight: '300px',
|
||||
overflowY: 'auto',
|
||||
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)'
|
||||
overflowY: 'auto'
|
||||
}}
|
||||
>
|
||||
<Stack gap="xs" style={{ padding: '8px' }}>
|
||||
@ -101,7 +110,10 @@ const ToolSearch = ({
|
||||
<Button
|
||||
key={id}
|
||||
variant="subtle"
|
||||
onClick={() => onToolSelect && onToolSelect(id)}
|
||||
onClick={() => {
|
||||
onToolSelect && onToolSelect(id);
|
||||
setDropdownOpen(false);
|
||||
}}
|
||||
leftSection={
|
||||
<div style={{ color: 'var(--tools-text-and-icon-color)' }}>
|
||||
{tool.icon}
|
||||
@ -130,4 +142,4 @@ const ToolSearch = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default ToolSearch;
|
||||
export default ToolSearch;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import SplitPdfPanel from "../tools/Split";
|
||||
import CompressPdfPanel from "../tools/Compress";
|
||||
@ -21,7 +21,7 @@ import RemoveCertificateSign from '../tools/RemoveCertificateSign';
|
||||
export function useFlatToolRegistry(): ToolRegistry {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return {
|
||||
return useMemo(() => ({
|
||||
// Signing
|
||||
|
||||
"certSign": {
|
||||
@ -618,5 +618,5 @@ export function useFlatToolRegistry(): ToolRegistry {
|
||||
category: ToolCategory.RECOMMENDED_TOOLS,
|
||||
subcategory: SubcategoryId.GENERAL
|
||||
},
|
||||
};
|
||||
}), [t]);
|
||||
}
|
@ -5,12 +5,13 @@ import { useToolFileSelection } from "../contexts/FileSelectionContext";
|
||||
|
||||
import { createToolFlow } from "../components/tools/shared/createToolFlow";
|
||||
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 { useAutomateOperation } from "../hooks/tools/automate/useAutomateOperation";
|
||||
import { BaseToolProps } from "../types/tool";
|
||||
import { useFlatToolRegistry } from "../data/useTranslatedToolRegistry";
|
||||
import { useSavedAutomations } from "../hooks/tools/automate/useSavedAutomations";
|
||||
|
||||
const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||
const { t } = useTranslation();
|
||||
@ -22,6 +23,7 @@ const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||
|
||||
const automateOperation = useAutomateOperation();
|
||||
const toolRegistry = useFlatToolRegistry();
|
||||
const { savedAutomations, deleteAutomation } = useSavedAutomations();
|
||||
|
||||
const handleStepChange = (data: any) => {
|
||||
setStepData(data);
|
||||
@ -40,9 +42,18 @@ const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||
case 'selection':
|
||||
return (
|
||||
<AutomationSelection
|
||||
onSelectCustom={() => handleStepChange({ step: 'creation', mode: 'custom' })}
|
||||
onSelectSuggested={(automation: any) => handleStepChange({ step: 'creation', mode: 'suggested', automation })}
|
||||
onCreateNew={() => handleStepChange({ step: 'creation', mode: 'create' })}
|
||||
savedAutomations={savedAutomations}
|
||||
onCreateNew={() => handleStepChange({ step: 'creation', mode: AutomationMode.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}`);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user