Some checks failed
Deploy Nyla Frontend to Integration / deploy (push) Failing after 56s
158 lines
5.6 KiB
TypeScript
158 lines
5.6 KiB
TypeScript
// Copyright (c) 2026 PowerOn AG
|
|
// All rights reserved.
|
|
/**
|
|
* Form node config - draggable fields, types, required toggle
|
|
*/
|
|
|
|
import React from 'react';
|
|
import { FaGripVertical, FaTimes } from 'react-icons/fa';
|
|
import type { FormField, NodeConfigRendererProps } from '../shared/types';
|
|
import { FORM_FIELD_TYPES, FORM_FIELD_TYPE_LABELS } from '../../../../utils/attributeTypeMapper';
|
|
import styles from '../../editor/WorkflowFlowEditor.module.css';
|
|
import { useWorkflowDataFlow } from '../../context/WorkflowDataFlowContext';
|
|
import { FormFieldOptionsEditor } from './FormFieldOptionsEditor';
|
|
import {
|
|
deriveFormFieldPayloadKey,
|
|
formFieldTypeHasConfigurableOptions,
|
|
normalizeFormFieldOptions,
|
|
} from './formFieldOptionsUtils';
|
|
|
|
import { useLanguage } from '../../../../providers/language/LanguageContext';
|
|
|
|
export const FormNodeConfig: React.FC<NodeConfigRendererProps> = ({ params, updateParam }) => {
|
|
const { t } = useLanguage();
|
|
const ctx = useWorkflowDataFlow();
|
|
const fieldTypeOptions = ctx?.formFieldTypes?.length
|
|
? ctx.formFieldTypes
|
|
: FORM_FIELD_TYPES.map((ft) => ({ id: ft, label: FORM_FIELD_TYPE_LABELS[ft] ?? ft, portType: 'str' }));
|
|
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);
|
|
};
|
|
|
|
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('Bezeichnung')}
|
|
value={f.label ?? ''}
|
|
onChange={(e) => {
|
|
const label = e.target.value;
|
|
const next = [...fields];
|
|
next[i] = { ...next[i], label, name: deriveFormFieldPayloadKey(label, i) };
|
|
updateParam('fields', next);
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className={styles.formFieldRowFooter}>
|
|
<select
|
|
value={f.type ?? 'text'}
|
|
onChange={(e) => {
|
|
const next = [...fields];
|
|
const type = e.target.value as FormField['type'];
|
|
const row: FormField = { ...f, type };
|
|
if (formFieldTypeHasConfigurableOptions(type)) {
|
|
row.options = normalizeFormFieldOptions(row.options);
|
|
}
|
|
next[i] = row;
|
|
updateParam('fields', next);
|
|
}}
|
|
style={{ width: 'auto', minWidth: 90 }}
|
|
>
|
|
{fieldTypeOptions.map((ft) => (
|
|
<option key={ft.id} value={ft.id}>{t(ft.label)}</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>
|
|
{formFieldTypeHasConfigurableOptions(f.type) ? (
|
|
<FormFieldOptionsEditor
|
|
className={styles.formFieldOptionsBlock}
|
|
options={normalizeFormFieldOptions(f.options)}
|
|
onChange={(opts) => {
|
|
const next = [...fields];
|
|
next[i] = { ...next[i], options: opts };
|
|
updateParam('fields', next);
|
|
}}
|
|
/>
|
|
) : null}
|
|
</div>
|
|
))}
|
|
<button
|
|
type="button"
|
|
onClick={() =>
|
|
updateParam('fields', [
|
|
...fields,
|
|
{
|
|
name: deriveFormFieldPayloadKey('', fields.length),
|
|
type: 'text',
|
|
label: '',
|
|
required: false,
|
|
},
|
|
])
|
|
}
|
|
>
|
|
+ {t('Feld')}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|