import { FolderTree, Globe, Link2, Play, Trash2 } from 'lucide-react'; import React, { useCallback, useMemo, useState, type DragEvent } from 'react'; import type { Host, Snippet } from '@/domain/models'; import { appendHostConnectScript, getEditableHostConnectScriptIds, getGlobalConnectScripts, getGroupConnectScriptsForHost, removeHostConnectScript, reorderHostConnectScript, } from '@/domain/hostConnectScripts.ts'; import { isScriptSnippet } from '@/domain/snippetScript.ts'; import { cn } from '@/lib/utils'; import { getVaultDropPosition } from '@/components/vault/vaultReorderDrag'; import { VaultEntityIcon, vaultAutomationScriptIconClass, vaultEntityIconSmClass, } from '@/components/vault/VaultEntityIcon'; import { HostDetailsSection } from '../host-details'; import { Button } from '../ui/button'; import { Combobox } from '../ui/combobox'; const CONNECT_QUEUE_DRAG_TYPE = 'application/x-netcatty-connect-script-id'; export interface HostDetailsScriptsSectionProps { host: Host; onHostChange: (host: Host) => void; snippets: Snippet[]; t: (key: string, params?: Record) => string; } function triggerLabel( snippet: Snippet, t: HostDetailsScriptsSectionProps['t'], ): string { if (snippet.trigger === 'onConnect') return t('scripts.trigger.onConnect'); if (snippet.trigger === 'onOutput') return t('scripts.trigger.onOutput'); return t('scripts.trigger.manual'); } function scriptById(snippets: Snippet[], scriptId: string): Snippet | undefined { return snippets.find((snippet) => snippet.id === scriptId && isScriptSnippet(snippet)); } export const HostDetailsScriptsSection: React.FC = ({ host, onHostChange, snippets, t, }) => { const [draggingScriptId, setDraggingScriptId] = useState(null); const [dropIndicator, setDropIndicator] = useState<{ scriptId: string; position: 'before' | 'after' } | null>(null); const scripts = useMemo( () => snippets.filter(isScriptSnippet), [snippets], ); const globalScripts = useMemo( () => getGlobalConnectScripts(snippets), [snippets], ); const queueIds = useMemo( () => getEditableHostConnectScriptIds(host, snippets), [host, snippets], ); const groupScripts = useMemo( () => getGroupConnectScriptsForHost(host, snippets).filter( (script) => !script.id || !queueIds.includes(script.id), ), [host, queueIds, snippets], ); const groupScriptIds = useMemo( () => new Set(groupScripts.map((script) => script.id)), [groupScripts], ); const queuedScripts = useMemo( () => queueIds .map((id) => scriptById(snippets, id)) .filter((snippet): snippet is Snippet => Boolean(snippet)), [queueIds, snippets], ); const linkableScripts = useMemo( () => scripts.filter((script) => { if (!script.id) return false; if (script.targetsAllHosts) return false; if (groupScriptIds.has(script.id)) return false; if (queueIds.includes(script.id)) return false; return true; }), [groupScriptIds, queueIds, scripts], ); const linkOptions = useMemo( () => linkableScripts.map((script) => ({ value: script.id!, label: script.label || t('scripts.running.unnamed'), })), [linkableScripts, t], ); const clearDragState = useCallback(() => { setDraggingScriptId(null); setDropIndicator(null); }, []); if (scripts.length === 0) return null; const handleAddToQueue = (scriptId: string) => { if (!scriptId) return; const snippet = scriptById(snippets, scriptId); if (!snippet) return; onHostChange(appendHostConnectScript(host, scriptId, snippets)); }; const handleRemoveFromQueue = (scriptId: string) => { onHostChange(removeHostConnectScript(host, scriptId, snippets)); }; const handleDragStart = (event: DragEvent, scriptId: string) => { event.dataTransfer.effectAllowed = 'move'; event.dataTransfer.setData(CONNECT_QUEUE_DRAG_TYPE, scriptId); setDraggingScriptId(scriptId); }; const handleDragOver = (event: DragEvent, scriptId: string) => { if (!event.dataTransfer.types.includes(CONNECT_QUEUE_DRAG_TYPE)) return; event.preventDefault(); event.dataTransfer.dropEffect = 'move'; const position = getVaultDropPosition(event.currentTarget, event.clientX, event.clientY, false); setDropIndicator((prev) => ( prev?.scriptId === scriptId && prev.position === position ? prev : { scriptId, position } )); }; const handleDrop = (event: DragEvent, targetScriptId: string) => { event.preventDefault(); const draggedScriptId = event.dataTransfer.getData(CONNECT_QUEUE_DRAG_TYPE); if (!draggedScriptId || draggedScriptId === targetScriptId) { clearDragState(); return; } const position = getVaultDropPosition(event.currentTarget, event.clientX, event.clientY, false); onHostChange(reorderHostConnectScript(host, draggedScriptId, targetScriptId, position, snippets)); clearDragState(); }; return ( } title={t('hostDetails.section.automation')} hint={t('hostDetails.automation.queueHint')} >
{globalScripts.length > 0 ? (

{t('hostDetails.automation.globalScriptsHint')}

{globalScripts.map((script) => (
} />
{script.label || t('scripts.running.unnamed')}
{triggerLabel(script, t)}
))}
) : null} {groupScripts.length > 0 ? (

{t('hostDetails.automation.groupScriptsHint')}

{groupScripts.map((script) => (
} />
{script.label || t('scripts.running.unnamed')}
{script.targetGroups?.join(', ')}
))}
) : null}
{queuedScripts.length === 0 ? (

{t('hostDetails.automation.connectQueueEmpty')}

) : (
{queuedScripts.map((script) => { const scriptId = script.id!; const isDragging = draggingScriptId === scriptId; const indicator = dropIndicator?.scriptId === scriptId ? dropIndicator.position : null; return (
handleDragStart(event, scriptId)} onDragOver={(event) => handleDragOver(event, scriptId)} onDrop={(event) => handleDrop(event, scriptId)} > } />
{script.label || t('scripts.running.unnamed')}
{triggerLabel(script, t)}
); })}
)} {linkOptions.length > 0 ? ( } triggerClassName="h-9" /> ) : null}
); };