ui-nyla/src/components/FlowEditor/workflowRuntime/WorkflowRuntimeFormFields.tsx
ValueOn AG f35e22c7f4
Some checks failed
Deploy Nyla Frontend to Production / build-and-deploy (push) Failing after 2s
Sync: full codebase from GitHub frontend_nyla main
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 23:54:30 +02:00

236 lines
7.1 KiB
TypeScript

/**
* Runtime form fields — shared by Human Task input.form and workflow list trigger.form start.
* Field rows match task.config.fields / graph node parameters.formFields shape from the backend.
*/
import React, { useEffect, useState } from 'react';
import { useApiRequest } from '../../../hooks/useApi';
import { loadClickupListTasksForDropdown, type ApiRequestFunction } from '../../../api/workflowApi';
import { normalizeFormFieldOptions } from '../nodes/form';
import { useLanguage } from '../../../providers/language/LanguageContext';
export type WorkflowRuntimeFormFieldRow = {
name: string;
type: string;
label: string;
required?: boolean;
options?: unknown;
clickupConnectionId?: string;
clickupListId?: string;
clickupStatusOptions?: Array<{ value: string; label: string }>;
};
export function relationshipTaskIdFromFormValue(v: unknown): string {
if (v && typeof v === 'object' && !Array.isArray(v) && 'add' in v) {
const a = (v as { add?: unknown[] }).add;
if (Array.isArray(a) && a[0] != null && String(a[0]).trim()) return String(a[0]);
}
return '';
}
function InputFormClickupTaskField({
connectionId,
listId,
value,
onChange,
request,
}: {
connectionId: string;
listId: string;
value: unknown;
onChange: (v: unknown) => void;
request: ApiRequestFunction;
}) {
const { t } = useLanguage();
const [tasks, setTasks] = useState<Array<{ id: string; name: string }>>([]);
const [loading, setLoading] = useState(false);
const [err, setErr] = useState<string | null>(null);
useEffect(() => {
const cid = connectionId.trim();
const lid = listId.trim();
if (!cid || !lid) {
setTasks([]);
return;
}
let cancelled = false;
setLoading(true);
setErr(null);
loadClickupListTasksForDropdown(request, cid, lid)
.then((rows) => {
if (!cancelled) setTasks(rows);
})
.catch(() => {
if (!cancelled) {
setTasks([]);
setErr(t('Aufgaben konnten nicht geladen werden.'));
}
})
.finally(() => {
if (!cancelled) setLoading(false);
});
return () => {
cancelled = true;
};
}, [request, connectionId, listId, t]);
const sel = relationshipTaskIdFromFormValue(value);
if (!connectionId.trim() || !listId.trim()) {
return (
<p style={{ fontSize: '0.85rem', color: 'var(--text-secondary, #666)' }}>
{t('Für dieses Feld sind im Formular-Node ClickUp-Verbindung und Listen-ID gesetzt — bitte Workflow prüfen.')}
</p>
);
}
return (
<>
{err ? (
<p style={{ fontSize: '0.85rem', color: 'var(--text-secondary, #c00)' }}>{err}</p>
) : null}
{loading ? (
<p style={{ fontSize: '0.85rem', color: 'var(--text-secondary, #666)' }}>{t('lade Aufgaben')}</p>
) : (
<select
value={sel}
onChange={(e) => {
const tid = e.target.value;
if (!tid) onChange({ add: [], rem: [] });
else onChange({ add: [tid], rem: [] });
}}
>
<option value="">{t('Aufgabe wählen')}</option>
{tasks.map((taskRow) => (
<option key={taskRow.id} value={taskRow.id}>
{taskRow.name}
</option>
))}
</select>
)}
</>
);
}
export function useWorkflowRuntimeFormRequiredOk(
fields: WorkflowRuntimeFormFieldRow[],
formData: Record<string, unknown>
): boolean {
const requiredFields = fields.filter((f) => f.required);
return requiredFields.every((f) => {
const v = formData[f.name];
if (f.type === 'boolean') return true;
if (f.type === 'clickup_tasks') {
return relationshipTaskIdFromFormValue(v) !== '';
}
if (f.type === 'clickup_status') {
return v !== undefined && v !== null && String(v).trim() !== '';
}
if (
(f.type === 'select' || f.type === 'enum') &&
normalizeFormFieldOptions(f.options).some((o) => String(o.value).trim() !== '')
) {
return v !== undefined && v !== null && String(v).trim() !== '';
}
return v !== undefined && v !== null && String(v).trim() !== '';
});
}
export interface WorkflowRuntimeFormFieldsProps {
fields: WorkflowRuntimeFormFieldRow[];
formData: Record<string, unknown>;
setFormData: React.Dispatch<React.SetStateAction<Record<string, unknown>>>;
formFieldsClassName: string;
}
/**
* Renders the same controls as TaskCard input.form (no Popup — parent wraps if needed).
*/
export const WorkflowRuntimeFormFields: React.FC<WorkflowRuntimeFormFieldsProps> = ({
fields,
formData,
setFormData,
formFieldsClassName,
}) => {
const { t } = useLanguage();
const { request } = useApiRequest();
const renderFormControl = (field: WorkflowRuntimeFormFieldRow): React.ReactNode => {
const selectChoices = normalizeFormFieldOptions(field.options).filter((o) => String(o.value).trim() !== '');
if (field.type === 'boolean') {
return (
<input
type="checkbox"
checked={(formData[field.name] as boolean) ?? false}
onChange={(e) => setFormData((p) => ({ ...p, [field.name]: e.target.checked }))}
/>
);
}
if (field.type === 'clickup_tasks' && request) {
return (
<InputFormClickupTaskField
connectionId={field.clickupConnectionId ?? ''}
listId={field.clickupListId ?? ''}
value={formData[field.name]}
onChange={(v) => setFormData((p) => ({ ...p, [field.name]: v }))}
request={request}
/>
);
}
if (
field.type === 'clickup_status' &&
Array.isArray(field.clickupStatusOptions) &&
field.clickupStatusOptions.length > 0
) {
return (
<select
value={(formData[field.name] as string) ?? ''}
onChange={(e) => setFormData((p) => ({ ...p, [field.name]: e.target.value }))}
>
<option value="">{t('Status wählen')}</option>
{field.clickupStatusOptions.map((o) => (
<option key={o.value} value={o.value}>
{o.label}
</option>
))}
</select>
);
}
if ((field.type === 'select' || field.type === 'enum') && selectChoices.length > 0) {
return (
<select
value={(formData[field.name] as string) ?? ''}
onChange={(e) => setFormData((p) => ({ ...p, [field.name]: e.target.value }))}
>
<option value="">{t('Bitte wählen')}</option>
{selectChoices.map((o) => (
<option key={o.value} value={o.value}>
{o.label || o.value}
</option>
))}
</select>
);
}
return (
<input
type={field.type === 'number' ? 'number' : field.type === 'date' ? 'date' : 'text'}
value={(formData[field.name] as string) ?? ''}
onChange={(e) => setFormData((p) => ({ ...p, [field.name]: e.target.value }))}
/>
);
};
return (
<div className={formFieldsClassName}>
{fields.map((f) => (
<div key={f.name}>
<label>
{f.label || f.name}
{f.required && ' *'}
</label>
{renderFormControl(f)}
</div>
))}
</div>
);
};