Design improvemtns to automation customisation

This commit is contained in:
Connor Yoh 2025-08-25 14:37:32 +01:00
parent f7a4aaa766
commit d835dbd510
6 changed files with 129 additions and 106 deletions

View File

@ -30,6 +30,8 @@ export interface TextInputProps {
readOnly?: boolean; readOnly?: boolean;
/** Accessibility label */ /** Accessibility label */
'aria-label'?: string; 'aria-label'?: string;
/** Focus event handler */
onFocus?: () => void;
} }
export const TextInput = forwardRef<HTMLInputElement, TextInputProps>(({ export const TextInput = forwardRef<HTMLInputElement, TextInputProps>(({
@ -45,6 +47,7 @@ export const TextInput = forwardRef<HTMLInputElement, TextInputProps>(({
disabled = false, disabled = false,
readOnly = false, readOnly = false,
'aria-label': ariaLabel, 'aria-label': ariaLabel,
onFocus,
...props ...props
}, ref) => { }, ref) => {
const { colorScheme } = useMantineColorScheme(); const { colorScheme } = useMantineColorScheme();
@ -62,7 +65,7 @@ export const TextInput = forwardRef<HTMLInputElement, TextInputProps>(({
return ( return (
<div className={`${styles.container} ${className}`} style={style}> <div className={`${styles.container} ${className}`} style={style}>
{icon && ( {icon && (
<span <span
className={styles.icon} className={styles.icon}
style={{ color: colorScheme === 'dark' ? '#FFFFFF' : '#6B7382' }} style={{ color: colorScheme === 'dark' ? '#FFFFFF' : '#6B7382' }}
> >
@ -80,6 +83,7 @@ export const TextInput = forwardRef<HTMLInputElement, TextInputProps>(({
disabled={disabled} disabled={disabled}
readOnly={readOnly} readOnly={readOnly}
aria-label={ariaLabel} aria-label={ariaLabel}
onFocus={onFocus}
style={{ style={{
backgroundColor: colorScheme === 'dark' ? '#4B525A' : '#FFFFFF', backgroundColor: colorScheme === 'dark' ? '#4B525A' : '#FFFFFF',
color: colorScheme === 'dark' ? '#FFFFFF' : '#6B7382', color: colorScheme === 'dark' ? '#FFFFFF' : '#6B7382',

View File

@ -1,14 +1,13 @@
import React from 'react'; import React from "react";
import { useTranslation } from 'react-i18next'; import { useTranslation } from "react-i18next";
import { Text, Stack, Group, ActionIcon } from '@mantine/core'; import { Text, Stack, Group, ActionIcon } from "@mantine/core";
import DeleteIcon from '@mui/icons-material/Delete'; import SettingsIcon from "@mui/icons-material/Settings";
import SettingsIcon from '@mui/icons-material/Settings'; import CloseIcon from "@mui/icons-material/Close";
import CloseIcon from '@mui/icons-material/Close'; import AddCircleOutline from "@mui/icons-material/AddCircleOutline";
import AddCircleOutline from '@mui/icons-material/AddCircleOutline'; import { AutomationTool } from "../../../types/automation";
import { AutomationTool } from '../../../types/automation'; import { ToolRegistryEntry } from "../../../data/toolsTaxonomy";
import { ToolRegistryEntry } from '../../../data/toolsTaxonomy'; import ToolSelector from "./ToolSelector";
import ToolSelector from './ToolSelector'; import AutomationEntry from "./AutomationEntry";
import AutomationEntry from './AutomationEntry';
interface ToolListProps { interface ToolListProps {
tools: AutomationTool[]; tools: AutomationTool[];
@ -29,35 +28,39 @@ export default function ToolList({
onToolConfigure, onToolConfigure,
onToolAdd, onToolAdd,
getToolName, getToolName,
getToolDefaultParameters getToolDefaultParameters,
}: ToolListProps) { }: ToolListProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const handleToolSelect = (index: number, newOperation: string) => { const handleToolSelect = (index: number, newOperation: string) => {
const defaultParams = getToolDefaultParameters(newOperation); const defaultParams = getToolDefaultParameters(newOperation);
onToolUpdate(index, { onToolUpdate(index, {
operation: newOperation, operation: newOperation,
name: getToolName(newOperation), name: getToolName(newOperation),
configured: false, configured: false,
parameters: defaultParams parameters: defaultParams,
}); });
}; };
return ( return (
<div> <div>
<Text size="sm" fw={500} mb="xs" style={{ color: 'var(--mantine-color-text)' }}> <Text size="sm" fw={500} mb="xs" style={{ color: "var(--mantine-color-text)" }}>
{t('automate.creation.tools.selected', 'Selected Tools')} ({tools.length}) {t("automate.creation.tools.selected", "Selected Tools")} ({tools.length})
</Text> </Text>
<Stack gap="0"> <Stack gap="0">
{tools.map((tool, index) => ( {tools.map((tool, index) => (
<React.Fragment key={tool.id}> <React.Fragment key={tool.id}>
<div <div
style={{ style={{
border: '1px solid var(--mantine-color-gray-2)', border: "1px solid var(--mantine-color-gray-2)",
borderRadius: 'var(--mantine-radius-sm)', borderRadius: tool.operation && !tool.configured
position: 'relative', ? "var(--mantine-radius-lg) var(--mantine-radius-lg) 0 0"
padding: 'var(--mantine-spacing-xs)' : "var(--mantine-radius-lg)",
backgroundColor: "var(--mantine-color-gray-2)",
position: "relative",
padding: "var(--mantine-spacing-xs)",
borderBottomWidth: tool.operation && !tool.configured ? "0" : "1px",
}} }}
> >
{/* Delete X in top right */} {/* Delete X in top right */}
@ -65,26 +68,26 @@ export default function ToolList({
variant="subtle" variant="subtle"
size="xs" size="xs"
onClick={() => onToolRemove(index)} onClick={() => onToolRemove(index)}
title={t('automate.creation.tools.remove', 'Remove tool')} title={t("automate.creation.tools.remove", "Remove tool")}
style={{ style={{
position: 'absolute', position: "absolute",
top: '4px', top: "4px",
right: '4px', right: "4px",
zIndex: 1, zIndex: 1,
color: 'var(--mantine-color-gray-6)' color: "var(--mantine-color-gray-6)",
}} }}
> >
<CloseIcon style={{ fontSize: 12 }} /> <CloseIcon style={{ fontSize: 16 }} />
</ActionIcon> </ActionIcon>
<div style={{ paddingRight: '1.25rem' }}> <div style={{ paddingRight: "1.25rem" }}>
{/* Tool Selection Dropdown with inline settings cog */} {/* Tool Selection Dropdown with inline settings cog */}
<Group gap="xs" align="center" wrap="nowrap"> <Group gap="xs" align="center" wrap="nowrap">
<div style={{ flex: 1, minWidth: 0 }}> <div style={{ flex: 1, minWidth: 0 }}>
<ToolSelector <ToolSelector
key={`tool-selector-${tool.id}`} key={`tool-selector-${tool.id}`}
onSelect={(newOperation) => handleToolSelect(index, newOperation)} onSelect={(newOperation) => handleToolSelect(index, newOperation)}
excludeTools={['automate']} excludeTools={["automate"]}
toolRegistry={toolRegistry} toolRegistry={toolRegistry}
selectedValue={tool.operation} selectedValue={tool.operation}
placeholder={tool.name} placeholder={tool.name}
@ -97,26 +100,37 @@ export default function ToolList({
variant="subtle" variant="subtle"
size="sm" size="sm"
onClick={() => onToolConfigure(index)} onClick={() => onToolConfigure(index)}
title={t('automate.creation.tools.configure', 'Configure tool')} title={t("automate.creation.tools.configure", "Configure tool")}
style={{ color: 'var(--mantine-color-gray-6)' }} style={{ color: "var(--mantine-color-gray-6)" }}
> >
<SettingsIcon style={{ fontSize: 16 }} /> <SettingsIcon style={{ fontSize: 16 }} />
</ActionIcon> </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>
</div> </div>
{/* Configuration status underneath */}
{tool.operation && !tool.configured && (
<div
style={{
width: "100%",
border: "1px solid var(--mantine-color-gray-2)",
borderTop: "none",
borderRadius: "0 0 var(--mantine-radius-lg) var(--mantine-radius-lg)",
backgroundColor: "var(--active-bg)",
padding: "var(--mantine-spacing-xs)",
}}
>
<Text pl="md" size="xs" >
{t("automate.creation.tools.notConfigured", "! Not Configured")}
</Text>
</div>
)}
{index < tools.length - 1 && ( {index < tools.length - 1 && (
<div style={{ textAlign: 'center', padding: '8px 0' }}> <div style={{ textAlign: "center", padding: "8px 0" }}>
<Text size="xs" c="dimmed"></Text> <Text size="xs" c="dimmed">
</Text>
</div> </div>
)} )}
</React.Fragment> </React.Fragment>
@ -124,19 +138,23 @@ export default function ToolList({
{/* Arrow before Add Tool Button */} {/* Arrow before Add Tool Button */}
{tools.length > 0 && ( {tools.length > 0 && (
<div style={{ textAlign: 'center', padding: '8px 0' }}> <div style={{ textAlign: "center", padding: "8px 0" }}>
<Text size="xs" c="dimmed"></Text> <Text size="xs" c="dimmed">
</Text>
</div> </div>
)} )}
{/* Add Tool Button */} {/* Add Tool Button */}
<div style={{ <div
border: '1px solid var(--mantine-color-gray-2)', style={{
borderRadius: 'var(--mantine-radius-sm)', border: "1px solid var(--mantine-color-gray-2)",
overflow: 'hidden' borderRadius: "var(--mantine-radius-sm)",
}}> overflow: "hidden",
}}
>
<AutomationEntry <AutomationEntry
title={t('automate.creation.tools.addTool', 'Add Tool')} title={t("automate.creation.tools.addTool", "Add Tool")}
badgeIcon={AddCircleOutline} badgeIcon={AddCircleOutline}
operations={[]} operations={[]}
onClick={onToolAdd} onClick={onToolAdd}
@ -146,4 +164,4 @@ export default function ToolList({
</Stack> </Stack>
</div> </div>
); );
} }

View File

@ -5,6 +5,7 @@ import { ToolRegistryEntry } from '../../../data/toolsTaxonomy';
import { useToolSections } from '../../../hooks/useToolSections'; import { useToolSections } from '../../../hooks/useToolSections';
import { renderToolButtons } from '../shared/renderToolButtons'; import { renderToolButtons } from '../shared/renderToolButtons';
import ToolSearch from '../toolPicker/ToolSearch'; import ToolSearch from '../toolPicker/ToolSearch';
import ToolButton from '../toolPicker/ToolButton';
interface ToolSelectorProps { interface ToolSelectorProps {
onSelect: (toolKey: string) => void; onSelect: (toolKey: string) => void;
@ -72,7 +73,8 @@ export default function ToolSelector({
if (baseFilteredTools.length > 0) { if (baseFilteredTools.length > 0) {
return [{ return [{
name: 'All Tools', name: 'All Tools',
tools: baseFilteredTools.map(([key, tool]) => ({ key, ...tool })) subcategoryId: 'all' as any,
tools: baseFilteredTools.map(([key, tool]) => ({ id: key, tool }))
}]; }];
} }
return []; return [];
@ -140,26 +142,15 @@ export default function ToolSelector({
}; };
return ( return (
<div ref={containerRef} style={{ position: 'relative', width: '100%' }}> <div ref={containerRef} className='rounded-xl'>
{/* Always show the target - either selected tool or search input */} {/* Always show the target - either selected tool or search input */}
<div style={{ width: '100%' }}>
{selectedValue && toolRegistry[selectedValue] && !opened ? ( {selectedValue && toolRegistry[selectedValue] && !opened ? (
// Show selected tool in AutomationEntry style when tool is selected and dropdown closed // Show selected tool in AutomationEntry style when tool is selected and dropdown closed
<div onClick={handleSearchFocus} style={{ cursor: 'pointer' }}> <div onClick={handleSearchFocus} style={{ cursor: 'pointer',
<div style={{ borderRadius: "var(--mantine-radius-lg)" }}>
display: 'flex', <ToolButton id='tool' tool={toolRegistry[selectedValue]} isSelected={false}
alignItems: 'center', onSelect={()=>{}} rounded={true}></ToolButton>
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> </div>
) : ( ) : (
// Show search input when no tool selected OR when dropdown is opened // Show search input when no tool selected OR when dropdown is opened
@ -167,14 +158,13 @@ export default function ToolSelector({
value={searchTerm} value={searchTerm}
onChange={handleSearchChange} onChange={handleSearchChange}
toolRegistry={filteredToolRegistry} toolRegistry={filteredToolRegistry}
mode="filter" mode="unstyled"
placeholder={getDisplayValue()} placeholder={getDisplayValue()}
hideIcon={true} hideIcon={true}
onFocus={handleInputFocus} onFocus={handleInputFocus}
autoFocus={shouldAutoFocus} autoFocus={shouldAutoFocus}
/> />
)} )}
</div>
{/* Custom dropdown */} {/* Custom dropdown */}
{opened && ( {opened && (

View File

@ -9,9 +9,10 @@ interface ToolButtonProps {
tool: ToolRegistryEntry; tool: ToolRegistryEntry;
isSelected: boolean; isSelected: boolean;
onSelect: (id: string) => void; onSelect: (id: string) => void;
rounded?: boolean;
} }
const ToolButton: React.FC<ToolButtonProps> = ({ id, tool, isSelected, onSelect }) => { const ToolButton: React.FC<ToolButtonProps> = ({ id, tool, isSelected, onSelect, rounded = false }) => {
const handleClick = (id: string) => { const handleClick = (id: string) => {
if (tool.link) { if (tool.link) {
// Open external link in new tab // Open external link in new tab
@ -33,7 +34,17 @@ const ToolButton: React.FC<ToolButtonProps> = ({ id, tool, isSelected, onSelect
fullWidth fullWidth
justify="flex-start" justify="flex-start"
className="tool-button" 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)',
}
})
}
}}
> >
<FitText <FitText
text={tool.name} text={tool.name}

View File

@ -76,4 +76,4 @@
.search-input-container { .search-input-container {
margin-top: 0.5rem; margin-top: 0.5rem;
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
} }

View File

@ -3,14 +3,14 @@ import { Stack, Button, Text } from "@mantine/core";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { ToolRegistryEntry } from "../../../data/toolsTaxonomy"; import { ToolRegistryEntry } from "../../../data/toolsTaxonomy";
import { TextInput } from "../../shared/TextInput"; import { TextInput } from "../../shared/TextInput";
import './ToolPicker.css'; import "./ToolPicker.css";
interface ToolSearchProps { interface ToolSearchProps {
value: string; value: string;
onChange: (value: string) => void; onChange: (value: string) => void;
toolRegistry: Readonly<Record<string, ToolRegistryEntry>>; toolRegistry: Readonly<Record<string, ToolRegistryEntry>>;
onToolSelect?: (toolId: string) => void; onToolSelect?: (toolId: string) => void;
mode: 'filter' | 'dropdown'; mode: "filter" | "dropdown" | "unstyled";
selectedToolKey?: string | null; selectedToolKey?: string | null;
placeholder?: string; placeholder?: string;
hideIcon?: boolean; hideIcon?: boolean;
@ -23,12 +23,12 @@ const ToolSearch = ({
onChange, onChange,
toolRegistry, toolRegistry,
onToolSelect, onToolSelect,
mode = 'filter', mode = "filter",
selectedToolKey, selectedToolKey,
placeholder, placeholder,
hideIcon = false, hideIcon = false,
onFocus, onFocus,
autoFocus = false autoFocus = false,
}: ToolSearchProps) => { }: ToolSearchProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [dropdownOpen, setDropdownOpen] = useState(false); const [dropdownOpen, setDropdownOpen] = useState(false);
@ -39,9 +39,10 @@ const ToolSearch = ({
if (!value.trim()) return []; if (!value.trim()) return [];
return Object.entries(toolRegistry) return Object.entries(toolRegistry)
.filter(([id, tool]) => { .filter(([id, tool]) => {
if (mode === 'dropdown' && id === selectedToolKey) return false; if (mode === "dropdown" && id === selectedToolKey) return false;
return tool.name.toLowerCase().includes(value.toLowerCase()) || return (
tool.description.toLowerCase().includes(value.toLowerCase()); tool.name.toLowerCase().includes(value.toLowerCase()) || tool.description.toLowerCase().includes(value.toLowerCase())
);
}) })
.slice(0, 6) .slice(0, 6)
.map(([id, tool]) => ({ id, tool })); .map(([id, tool]) => ({ id, tool }));
@ -49,7 +50,7 @@ const ToolSearch = ({
const handleSearchChange = (searchValue: string) => { const handleSearchChange = (searchValue: string) => {
onChange(searchValue); onChange(searchValue);
if (mode === 'dropdown') { if (mode === "dropdown") {
setDropdownOpen(searchValue.trim().length > 0 && filteredTools.length > 0); setDropdownOpen(searchValue.trim().length > 0 && filteredTools.length > 0);
} }
}; };
@ -65,8 +66,8 @@ const ToolSearch = ({
setDropdownOpen(false); setDropdownOpen(false);
} }
}; };
document.addEventListener('mousedown', handleClickOutside); document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside); return () => document.removeEventListener("mousedown", handleClickOutside);
}, []); }, []);
// Auto-focus the input when requested // Auto-focus the input when requested
@ -79,7 +80,6 @@ const ToolSearch = ({
}, [autoFocus]); }, [autoFocus]);
const searchInput = ( const searchInput = (
<div className="search-input-container">
<TextInput <TextInput
ref={searchRef} ref={searchRef}
value={value} value={value}
@ -87,36 +87,40 @@ 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"
style={{padding: 0}}
onFocus={onFocus} onFocus={onFocus}
/> />
</div>
); );
if (mode === 'filter') { if (mode === "filter") {
return <div className="search-input-container">{searchInput}</div>;
}
if (mode === "unstyled") {
return searchInput; return searchInput;
} }
return ( return (
<div ref={searchRef} style={{ position: 'relative' }}> <div ref={searchRef} style={{ position: "relative" }}>
{searchInput} {searchInput}
{dropdownOpen && filteredTools.length > 0 && ( {dropdownOpen && filteredTools.length > 0 && (
<div <div
ref={dropdownRef} 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(--mantine-color-body)', backgroundColor: "var(--mantine-color-body)",
border: '1px solid var(--mantine-color-gray-3)', border: "1px solid var(--mantine-color-gray-3)",
borderRadius: '6px', borderRadius: "6px",
boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)', boxShadow: "0 4px 6px rgba(0, 0, 0, 0.1)",
maxHeight: '300px', maxHeight: "300px",
overflowY: 'auto' overflowY: "auto",
}} }}
> >
<Stack gap="xs" style={{ padding: '8px' }}> <Stack gap="xs" style={{ padding: "8px" }}>
{filteredTools.map(({ id, tool }) => ( {filteredTools.map(({ id, tool }) => (
<Button <Button
key={id} key={id}
@ -125,22 +129,18 @@ const ToolSearch = ({
onToolSelect && onToolSelect(id); onToolSelect && onToolSelect(id);
setDropdownOpen(false); setDropdownOpen(false);
}} }}
leftSection={ leftSection={<div style={{ color: "var(--tools-text-and-icon-color)" }}>{tool.icon}</div>}
<div style={{ color: 'var(--tools-text-and-icon-color)' }}>
{tool.icon}
</div>
}
fullWidth fullWidth
justify="flex-start" justify="flex-start"
style={{ style={{
borderRadius: '6px', borderRadius: "6px",
color: 'var(--tools-text-and-icon-color)', color: "var(--tools-text-and-icon-color)",
padding: '8px 12px' padding: "8px 12px",
}} }}
> >
<div style={{ textAlign: 'left' }}> <div style={{ textAlign: "left" }}>
<div style={{ fontWeight: 500 }}>{tool.name}</div> <div style={{ fontWeight: 500 }}>{tool.name}</div>
<Text size="xs" c="dimmed" style={{ marginTop: '2px' }}> <Text size="xs" c="dimmed" style={{ marginTop: "2px" }}>
{tool.description} {tool.description}
</Text> </Text>
</div> </div>