mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-08-26 14:19:24 +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 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 && (
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -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({});
|
||||||
|
@ -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';
|
||||||
@ -74,11 +74,17 @@ export default function ToolSelector({
|
|||||||
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,27 +106,61 @@ 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 ? (
|
||||||
@ -131,13 +171,12 @@ export default function ToolSelector({
|
|||||||
}
|
}
|
||||||
</Text>
|
</Text>
|
||||||
) : (
|
) : (
|
||||||
displayGroups.map((subcategory) =>
|
renderedTools
|
||||||
renderToolButtons(subcategory, null, handleToolSelect, !isSearching)
|
|
||||||
)
|
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</Menu.Dropdown>
|
</Menu.Dropdown>
|
||||||
</Menu>
|
</Menu>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
@ -14,6 +14,7 @@ interface ToolSearchProps {
|
|||||||
selectedToolKey?: string | null;
|
selectedToolKey?: string | null;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
hideIcon?: boolean;
|
hideIcon?: boolean;
|
||||||
|
onFocus?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ToolSearch = ({
|
const ToolSearch = ({
|
||||||
@ -24,11 +25,13 @@ const ToolSearch = ({
|
|||||||
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}
|
||||||
|
@ -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]);
|
||||||
}
|
}
|
@ -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}`);
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user