/** * RequiredAttributePicker — Phase-4 Schicht-4 binding affordance for * required parameters of a Schicht-3 Adapter (Editor-Node). * * 0/1/N logic, applied on the set of typed source candidates: * - 0 candidates → red pill: "Keine typkompatible Quelle vorhanden" * (user must add an upstream node first) * - 1 candidate → auto-bound chip with a "Andere wählen…" override button * (still shown explicitly so the user sees what was chosen) * - N candidates → "Quelle wählen…" button that opens the DataPicker * pre-filtered to the expected type * * The picker also surfaces a "Iterieren als Loop" hint when the expected type * is `X` and an upstream candidate is `List[X]` — see paramValidation.ts. */ import React, { useMemo, useState } from 'react'; import { useAutomation2DataFlow } from '../../context/Automation2DataFlowContext'; import { DataPicker } from './DataPicker'; import { createRef, formatRefLabel, isRef, type DataRef, type SystemVarRef } from './dataRef'; import { findSourceCandidates, strictlyCompatible, type SourceCandidate } from './paramValidation'; import styles from '../../editor/Automation2FlowEditor.module.css'; import { useLanguage } from '../../../../providers/language/LanguageContext'; export interface RequiredAttributePickerProps { /** Display label for the parameter (already localized). */ label: string; /** Type expected by the bound action argument (e.g. "DocumentList", "str"). */ expectedType?: string; /** Current bound value (DataRef, SystemVarRef, or unset). */ value: unknown; /** Persist a new binding (or `null` to clear). */ onChange: (next: DataRef | SystemVarRef | null) => void; /** Optional description shown beneath the picker. */ description?: React.ReactNode; } export const RequiredAttributePicker: React.FC = ({ label, expectedType, value, onChange, description, }) => { const { t } = useLanguage(); const ctx = useAutomation2DataFlow(); const [pickerOpen, setPickerOpen] = useState(false); const consumerNodeId = ctx?.currentNodeId ?? ''; const nodes = ctx?.nodes ?? []; const connections = ctx?.connections ?? []; const nodeTypes = ctx?.nodeTypes ?? []; const catalog = ctx?.portTypeCatalog ?? {}; const allCandidates: SourceCandidate[] = useMemo(() => { if (!consumerNodeId) return []; return findSourceCandidates({ consumerNodeId, expectedType, nodes, connections: connections.map((c) => ({ id: c.id, sourceId: c.sourceId, sourceHandle: c.sourceHandle, targetId: c.targetId, targetHandle: c.targetHandle, })), nodeTypes, portTypeCatalog: catalog, }); }, [consumerNodeId, expectedType, nodes, connections, nodeTypes, catalog]); const compatibleCandidates = useMemo(() => strictlyCompatible(allCandidates), [allCandidates]); const isBoundRef = isRef(value); const boundLabel = isBoundRef ? formatRefLabel(value as DataRef, nodes) : null; // 0/1/N const candidateCount = compatibleCandidates.length; const single = candidateCount === 1 ? compatibleCandidates[0] : null; const handleAutoBind = () => { if (!single) return; const ref = createRef(single.nodeId, single.iterable && expectedType ? [...single.path, '*'] : single.path, expectedType); onChange(ref); }; const handlePicked = (picked: DataRef | SystemVarRef) => { onChange(picked); }; return (
{/* Header: label always takes the full row (flex-basis 100 %), badge wraps below — prevents long type names like List[ActionDocument] from escaping the panel frame on the right. */}
{expectedType && ( {expectedType} )}
{isBoundRef ? (
{boundLabel}
) : candidateCount === 0 ? (
{t('Keine typkompatible Quelle vorhanden — füge zuerst einen Knoten ein, der ')} {expectedType ?? '?'} {t(' liefert.')}
) : single ? (
) : (
)} {description && (
{description}
)} {pickerOpen && ( setPickerOpen(false)} onPick={(picked) => { handlePicked(picked); setPickerOpen(false); }} availableSourceIds={ctx?.getAvailableSourceIds() ?? []} nodes={nodes} nodeOutputsPreview={ctx?.nodeOutputsPreview ?? {}} getNodeLabel={(n) => ctx?.getNodeLabel(n as { id: string; title?: string; label?: string; type?: string }) ?? n.id } expectedParamType={expectedType} /> )}
); };