233 lines
9 KiB
TypeScript
233 lines
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>{t('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('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={t('name')}
|
|
value={f.name ?? ''}
|
|
onChange={(e) => {
|
|
const next = [...fields];
|
|
next[i] = { ...next[i], name: e.target.value };
|
|
updateParam('fields', next);
|
|
}}
|
|
/>
|
|
<input
|
|
placeholder={t('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 fieldType = e.target.value;
|
|
next[i] = {
|
|
...next[i],
|
|
type: fieldType,
|
|
...(fieldType === 'clickup_tasks'
|
|
? { clickupStatusOptions: undefined }
|
|
: fieldType === 'clickup_status'
|
|
? { clickupConnectionId: undefined, clickupListId: undefined }
|
|
: {
|
|
clickupConnectionId: undefined,
|
|
clickupListId: undefined,
|
|
clickupStatusOptions: undefined,
|
|
}),
|
|
};
|
|
updateParam('fields', next);
|
|
}}
|
|
style={{ width: 'auto', minWidth: 90 }}
|
|
>
|
|
<option value="string">{t('Text')}</option>
|
|
<option value="number">{t('Zahl')}</option>
|
|
<option value="date">{t('Datum')}</option>
|
|
<option value="boolean">{t('Kontrollkästchen')}</option>
|
|
<option value="clickup_tasks">{t('ClickUp-Aufgabe Referenz')}</option>
|
|
<option value="clickup_status">{t('ClickUp-Status Liste')}</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);
|
|
}}
|
|
/>
|
|
{t('Pflichtfeld')}
|
|
</label>
|
|
<button
|
|
type="button"
|
|
onClick={() => removeField(i)}
|
|
title={t('Feld entfernen')}
|
|
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' }}>
|
|
{t(
|
|
'Dropdown mit {count} Status aus der ClickUp-Liste (Wert = exakter Status-Name für die API).',
|
|
{ count: String(f.clickupStatusOptions.length) }
|
|
)}
|
|
</p>
|
|
) : (
|
|
<p style={{ margin: '0 0 6px' }}>
|
|
{t(
|
|
'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 }}>
|
|
{t('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 ? t('Lade…') : t('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 }}>
|
|
{t('Listen-ID (verknüpfte Liste / Ziel-Liste)')}
|
|
</label>
|
|
<input
|
|
placeholder={t('z.B. aus ClickUp-URL: list/123456789')}
|
|
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 }}>
|
|
{t('Liefert beim Ausfüllen denselben Wert wie ein ClickUp-Relationship-Feld:')}{' '}
|
|
<code>{'{ add: [taskId], rem: [] }'}</code>{' '}
|
|
{t('— 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 }])
|
|
}
|
|
>
|
|
+ {t('Feld')}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|