ui-nyla/src/components/FlowEditor/nodes/form/FormNodeConfig.tsx
2026-04-10 12:33:32 +02:00

230 lines
8.9 KiB
TypeScript

/**
* Form node config - draggable fields, types, required toggle
*/
import React, { useEffect, useState } from 'react';
import { FaGripVertical, FaTimes } from 'react-icons/fa';
import type { FormField, NodeConfigRendererProps } from '../shared/types';
import { fetchConnections, type UserConnection } from '../../../../api/workflowApi';
import styles from '../../editor/Automation2FlowEditor.module.css';
import { useLanguage } from '../../../../providers/language/LanguageContext';
export const FormNodeConfig: React.FC<NodeConfigRendererProps> = ({ params,
updateParam,
instanceId,
request,
}) => {
const { t } = useLanguage();
const fields = (params.fields as FormField[]) ?? [];
const [connections, setConnections] = useState<UserConnection[]>([]);
const [connectionsLoading, setConnectionsLoading] = useState(false);
useEffect(() => {
if (!instanceId || !request) {
setConnections([]);
return;
}
let cancelled = false;
setConnectionsLoading(true);
fetchConnections(request, instanceId)
.then((rows) => {
if (!cancelled) setConnections(rows.filter((c) => c.authority === 'clickup'));
})
.catch(() => {
if (!cancelled) setConnections([]);
})
.finally(() => {
if (!cancelled) setConnectionsLoading(false);
});
return () => {
cancelled = true;
};
}, [instanceId, request]);
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);
};
const removeField = (index: number) => {
const next = fields.filter((_, i) => i !== index);
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={t('formNodeConfig.zumVerschiebenZiehen')}
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];
const t = e.target.value;
next[i] = {
...next[i],
type: t,
...(t === 'clickup_tasks'
? { clickupStatusOptions: undefined }
: t === 'clickup_status'
? { clickupConnectionId: undefined, clickupListId: undefined }
: {
clickupConnectionId: undefined,
clickupListId: undefined,
clickupStatusOptions: undefined,
}),
};
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>
<option value="clickup_tasks">{t('formNodeConfig.clickupaufgabeReferenz')}</option>
<option value="clickup_status">{t('formNodeConfig.clickupstatusListe')}</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>
<button
type="button"
onClick={() => removeField(i)}
title={t('formNodeConfig.feldEntfernen')}
className={styles.formFieldRemoveButton}
>
<FaTimes />
</button>
</div>
{f.type === 'clickup_status' ? (
<div style={{ marginTop: 8, paddingLeft: 4, width: '100%', fontSize: '0.8rem', color: 'var(--text-secondary, #666)' }}>
{Array.isArray(f.clickupStatusOptions) && f.clickupStatusOptions.length > 0 ? (
<p style={{ margin: '0 0 6px' }}>
Dropdown mit {f.clickupStatusOptions.length} Status aus der ClickUp-Liste (Wert = exakter
Status-Name für die API).
</p>
) : (
<p style={{ margin: '0 0 6px' }}>
Keine Optionen im ClickUp-Knoten Aufgabe erstellen Liste wählen und Formular mit Liste
abgleichen.
</p>
)}
</div>
) : null}
{f.type === 'clickup_tasks' ? (
<div style={{ marginTop: 8, paddingLeft: 4, width: '100%' }}>
<label style={{ display: 'block', fontSize: '0.8rem', marginBottom: 4 }}>
ClickUp-Verbindung
</label>
<select
value={f.clickupConnectionId ?? ''}
onChange={(e) => {
const next = [...fields];
next[i] = { ...next[i], clickupConnectionId: e.target.value };
updateParam('fields', next);
}}
disabled={connectionsLoading || !instanceId}
style={{ width: '100%', marginBottom: 8 }}
>
<option value="">{connectionsLoading ? 'Lade…' : 'Verbindung wählen…'}</option>
{connections.map((c) => (
<option key={c.id} value={c.id}>
{c.externalUsername ?? c.id}
</option>
))}
</select>
<label style={{ display: 'block', fontSize: '0.8rem', marginBottom: 4 }}>
Listen-ID (verknüpfte Liste / Ziel-Liste)
</label>
<input
placeholder={t('formNodeConfig.zBAusClickupurlList123456789')}
value={f.clickupListId ?? ''}
onChange={(e) => {
const next = [...fields];
next[i] = { ...next[i], clickupListId: e.target.value };
updateParam('fields', next);
}}
style={{ width: '100%' }}
/>
<p style={{ fontSize: '0.75rem', color: 'var(--text-secondary, #666)', marginTop: 6 }}>
Liefert beim Ausfüllen denselben Wert wie ein ClickUp-Relationship-Feld:{' '}
<code>{'{ add: [taskId], rem: [] }'}</code> im ClickUp-Node per Datenquelle auf das
Formularfeld mappen.
</p>
</div>
) : null}
</div>
))}
<button
type="button"
onClick={() =>
updateParam('fields', [...fields, { name: '', type: 'string', label: '', required: false }])
}
>
+ Feld
</button>
</div>
</div>
);
};