fix: formular trigger
This commit is contained in:
parent
41eaa63d49
commit
2e6fce188d
2 changed files with 363 additions and 214 deletions
|
|
@ -0,0 +1,236 @@
|
||||||
|
/**
|
||||||
|
* 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -17,18 +17,20 @@ import {
|
||||||
fetchCompletedRuns,
|
fetchCompletedRuns,
|
||||||
fetchWorkflows,
|
fetchWorkflows,
|
||||||
executeGraph,
|
executeGraph,
|
||||||
loadClickupListTasksForDropdown,
|
|
||||||
type Automation2Task,
|
type Automation2Task,
|
||||||
type Automation2Workflow,
|
type Automation2Workflow,
|
||||||
type CompletedRun,
|
type CompletedRun,
|
||||||
type ApiRequestFunction,
|
|
||||||
} from '../../../api/workflowApi';
|
} from '../../../api/workflowApi';
|
||||||
import { useToast } from '../../../contexts/ToastContext';
|
import { useToast } from '../../../contexts/ToastContext';
|
||||||
import { Popup } from '../../../components/UiComponents/Popup';
|
import { Popup } from '../../../components/UiComponents/Popup';
|
||||||
import { getAcceptStringFromConfig, fileMatchesAccept } from '../../../components/FlowEditor';
|
import { getAcceptStringFromConfig, fileMatchesAccept } from '../../../components/FlowEditor';
|
||||||
import { useFileOperations } from '../../../hooks/useFiles';
|
import { useFileOperations } from '../../../hooks/useFiles';
|
||||||
import styles from './Automation2WorkflowsTasks.module.css';
|
import styles from './Automation2WorkflowsTasks.module.css';
|
||||||
import { normalizeFormFieldOptions } from '../../../components/FlowEditor/nodes/form';
|
import {
|
||||||
|
WorkflowRuntimeFormFields,
|
||||||
|
useWorkflowRuntimeFormRequiredOk,
|
||||||
|
type WorkflowRuntimeFormFieldRow,
|
||||||
|
} from '../../../components/FlowEditor/workflowRuntime/WorkflowRuntimeFormFields';
|
||||||
|
|
||||||
import { useLanguage } from '../../../providers/language/LanguageContext';
|
import { useLanguage } from '../../../providers/language/LanguageContext';
|
||||||
|
|
||||||
|
|
@ -77,17 +79,38 @@ function hasManualOrFormInvocation(wf: Automation2Workflow): boolean {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Primary entry for execute — POST /api/workflows/{instanceId}/execute with collected inputs.
|
* Primary entry for execute — align with first start node in graph order (backend-driven),
|
||||||
* (manual first, then form or api).
|
* then fall back to manual / form / api on invocations list.
|
||||||
*/
|
*/
|
||||||
function getPrimaryEntryPoint(wf: Automation2Workflow) {
|
function getPrimaryEntryPoint(wf: Automation2Workflow) {
|
||||||
const invs = wf.invocations || [];
|
const invs = wf.invocations || [];
|
||||||
|
const nodes = wf.graph?.nodes ?? [];
|
||||||
|
for (const n of nodes) {
|
||||||
|
const nodeType = n.type;
|
||||||
|
if (typeof nodeType === 'string' && nodeType.startsWith('trigger.')) {
|
||||||
|
const inv = invs.find((i) => i.enabled !== false && i.id === n.id);
|
||||||
|
if (inv) return inv;
|
||||||
|
}
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
invs.find((i) => i.enabled !== false && i.kind === 'manual') ||
|
invs.find((i) => i.enabled !== false && i.kind === 'manual') ||
|
||||||
invs.find((i) => i.enabled !== false && (i.kind === 'form' || i.kind === 'api'))
|
invs.find((i) => i.enabled !== false && (i.kind === 'form' || i.kind === 'api'))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Form field rows from graph trigger.form for workflow list (parameters.formFields). */
|
||||||
|
function getTriggerFormFieldsForWorkflow(wf: Automation2Workflow): WorkflowRuntimeFormFieldRow[] {
|
||||||
|
const primary = getPrimaryEntryPoint(wf);
|
||||||
|
if (!primary || primary.kind !== 'form') return [];
|
||||||
|
const nodes = wf.graph?.nodes ?? [];
|
||||||
|
let node = nodes.find((n) => n.id === primary.id && n.type === 'trigger.form');
|
||||||
|
if (!node) node = nodes.find((n) => n.type === 'trigger.form');
|
||||||
|
if (!node) return [];
|
||||||
|
const raw = (node.parameters as Record<string, unknown> | undefined)?.formFields;
|
||||||
|
if (!Array.isArray(raw)) return [];
|
||||||
|
return raw as WorkflowRuntimeFormFieldRow[];
|
||||||
|
}
|
||||||
|
|
||||||
function primaryKindLabel(kind: string): string {
|
function primaryKindLabel(kind: string): string {
|
||||||
if (kind === 'form') return 'Formular';
|
if (kind === 'form') return 'Formular';
|
||||||
if (kind === 'manual') return 'Manuell';
|
if (kind === 'manual') return 'Manuell';
|
||||||
|
|
@ -109,6 +132,9 @@ export const GraphicalEditorWorkflowsTasksPage: React.FC = () => {
|
||||||
const [submitting, setSubmitting] = useState<string | null>(null);
|
const [submitting, setSubmitting] = useState<string | null>(null);
|
||||||
const [dismissingTaskId, setDismissingTaskId] = useState<string | null>(null);
|
const [dismissingTaskId, setDismissingTaskId] = useState<string | null>(null);
|
||||||
const [executingWorkflowId, setExecutingWorkflowId] = useState<string | null>(null);
|
const [executingWorkflowId, setExecutingWorkflowId] = useState<string | null>(null);
|
||||||
|
const [formStartWorkflow, setFormStartWorkflow] = useState<Automation2Workflow | null>(null);
|
||||||
|
const [formStartFields, setFormStartFields] = useState<WorkflowRuntimeFormFieldRow[]>([]);
|
||||||
|
const [startFormData, setStartFormData] = useState<Record<string, unknown>>({});
|
||||||
|
|
||||||
const load = useCallback(async () => {
|
const load = useCallback(async () => {
|
||||||
if (!instanceId) return;
|
if (!instanceId) return;
|
||||||
|
|
@ -185,6 +211,12 @@ export const GraphicalEditorWorkflowsTasksPage: React.FC = () => {
|
||||||
async (wf: Automation2Workflow) => {
|
async (wf: Automation2Workflow) => {
|
||||||
if (!instanceId || !wf.graph) return;
|
if (!instanceId || !wf.graph) return;
|
||||||
const primary = getPrimaryEntryPoint(wf);
|
const primary = getPrimaryEntryPoint(wf);
|
||||||
|
if (primary?.kind === 'form') {
|
||||||
|
setFormStartFields(getTriggerFormFieldsForWorkflow(wf));
|
||||||
|
setStartFormData({});
|
||||||
|
setFormStartWorkflow(wf);
|
||||||
|
return;
|
||||||
|
}
|
||||||
setExecutingWorkflowId(wf.id);
|
setExecutingWorkflowId(wf.id);
|
||||||
try {
|
try {
|
||||||
const result = await executeGraph(request, instanceId, wf.graph, wf.id, {
|
const result = await executeGraph(request, instanceId, wf.graph, wf.id, {
|
||||||
|
|
@ -211,6 +243,48 @@ export const GraphicalEditorWorkflowsTasksPage: React.FC = () => {
|
||||||
[instanceId, request, showSuccess, showError, load, t]
|
[instanceId, request, showSuccess, showError, load, t]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const formStartRequiredOk = useWorkflowRuntimeFormRequiredOk(formStartFields, startFormData);
|
||||||
|
|
||||||
|
const handleFormStartSubmit = useCallback(async () => {
|
||||||
|
if (!instanceId || !formStartWorkflow?.graph) return;
|
||||||
|
const wf = formStartWorkflow;
|
||||||
|
const primary = getPrimaryEntryPoint(wf);
|
||||||
|
const payload = { ...startFormData };
|
||||||
|
setExecutingWorkflowId(wf.id);
|
||||||
|
try {
|
||||||
|
const result = await executeGraph(request, instanceId, wf.graph, wf.id, {
|
||||||
|
...(primary ? { entryPointId: primary.id } : {}),
|
||||||
|
payload,
|
||||||
|
});
|
||||||
|
if (result?.success) {
|
||||||
|
if (result?.paused) {
|
||||||
|
showSuccess(t('Workflow gestartet und bei Human Task pausiert.'));
|
||||||
|
} else {
|
||||||
|
showSuccess(t('Workflow gestartet'));
|
||||||
|
}
|
||||||
|
await load();
|
||||||
|
} else {
|
||||||
|
showError(result?.error || t('Ausführung fehlgeschlagen'));
|
||||||
|
}
|
||||||
|
} catch (e: unknown) {
|
||||||
|
const msg =
|
||||||
|
(e as { message?: string })?.message ?? t('Ausführung fehlgeschlagen');
|
||||||
|
showError(msg);
|
||||||
|
} finally {
|
||||||
|
setExecutingWorkflowId(null);
|
||||||
|
setFormStartWorkflow(null);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
instanceId,
|
||||||
|
formStartWorkflow,
|
||||||
|
startFormData,
|
||||||
|
request,
|
||||||
|
showSuccess,
|
||||||
|
showError,
|
||||||
|
load,
|
||||||
|
t,
|
||||||
|
]);
|
||||||
|
|
||||||
const openTasks = tasks.filter((task) => task.status === 'pending');
|
const openTasks = tasks.filter((task) => task.status === 'pending');
|
||||||
const completedTasks = tasks.filter((task) => task.status !== 'pending');
|
const completedTasks = tasks.filter((task) => task.status !== 'pending');
|
||||||
|
|
||||||
|
|
@ -364,6 +438,41 @@ export const GraphicalEditorWorkflowsTasksPage: React.FC = () => {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
|
<Popup
|
||||||
|
isOpen={formStartWorkflow != null}
|
||||||
|
title={t('Formular ausfüllen')}
|
||||||
|
onClose={() => setFormStartWorkflow(null)}
|
||||||
|
closable={
|
||||||
|
!(formStartWorkflow != null && executingWorkflowId === formStartWorkflow.id)
|
||||||
|
}
|
||||||
|
closeOnEscape={
|
||||||
|
!(formStartWorkflow != null && executingWorkflowId === formStartWorkflow.id)
|
||||||
|
}
|
||||||
|
size="medium"
|
||||||
|
footerContent={
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => void handleFormStartSubmit()}
|
||||||
|
disabled={
|
||||||
|
!formStartRequiredOk ||
|
||||||
|
(formStartWorkflow != null && executingWorkflowId === formStartWorkflow.id)
|
||||||
|
}
|
||||||
|
className={styles.popupSubmitButton}
|
||||||
|
>
|
||||||
|
{formStartWorkflow != null && executingWorkflowId === formStartWorkflow.id
|
||||||
|
? t('wird gesendet')
|
||||||
|
: t('absenden')}
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<WorkflowRuntimeFormFields
|
||||||
|
fields={formStartFields}
|
||||||
|
formData={startFormData}
|
||||||
|
setFormData={setStartFormData}
|
||||||
|
formFieldsClassName={styles.formFields}
|
||||||
|
/>
|
||||||
|
</Popup>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
@ -439,99 +548,6 @@ interface TaskCardProps {
|
||||||
dismissing?: boolean;
|
dismissing?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Check if file matches accept string (e.g. ".pdf,image/*"). */
|
|
||||||
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]);
|
|
||||||
|
|
||||||
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>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const TaskCard: React.FC<TaskCardProps> = ({
|
const TaskCard: React.FC<TaskCardProps> = ({
|
||||||
task,
|
task,
|
||||||
instanceId,
|
instanceId,
|
||||||
|
|
@ -555,6 +571,12 @@ const TaskCard: React.FC<TaskCardProps> = ({
|
||||||
const nodeType = task.nodeType;
|
const nodeType = task.nodeType;
|
||||||
const stepLabel = getNodeStepLabel(config);
|
const stepLabel = getNodeStepLabel(config);
|
||||||
|
|
||||||
|
const inputFormFields: WorkflowRuntimeFormFieldRow[] =
|
||||||
|
nodeType === 'input.form'
|
||||||
|
? ((config.fields as WorkflowRuntimeFormFieldRow[]) ?? [])
|
||||||
|
: [];
|
||||||
|
const inputFormRequiredOk = useWorkflowRuntimeFormRequiredOk(inputFormFields, formData);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setUploadedFiles([]);
|
setUploadedFiles([]);
|
||||||
setUploadError(null);
|
setUploadError(null);
|
||||||
|
|
@ -564,122 +586,13 @@ const TaskCard: React.FC<TaskCardProps> = ({
|
||||||
if (readOnly) return null;
|
if (readOnly) return null;
|
||||||
switch (nodeType) {
|
switch (nodeType) {
|
||||||
case 'input.form': {
|
case 'input.form': {
|
||||||
const fields =
|
|
||||||
(config.fields as Array<{
|
|
||||||
name: string;
|
|
||||||
type: string;
|
|
||||||
label: string;
|
|
||||||
required?: boolean;
|
|
||||||
options?: unknown;
|
|
||||||
clickupConnectionId?: string;
|
|
||||||
clickupListId?: string;
|
|
||||||
clickupStatusOptions?: Array<{ value: string; label: string }>;
|
|
||||||
}>) ?? [];
|
|
||||||
const requiredFields = fields.filter((f) => f.required);
|
|
||||||
const allRequiredFilled = 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() !== '';
|
|
||||||
});
|
|
||||||
const renderFormControl = (
|
|
||||||
field: (typeof fields)[number],
|
|
||||||
): 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 }))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
const formContent = (
|
const formContent = (
|
||||||
<div className={styles.formFields}>
|
<WorkflowRuntimeFormFields
|
||||||
{fields.map((f) => (
|
fields={inputFormFields}
|
||||||
<div key={f.name}>
|
formData={formData}
|
||||||
<label>
|
setFormData={setFormData}
|
||||||
{f.label || f.name}
|
formFieldsClassName={styles.formFields}
|
||||||
{f.required && ' *'}
|
/>
|
||||||
</label>
|
|
||||||
{renderFormControl(f)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
@ -704,7 +617,7 @@ const TaskCard: React.FC<TaskCardProps> = ({
|
||||||
onSubmit({ payload: formData });
|
onSubmit({ payload: formData });
|
||||||
setFormPopupOpen(false);
|
setFormPopupOpen(false);
|
||||||
}}
|
}}
|
||||||
disabled={submitting || !allRequiredFilled}
|
disabled={submitting || !inputFormRequiredOk}
|
||||||
className={styles.popupSubmitButton}
|
className={styles.popupSubmitButton}
|
||||||
>
|
>
|
||||||
{submitting ? t('wird gesendet') : t('absenden')}
|
{submitting ? t('wird gesendet') : t('absenden')}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue