Some checks failed
Deploy Nyla Frontend to Production / build-and-deploy (push) Failing after 2s
Co-authored-by: Cursor <cursoragent@cursor.com>
236 lines
7.1 KiB
TypeScript
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>
|
|
);
|
|
};
|