ui-nyla/src/components/ActionsPanel/ActionsPanel.tsx

216 lines
9.9 KiB
TypeScript

/**
* ActionsPanel
*
* Displays available workflow actions for copy/paste into templates.
* Groups actions by method and shows parameters + example JSON.
*/
import React, { useState, useMemo, useEffect } from 'react';
import { useWorkflowActions, type WorkflowAction } from '../../hooks/useAutomations';
import { FaSearch, FaCopy, FaChevronDown, FaChevronRight, FaCheck } from 'react-icons/fa';
import { useToast } from '../../contexts/ToastContext';
import styles from './ActionsPanel.module.css';
interface ActionsPanelProps {
/** Callback when action JSON is inserted (optional) */
onInsert?: (actionJson: string) => void;
/** Callback when action JSON is copied (optional) */
onCopy?: (actionJson: string) => void;
}
export const ActionsPanel: React.FC<ActionsPanelProps> = ({ onInsert, onCopy }) => {
const { actions, loading, error, fetchActions } = useWorkflowActions();
const { showSuccess } = useToast();
const [filter, setFilter] = useState('');
const [expandedMethods, setExpandedMethods] = useState<Set<string>>(new Set());
const [expandedAction, setExpandedAction] = useState<string | null>(null);
const [copiedAction, setCopiedAction] = useState<string | null>(null);
useEffect(() => {
fetchActions();
}, [fetchActions]);
// Filter actions by search term
const filteredActions = useMemo(() => {
if (!filter) return actions;
const lower = filter.toLowerCase();
return actions.filter(a =>
a.method.toLowerCase().includes(lower) ||
a.action.toLowerCase().includes(lower) ||
a.description.toLowerCase().includes(lower) ||
a.actionId.toLowerCase().includes(lower)
);
}, [actions, filter]);
// Group actions by method
const groupedActions = useMemo(() => {
const groups: Record<string, WorkflowAction[]> = {};
filteredActions.forEach(action => {
if (!groups[action.method]) {
groups[action.method] = [];
}
groups[action.method].push(action);
});
return groups;
}, [filteredActions]);
// Toggle method expansion
const toggleMethod = (method: string) => {
setExpandedMethods(prev => {
const newSet = new Set(prev);
if (newSet.has(method)) {
newSet.delete(method);
} else {
newSet.add(method);
}
return newSet;
});
};
// Toggle action details
const toggleAction = (actionId: string) => {
setExpandedAction(prev => prev === actionId ? null : actionId);
};
// Copy action JSON to clipboard
const handleCopy = async (action: WorkflowAction) => {
const json = JSON.stringify(action.exampleJson, null, 2);
try {
await navigator.clipboard.writeText(json);
setCopiedAction(action.actionId);
setTimeout(() => setCopiedAction(null), 2000);
showSuccess('JSON kopiert');
onCopy?.(json);
} catch (err) {
console.error('Failed to copy:', err);
}
};
// Insert action JSON
const handleInsert = (action: WorkflowAction) => {
const json = JSON.stringify(action.exampleJson, null, 2);
onInsert?.(json);
};
if (loading) {
return (
<div className={styles.panel}>
<div className={styles.loading}>Lade Actions...</div>
</div>
);
}
if (error) {
return (
<div className={styles.panel}>
<div className={styles.error}>Fehler: {error}</div>
<button className={styles.retryButton} onClick={() => fetchActions()}>
Erneut versuchen
</button>
</div>
);
}
return (
<div className={styles.panel}>
<div className={styles.header}>
<h3 className={styles.title}>Verfügbare Actions</h3>
<div className={styles.searchBox}>
<FaSearch className={styles.searchIcon} />
<input
type="text"
placeholder="Suchen..."
value={filter}
onChange={(e) => setFilter(e.target.value)}
className={styles.searchInput}
/>
</div>
</div>
<div className={styles.actionsList}>
{Object.keys(groupedActions).length === 0 ? (
<div className={styles.empty}>Keine Actions gefunden</div>
) : (
Object.entries(groupedActions).map(([method, methodActions]) => (
<div key={method} className={styles.methodGroup}>
<button
className={styles.methodHeader}
onClick={() => toggleMethod(method)}
>
{expandedMethods.has(method) ? <FaChevronDown /> : <FaChevronRight />}
<span className={styles.methodName}>{method}</span>
<span className={styles.methodCount}>{methodActions.length}</span>
</button>
{expandedMethods.has(method) && (
<div className={styles.methodActions}>
{methodActions.map(action => (
<div key={action.actionId} className={styles.actionItem}>
<div
className={styles.actionHeader}
onClick={() => toggleAction(action.actionId)}
>
<div className={styles.actionInfo}>
<span className={styles.actionName}>{action.action}</span>
<span className={styles.actionDesc}>{action.description}</span>
</div>
<button
className={styles.copyButton}
onClick={(e) => { e.stopPropagation(); handleCopy(action); }}
title="JSON kopieren"
>
{copiedAction === action.actionId ? <FaCheck /> : <FaCopy />}
</button>
</div>
{expandedAction === action.actionId && (
<div className={styles.actionDetails}>
{action.parameters.length > 0 && (
<div className={styles.parameters}>
<h5>Parameter:</h5>
<ul>
{action.parameters.map(param => (
<li key={param.name} className={styles.param}>
<span className={styles.paramName}>
{param.name}
{param.required && <span className={styles.required}>*</span>}
</span>
<span className={styles.paramType}>{param.type}</span>
{param.description && (
<span className={styles.paramDesc}>{param.description}</span>
)}
</li>
))}
</ul>
</div>
)}
<div className={styles.exampleJson}>
<h5>Beispiel JSON:</h5>
<pre>{JSON.stringify(action.exampleJson, null, 2)}</pre>
</div>
{onInsert && (
<button
className={styles.insertButton}
onClick={() => handleInsert(action)}
>
In Template einfügen
</button>
)}
</div>
)}
</div>
))}
</div>
)}
</div>
))
)}
</div>
</div>
);
};
export default ActionsPanel;