318 lines
11 KiB
TypeScript
318 lines
11 KiB
TypeScript
/**
|
|
* NodeConfigPanel - Configures parameters for input/human nodes.
|
|
* Form fields: draggable, required toggle, layout ohne clipping.
|
|
*/
|
|
|
|
import React, { useState, useEffect } from 'react';
|
|
import { FaGripVertical } from 'react-icons/fa';
|
|
import type { CanvasNode } from './FlowCanvas';
|
|
import type { NodeType } from '../../api/automation2Api';
|
|
import styles from './Automation2FlowEditor.module.css';
|
|
|
|
type FormField = { name?: string; type?: string; label?: string; required?: boolean };
|
|
|
|
interface NodeConfigPanelProps {
|
|
node: CanvasNode | null;
|
|
nodeType: NodeType | undefined;
|
|
language: string;
|
|
onParametersChange: (nodeId: string, parameters: Record<string, unknown>) => void;
|
|
}
|
|
|
|
export const NodeConfigPanel: React.FC<NodeConfigPanelProps> = ({
|
|
node,
|
|
nodeType,
|
|
language,
|
|
onParametersChange,
|
|
}) => {
|
|
const [params, setParams] = useState<Record<string, unknown>>({});
|
|
|
|
useEffect(() => {
|
|
setParams(node?.parameters ?? {});
|
|
}, [node?.id, node?.parameters]);
|
|
|
|
const updateParam = (key: string, value: unknown) => {
|
|
const next = { ...params, [key]: value };
|
|
setParams(next);
|
|
if (node) onParametersChange(node.id, next);
|
|
};
|
|
|
|
if (!node || !node.type.startsWith('input.')) return null;
|
|
const nt = nodeType;
|
|
const getLabel = (text: string | Record<string, string> | undefined) => {
|
|
if (!text) return '';
|
|
if (typeof text === 'string') return text;
|
|
return (text as Record<string, string>)[language] ?? (text as Record<string, string>).en ?? '';
|
|
};
|
|
|
|
const renderConfig = () => {
|
|
switch (node.type) {
|
|
case 'input.form': {
|
|
const fields = (params.fields as FormField[]) ?? [];
|
|
const moveField = (fromIndex: number, toIndex: number) => {
|
|
if (fromIndex < 0 || toIndex < 0 || fromIndex >= fields.length || toIndex >= fields.length) return;
|
|
const next = [...fields];
|
|
const [removed] = next.splice(fromIndex, 1);
|
|
next.splice(toIndex, 0, removed);
|
|
updateParam('fields', next);
|
|
};
|
|
return (
|
|
<div>
|
|
<label>Felder</label>
|
|
<div className={styles.formFieldsList}>
|
|
{fields.map((f, i) => (
|
|
<div
|
|
key={i}
|
|
className={styles.formFieldRow}
|
|
onDragOver={(e) => {
|
|
e.preventDefault();
|
|
e.dataTransfer.dropEffect = 'move';
|
|
}}
|
|
onDrop={(e) => {
|
|
e.preventDefault();
|
|
const from = parseInt(e.dataTransfer.getData('text/plain'), 10);
|
|
if (!Number.isNaN(from) && from !== i) moveField(from, i);
|
|
}}
|
|
>
|
|
<div className={styles.formFieldRowHeader}>
|
|
<span
|
|
className={styles.formFieldDragHandle}
|
|
title="Zum Verschieben ziehen"
|
|
draggable
|
|
onDragStart={(e) => {
|
|
e.dataTransfer.setData('text/plain', String(i));
|
|
e.dataTransfer.effectAllowed = 'move';
|
|
}}
|
|
>
|
|
<FaGripVertical />
|
|
</span>
|
|
<div className={styles.formFieldInputs}>
|
|
<input
|
|
placeholder="name"
|
|
value={f.name ?? ''}
|
|
onChange={(e) => {
|
|
const next = [...fields];
|
|
next[i] = { ...next[i], name: e.target.value };
|
|
updateParam('fields', next);
|
|
}}
|
|
/>
|
|
<input
|
|
placeholder="label"
|
|
value={f.label ?? ''}
|
|
onChange={(e) => {
|
|
const next = [...fields];
|
|
next[i] = { ...next[i], label: e.target.value };
|
|
updateParam('fields', next);
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className={styles.formFieldRowFooter}>
|
|
<select
|
|
value={f.type ?? 'string'}
|
|
onChange={(e) => {
|
|
const next = [...fields];
|
|
next[i] = { ...next[i], type: e.target.value };
|
|
updateParam('fields', next);
|
|
}}
|
|
style={{ width: 'auto', minWidth: 90 }}
|
|
>
|
|
<option value="string">Text</option>
|
|
<option value="number">Number</option>
|
|
<option value="date">Date</option>
|
|
<option value="boolean">Checkbox</option>
|
|
</select>
|
|
<label className={styles.formFieldRequiredLabel}>
|
|
<input
|
|
type="checkbox"
|
|
checked={f.required ?? false}
|
|
onChange={(e) => {
|
|
const next = [...fields];
|
|
next[i] = { ...next[i], required: e.target.checked };
|
|
updateParam('fields', next);
|
|
}}
|
|
/>
|
|
Pflichtfeld
|
|
</label>
|
|
</div>
|
|
</div>
|
|
))}
|
|
<button
|
|
type="button"
|
|
onClick={() => updateParam('fields', [...fields, { name: '', type: 'string', label: '', required: false }])}
|
|
>
|
|
+ Feld
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
case 'input.approval':
|
|
return (
|
|
<>
|
|
<div>
|
|
<label>Titel</label>
|
|
<input
|
|
value={(params.title as string) ?? ''}
|
|
onChange={(e) => updateParam('title', e.target.value)}
|
|
placeholder="Genehmigungstitel"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label>Beschreibung</label>
|
|
<textarea
|
|
value={(params.description as string) ?? ''}
|
|
onChange={(e) => updateParam('description', e.target.value)}
|
|
placeholder="Was genehmigt werden soll"
|
|
/>
|
|
</div>
|
|
</>
|
|
);
|
|
case 'input.upload':
|
|
return (
|
|
<>
|
|
<div>
|
|
<label>Accept (MIME)</label>
|
|
<input
|
|
value={(params.accept as string) ?? ''}
|
|
onChange={(e) => updateParam('accept', e.target.value)}
|
|
placeholder=".pdf,image/*"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label>Max Größe (MB)</label>
|
|
<input
|
|
type="number"
|
|
value={(params.maxSize as number) ?? 10}
|
|
onChange={(e) => updateParam('maxSize', parseFloat(e.target.value) || 0)}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label>
|
|
<input
|
|
type="checkbox"
|
|
checked={(params.multiple as boolean) ?? false}
|
|
onChange={(e) => updateParam('multiple', e.target.checked)}
|
|
/>
|
|
Mehrere Dateien
|
|
</label>
|
|
</div>
|
|
</>
|
|
);
|
|
case 'input.comment':
|
|
return (
|
|
<>
|
|
<div>
|
|
<label>Platzhalter</label>
|
|
<input
|
|
value={(params.placeholder as string) ?? ''}
|
|
onChange={(e) => updateParam('placeholder', e.target.value)}
|
|
placeholder="Kommentar eingeben..."
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label>
|
|
<input
|
|
type="checkbox"
|
|
checked={(params.required as boolean) ?? true}
|
|
onChange={(e) => updateParam('required', e.target.checked)}
|
|
/>
|
|
Pflichtfeld
|
|
</label>
|
|
</div>
|
|
</>
|
|
);
|
|
case 'input.review':
|
|
return (
|
|
<>
|
|
<div>
|
|
<label>Content-Referenz</label>
|
|
<input
|
|
value={(params.contentRef as string) ?? ''}
|
|
onChange={(e) => updateParam('contentRef', e.target.value)}
|
|
placeholder="{{nodeId.field}}"
|
|
/>
|
|
</div>
|
|
</>
|
|
);
|
|
case 'input.selection': {
|
|
const options = (params.options as Array<{ value?: string; label?: string }>) ?? [];
|
|
return (
|
|
<div>
|
|
<label>Optionen</label>
|
|
{options.map((o, i) => (
|
|
<div key={i} style={{ display: 'flex', gap: 4, marginBottom: 4 }}>
|
|
<input
|
|
placeholder="value"
|
|
value={o.value ?? ''}
|
|
onChange={(e) => {
|
|
const next = [...options];
|
|
next[i] = { ...next[i], value: e.target.value };
|
|
updateParam('options', next);
|
|
}}
|
|
/>
|
|
<input
|
|
placeholder="label"
|
|
value={o.label ?? ''}
|
|
onChange={(e) => {
|
|
const next = [...options];
|
|
next[i] = { ...next[i], label: e.target.value };
|
|
updateParam('options', next);
|
|
}}
|
|
/>
|
|
</div>
|
|
))}
|
|
<button type="button" onClick={() => updateParam('options', [...options, { value: '', label: '' }])}>
|
|
+ Option
|
|
</button>
|
|
<div>
|
|
<label>
|
|
<input
|
|
type="checkbox"
|
|
checked={(params.multiple as boolean) ?? false}
|
|
onChange={(e) => updateParam('multiple', e.target.checked)}
|
|
/>
|
|
Mehrfachauswahl
|
|
</label>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
case 'input.confirmation':
|
|
return (
|
|
<>
|
|
<div>
|
|
<label>Frage</label>
|
|
<input
|
|
value={(params.question as string) ?? ''}
|
|
onChange={(e) => updateParam('question', e.target.value)}
|
|
placeholder="Möchten Sie bestätigen?"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label>Bestätigen-Button</label>
|
|
<input
|
|
value={(params.confirmLabel as string) ?? 'Confirm'}
|
|
onChange={(e) => updateParam('confirmLabel', e.target.value)}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label>Ablehnen-Button</label>
|
|
<input
|
|
value={(params.rejectLabel as string) ?? 'Reject'}
|
|
onChange={(e) => updateParam('rejectLabel', e.target.value)}
|
|
/>
|
|
</div>
|
|
</>
|
|
);
|
|
default:
|
|
return <p>Keine Konfiguration für {node.type}</p>;
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className={styles.nodeConfigPanel}>
|
|
<h4>{getLabel(nt?.label) || node.type}</h4>
|
|
{renderConfig()}
|
|
</div>
|
|
);
|
|
};
|