216 lines
9.9 KiB
TypeScript
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;
|